mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +00:00
Content warnings
This commit is contained in:
parent
da62bb6743
commit
fa4d666f8d
27 changed files with 329 additions and 84 deletions
|
@ -62,6 +62,10 @@ extension ContentDatabase {
|
||||||
t.column("pinned", .boolean)
|
t.column("pinned", .boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try db.create(table: "statusShowMoreToggle") { t in
|
||||||
|
t.column("statusId", .text).primaryKey().references("statusRecord", onDelete: .cascade)
|
||||||
|
}
|
||||||
|
|
||||||
try db.create(table: "timelineRecord") { t in
|
try db.create(table: "timelineRecord") { t in
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("listId", .text)
|
t.column("listId", .text)
|
||||||
|
|
|
@ -149,6 +149,39 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher {
|
||||||
|
if let toggle = try StatusShowMoreToggle
|
||||||
|
.filter(StatusShowMoreToggle.Columns.statusId == id)
|
||||||
|
.fetchOne($0) {
|
||||||
|
try toggle.delete($0)
|
||||||
|
} else {
|
||||||
|
try StatusShowMoreToggle(statusId: id).save($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func showMore(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher {
|
||||||
|
for id in ids {
|
||||||
|
try StatusShowMoreToggle(statusId: id).save($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func showLess(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher(
|
||||||
|
updates: StatusShowMoreToggle
|
||||||
|
.filter(ids.contains(StatusShowMoreToggle.Columns.statusId))
|
||||||
|
.deleteAll)
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
try list.save($0)
|
try list.save($0)
|
||||||
|
|
|
@ -35,7 +35,8 @@ extension ContextItemsInfo {
|
||||||
|
|
||||||
return .status(
|
return .status(
|
||||||
.init(info: statusInfo),
|
.init(info: statusInfo),
|
||||||
.init(isContextParent: statusInfo.record.id == parent.record.id,
|
.init(showMoreToggled: statusInfo.showMoreToggled,
|
||||||
|
isContextParent: statusInfo.record.id == parent.record.id,
|
||||||
isReplyInContext: isReplyInContext,
|
isReplyInContext: isReplyInContext,
|
||||||
hasReplyFollowing: hasReplyFollowing))
|
hasReplyFollowing: hasReplyFollowing))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ struct StatusAncestorJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
let parentId: Status.Id
|
let parentId: Status.Id
|
||||||
let statusId: Status.Id
|
let statusId: Status.Id
|
||||||
let index: Int
|
let index: Int
|
||||||
|
|
||||||
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusAncestorJoin {
|
extension StatusAncestorJoin {
|
||||||
|
@ -18,4 +16,6 @@ extension StatusAncestorJoin {
|
||||||
static let statusId = Column(StatusAncestorJoin.CodingKeys.statusId)
|
static let statusId = Column(StatusAncestorJoin.CodingKeys.statusId)
|
||||||
static let index = Column(StatusAncestorJoin.CodingKeys.index)
|
static let index = Column(StatusAncestorJoin.CodingKeys.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ struct StatusDescendantJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
let parentId: Status.Id
|
let parentId: Status.Id
|
||||||
let statusId: Status.Id
|
let statusId: Status.Id
|
||||||
let index: Int
|
let index: Int
|
||||||
|
|
||||||
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusDescendantJoin {
|
extension StatusDescendantJoin {
|
||||||
|
@ -18,4 +16,6 @@ extension StatusDescendantJoin {
|
||||||
static let statusId = Column(StatusDescendantJoin.CodingKeys.statusId)
|
static let statusId = Column(StatusDescendantJoin.CodingKeys.statusId)
|
||||||
static let index = Column(StatusDescendantJoin.CodingKeys.index)
|
static let index = Column(StatusDescendantJoin.CodingKeys.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ struct StatusInfo: Codable, Hashable, FetchableRecord {
|
||||||
let accountInfo: AccountInfo
|
let accountInfo: AccountInfo
|
||||||
let reblogAccountInfo: AccountInfo?
|
let reblogAccountInfo: AccountInfo?
|
||||||
let reblogRecord: StatusRecord?
|
let reblogRecord: StatusRecord?
|
||||||
|
let showMoreToggle: StatusShowMoreToggle?
|
||||||
|
let reblogShowMoreToggle: StatusShowMoreToggle?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusInfo {
|
extension StatusInfo {
|
||||||
|
@ -16,6 +18,9 @@ extension StatusInfo {
|
||||||
.including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount)
|
.including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount)
|
||||||
.forKey(CodingKeys.reblogAccountInfo))
|
.forKey(CodingKeys.reblogAccountInfo))
|
||||||
.including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord))
|
.including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord))
|
||||||
|
.including(optional: StatusRecord.showMoreToggle.forKey(CodingKeys.showMoreToggle))
|
||||||
|
.including(optional: StatusRecord.reblogShowMoreToggle
|
||||||
|
.forKey(CodingKeys.reblogShowMoreToggle))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func request(_ request: QueryInterfaceRequest<StatusRecord>) -> QueryInterfaceRequest<Self> {
|
static func request(_ request: QueryInterfaceRequest<StatusRecord>) -> QueryInterfaceRequest<Self> {
|
||||||
|
@ -25,4 +30,8 @@ extension StatusInfo {
|
||||||
var filterableContent: String {
|
var filterableContent: String {
|
||||||
(record.filterableContent + (reblogRecord?.filterableContent ?? [])).joined(separator: " ")
|
(record.filterableContent + (reblogRecord?.filterableContent ?? [])).joined(separator: " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showMoreToggled: Bool {
|
||||||
|
showMoreToggle != nil || reblogShowMoreToggle != nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,6 +92,11 @@ extension StatusRecord {
|
||||||
through: Self.reblogAccount,
|
through: Self.reblogAccount,
|
||||||
using: AccountRecord.moved)
|
using: AccountRecord.moved)
|
||||||
static let reblog = belongsTo(StatusRecord.self)
|
static let reblog = belongsTo(StatusRecord.self)
|
||||||
|
static let showMoreToggle = hasOne(StatusShowMoreToggle.self)
|
||||||
|
static let reblogShowMoreToggle = hasOne(
|
||||||
|
StatusShowMoreToggle.self,
|
||||||
|
through: Self.reblog,
|
||||||
|
using: Self.showMoreToggle)
|
||||||
static let ancestorJoins = hasMany(
|
static let ancestorJoins = hasMany(
|
||||||
StatusAncestorJoin.self,
|
StatusAncestorJoin.self,
|
||||||
using: ForeignKey([StatusAncestorJoin.Columns.parentId]))
|
using: ForeignKey([StatusAncestorJoin.Columns.parentId]))
|
||||||
|
|
25
DB/Sources/DB/Content/StatusShowMoreToggle.swift
Normal file
25
DB/Sources/DB/Content/StatusShowMoreToggle.swift
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct StatusShowMoreToggle: Codable, Hashable {
|
||||||
|
let statusId: Status.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusShowMoreToggle {
|
||||||
|
enum Columns {
|
||||||
|
static let statusId = Column(StatusShowMoreToggle.CodingKeys.statusId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusShowMoreToggle: FetchableRecord, PersistableRecord {
|
||||||
|
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||||
|
MastodonDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||||
|
MastodonEncoder()
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,11 @@ extension TimelineItemsInfo {
|
||||||
let timeline = Timeline(record: timelineRecord)!
|
let timeline = Timeline(record: timelineRecord)!
|
||||||
let filterRegularExpression = filters.regularExpression(context: timeline.filterContext)
|
let filterRegularExpression = filters.regularExpression(context: timeline.filterContext)
|
||||||
var timelineItems = statusInfos.filtered(regularExpression: filterRegularExpression)
|
var timelineItems = statusInfos.filtered(regularExpression: filterRegularExpression)
|
||||||
.map { CollectionItem.status(.init(info: $0), .init()) }
|
.map {
|
||||||
|
CollectionItem.status(
|
||||||
|
.init(info: $0),
|
||||||
|
.init(showMoreToggled: $0.showMoreToggled))
|
||||||
|
}
|
||||||
|
|
||||||
for loadMoreRecord in loadMoreRecords {
|
for loadMoreRecord in loadMoreRecords {
|
||||||
guard let index = timelineItems.firstIndex(where: {
|
guard let index = timelineItems.firstIndex(where: {
|
||||||
|
@ -51,7 +55,11 @@ extension TimelineItemsInfo {
|
||||||
|
|
||||||
if let pinnedStatusInfos = pinnedStatusesInfo?.pinnedStatusInfos {
|
if let pinnedStatusInfos = pinnedStatusesInfo?.pinnedStatusInfos {
|
||||||
return [pinnedStatusInfos.filtered(regularExpression: filterRegularExpression)
|
return [pinnedStatusInfos.filtered(regularExpression: filterRegularExpression)
|
||||||
.map { CollectionItem.status(.init(info: $0), .init(isPinned: true)) },
|
.map {
|
||||||
|
CollectionItem.status(
|
||||||
|
.init(info: $0),
|
||||||
|
.init(showMoreToggled: $0.showMoreToggled, isPinned: true))
|
||||||
|
},
|
||||||
timelineItems]
|
timelineItems]
|
||||||
} else {
|
} else {
|
||||||
return [timelineItems]
|
return [timelineItems]
|
||||||
|
|
|
@ -10,15 +10,18 @@ public enum CollectionItem: Hashable {
|
||||||
|
|
||||||
public extension CollectionItem {
|
public extension CollectionItem {
|
||||||
struct StatusConfiguration: Hashable {
|
struct StatusConfiguration: Hashable {
|
||||||
|
public let showMoreToggled: Bool
|
||||||
public let isContextParent: Bool
|
public let isContextParent: Bool
|
||||||
public let isPinned: Bool
|
public let isPinned: Bool
|
||||||
public let isReplyInContext: Bool
|
public let isReplyInContext: Bool
|
||||||
public let hasReplyFollowing: Bool
|
public let hasReplyFollowing: Bool
|
||||||
|
|
||||||
init(isContextParent: Bool = false,
|
init(showMoreToggled: Bool,
|
||||||
|
isContextParent: Bool = false,
|
||||||
isPinned: Bool = false,
|
isPinned: Bool = false,
|
||||||
isReplyInContext: Bool = false,
|
isReplyInContext: Bool = false,
|
||||||
hasReplyFollowing: Bool = false) {
|
hasReplyFollowing: Bool = false) {
|
||||||
|
self.showMoreToggled = showMoreToggled
|
||||||
self.isContextParent = isContextParent
|
self.isContextParent = isContextParent
|
||||||
self.isPinned = isPinned
|
self.isPinned = isPinned
|
||||||
self.isReplyInContext = isReplyInContext
|
self.isReplyInContext = isReplyInContext
|
||||||
|
@ -28,5 +31,5 @@ public extension CollectionItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension CollectionItem.StatusConfiguration {
|
public extension CollectionItem.StatusConfiguration {
|
||||||
static let `default` = Self()
|
static let `default` = Self(showMoreToggled: false)
|
||||||
}
|
}
|
||||||
|
|
49
Data Sources/TableViewDataSource.swift
Normal file
49
Data Sources/TableViewDataSource.swift
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionItemIdentifier> {
|
||||||
|
private let updateQueue =
|
||||||
|
DispatchQueue(label: "com.metabolist.metatext.collection-data-source.update-queue")
|
||||||
|
|
||||||
|
init(tableView: UITableView, viewModelProvider: @escaping (IndexPath) -> CollectionItemViewModel) {
|
||||||
|
for kind in CollectionItemIdentifier.Kind.allCases {
|
||||||
|
tableView.register(kind.cellClass, forCellReuseIdentifier: String(describing: kind.cellClass))
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(tableView: tableView) { tableView, indexPath, identifier in
|
||||||
|
let cell = tableView.dequeueReusableCell(
|
||||||
|
withIdentifier: String(describing: identifier.kind.cellClass),
|
||||||
|
for: indexPath)
|
||||||
|
|
||||||
|
switch (cell, viewModelProvider(indexPath)) {
|
||||||
|
case let (statusListCell as StatusListCell, statusViewModel as StatusViewModel):
|
||||||
|
statusListCell.viewModel = statusViewModel
|
||||||
|
case let (accountListCell as AccountListCell, accountViewModel as AccountViewModel):
|
||||||
|
accountListCell.viewModel = accountViewModel
|
||||||
|
case let (loadMoreCell as LoadMoreCell, loadMoreViewModel as LoadMoreViewModel):
|
||||||
|
loadMoreCell.viewModel = loadMoreViewModel
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRowAnimation = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<Int, CollectionItemIdentifier>,
|
||||||
|
animatingDifferences: Bool = true,
|
||||||
|
completion: (() -> Void)? = nil) {
|
||||||
|
let differenceExceptShowMoreToggled = self.snapshot().itemIdentifiers.difference(
|
||||||
|
from: snapshot.itemIdentifiers,
|
||||||
|
by: CollectionItemIdentifier.isSameExceptShowMoreToggled(lhs:rhs:))
|
||||||
|
let animated = snapshot.itemIdentifiers.count > 0 && differenceExceptShowMoreToggled.count == 0
|
||||||
|
|
||||||
|
updateQueue.async {
|
||||||
|
super.apply(snapshot, animatingDifferences: animated, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@
|
||||||
D0625E5F250F0CFF00502611 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5E250F0CFF00502611 /* StatusView.swift */; };
|
D0625E5F250F0CFF00502611 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5E250F0CFF00502611 /* StatusView.swift */; };
|
||||||
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
||||||
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
||||||
D0B7434925100DBB00C13DB6 /* StatusView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D0B7434825100DBB00C13DB6 /* StatusView.xib */; };
|
D0B7434925100DBB00C13DB6 /* StatusView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D0B7434825100DBB00C13DB6 /* StatusView.xib */; };
|
||||||
|
@ -111,6 +112,7 @@
|
||||||
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
||||||
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||||
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = "<group>"; };
|
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -212,6 +214,7 @@
|
||||||
D0C7D45224F76169001EBDBB /* Assets.xcassets */,
|
D0C7D45224F76169001EBDBB /* Assets.xcassets */,
|
||||||
D0AD03552505814D0085A466 /* Base16 */,
|
D0AD03552505814D0085A466 /* Base16 */,
|
||||||
D0D7C013250440610039AD6F /* CodableBloomFilter */,
|
D0D7C013250440610039AD6F /* CodableBloomFilter */,
|
||||||
|
D0A1F4F5252E7D2A004435BF /* Data Sources */,
|
||||||
D085C3BB25008DEC008A6C5E /* DB */,
|
D085C3BB25008DEC008A6C5E /* DB */,
|
||||||
D0C7D46824F76169001EBDBB /* Extensions */,
|
D0C7D46824F76169001EBDBB /* Extensions */,
|
||||||
D0666A7924C7745A00F3F04B /* Frameworks */,
|
D0666A7924C7745A00F3F04B /* Frameworks */,
|
||||||
|
@ -270,6 +273,14 @@
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||||
|
);
|
||||||
|
path = "Data Sources";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D0C7D41D24F76169001EBDBB /* Supporting Files */ = {
|
D0C7D41D24F76169001EBDBB /* Supporting Files */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -573,6 +584,7 @@
|
||||||
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */,
|
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */,
|
||||||
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
||||||
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
|
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
|
||||||
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */,
|
||||||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
||||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
||||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
||||||
|
|
|
@ -41,4 +41,8 @@ extension AccountListService: CollectionService {
|
||||||
.flatMap { contentDatabase.append(accounts: $0.result, toList: list) }
|
.flatMap { contentDatabase.append(accounts: $0.result, toList: list) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> {
|
||||||
|
contentDatabase.toggleShowMore(id: id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
public protocol CollectionService {
|
public protocol CollectionService {
|
||||||
var sections: AnyPublisher<[[CollectionItem]], Error> { get }
|
var sections: AnyPublisher<[[CollectionItem]], Error> { get }
|
||||||
|
@ -8,6 +9,7 @@ public protocol CollectionService {
|
||||||
var title: AnyPublisher<String, Never> { get }
|
var title: AnyPublisher<String, Never> { get }
|
||||||
var navigationService: NavigationService { get }
|
var navigationService: NavigationService { get }
|
||||||
func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error>
|
func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error>
|
||||||
|
func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CollectionService {
|
extension CollectionService {
|
||||||
|
|
|
@ -31,4 +31,16 @@ extension ContextService: CollectionService {
|
||||||
.flatMap { contentDatabase.insert(context: $0, parentId: id) })
|
.flatMap { contentDatabase.insert(context: $0, parentId: id) })
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> {
|
||||||
|
contentDatabase.toggleShowMore(id: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func showMore(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
|
||||||
|
contentDatabase.showMore(ids: ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func showLess(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
|
||||||
|
contentDatabase.showLess(ids: ids)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@ public struct StatusService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension StatusService {
|
public extension StatusService {
|
||||||
|
func toggleShowMore() -> AnyPublisher<Never, Error> {
|
||||||
|
contentDatabase.toggleShowMore(id: status.displayStatus.id)
|
||||||
|
}
|
||||||
|
|
||||||
func toggleFavorited() -> AnyPublisher<Never, Error> {
|
func toggleFavorited() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(status.displayStatus.favourited
|
mastodonAPIClient.request(status.displayStatus.favourited
|
||||||
? StatusEndpoint.unfavourite(id: status.displayStatus.id)
|
? StatusEndpoint.unfavourite(id: status.displayStatus.id)
|
||||||
|
|
|
@ -44,4 +44,8 @@ extension TimelineService: CollectionService {
|
||||||
.flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) }
|
.flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func toggleShowMore(id: Status.Id) -> AnyPublisher<Never, Error> {
|
||||||
|
contentDatabase.toggleShowMore(id: id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,30 +12,9 @@ class TableViewController: UITableViewController {
|
||||||
private let webfingerIndicatorView = WebfingerIndicatorView()
|
private let webfingerIndicatorView = WebfingerIndicatorView()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var cellHeightCaches = [CGFloat: [CollectionItemIdentifier: CGFloat]]()
|
private var cellHeightCaches = [CGFloat: [CollectionItemIdentifier: CGFloat]]()
|
||||||
private let dataSourceQueue =
|
|
||||||
DispatchQueue(label: "com.metabolist.metatext.collection.data-source-queue")
|
|
||||||
|
|
||||||
private lazy var dataSource: UITableViewDiffableDataSource<Int, CollectionItemIdentifier> = {
|
private lazy var dataSource: TableViewDataSource = {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, identifier in
|
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
|
||||||
guard let cellViewModel = self?.viewModel.viewModel(indexPath: indexPath) else { return nil }
|
|
||||||
|
|
||||||
let cell = tableView.dequeueReusableCell(
|
|
||||||
withIdentifier: String(describing: identifier.kind.cellClass),
|
|
||||||
for: indexPath)
|
|
||||||
|
|
||||||
switch (cell, cellViewModel) {
|
|
||||||
case (let statusListCell as StatusListCell, let statusViewModel as StatusViewModel):
|
|
||||||
statusListCell.viewModel = statusViewModel
|
|
||||||
case (let accountListCell as AccountListCell, let accountViewModel as AccountViewModel):
|
|
||||||
accountListCell.viewModel = accountViewModel
|
|
||||||
case (let loadMoreCell as LoadMoreCell, let loadMoreViewModel as LoadMoreViewModel):
|
|
||||||
loadMoreCell.viewModel = loadMoreViewModel
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
init(viewModel: CollectionViewModel, identification: Identification) {
|
init(viewModel: CollectionViewModel, identification: Identification) {
|
||||||
|
@ -53,10 +32,6 @@ class TableViewController: UITableViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
for kind in CollectionItemIdentifier.Kind.allCases {
|
|
||||||
tableView.register(kind.cellClass, forCellReuseIdentifier: String(describing: kind.cellClass))
|
|
||||||
}
|
|
||||||
|
|
||||||
tableView.dataSource = dataSource
|
tableView.dataSource = dataSource
|
||||||
tableView.prefetchDataSource = self
|
tableView.prefetchDataSource = self
|
||||||
tableView.cellLayoutMarginsFollowReadableWidth = true
|
tableView.cellLayoutMarginsFollowReadableWidth = true
|
||||||
|
@ -183,12 +158,16 @@ private extension TableViewController {
|
||||||
func setupViewModelBindings() {
|
func setupViewModelBindings() {
|
||||||
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
|
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.sections.sink { [weak self] in self?.update(items: $0) }.store(in: &cancellables)
|
viewModel.updates.sink { [weak self] in self?.update($0) }.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.events.receive(on: DispatchQueue.main)
|
viewModel.events.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] in self?.handle(event: $0) }
|
.sink { [weak self] in self?.handle(event: $0) }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.showMoreForAll.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] in self?.set(showMoreForAllState: $0) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.loading.receive(on: RunLoop.main).sink { [weak self] in
|
viewModel.loading.receive(on: RunLoop.main).sink { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
@ -204,29 +183,27 @@ private extension TableViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(items: [[CollectionItemIdentifier]]) {
|
func update(_ update: CollectionUpdate) {
|
||||||
var offsetFromNavigationBar: CGFloat?
|
var offsetFromNavigationBar: CGFloat?
|
||||||
|
|
||||||
if
|
if
|
||||||
let item = viewModel.maintainScrollPositionOfItem,
|
let item = update.maintainScrollPosition,
|
||||||
let indexPath = dataSource.indexPath(for: item),
|
let indexPath = dataSource.indexPath(for: item),
|
||||||
let navigationBar = navigationController?.navigationBar {
|
let navigationBar = navigationController?.navigationBar {
|
||||||
let navigationBarMaxY = tableView.convert(navigationBar.bounds, from: navigationBar).maxY
|
let navigationBarMaxY = tableView.convert(navigationBar.bounds, from: navigationBar).maxY
|
||||||
offsetFromNavigationBar = tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY
|
offsetFromNavigationBar = tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSourceQueue.async { [weak self] in
|
self.dataSource.apply(update.items.snapshot()) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
self.dataSource.apply(items.snapshot(), animatingDifferences: false) {
|
if
|
||||||
if
|
let item = update.maintainScrollPosition,
|
||||||
let item = self.viewModel.maintainScrollPositionOfItem,
|
let indexPath = self.dataSource.indexPath(for: item) {
|
||||||
let indexPath = self.dataSource.indexPath(for: item) {
|
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
|
||||||
|
|
||||||
if let offsetFromNavigationBar = offsetFromNavigationBar {
|
if let offsetFromNavigationBar = offsetFromNavigationBar {
|
||||||
self.tableView.contentOffset.y -= offsetFromNavigationBar
|
self.tableView.contentOffset.y -= offsetFromNavigationBar
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,6 +241,23 @@ private extension TableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func set(showMoreForAllState: ShowMoreForAllState) {
|
||||||
|
switch showMoreForAllState {
|
||||||
|
case .hidden:
|
||||||
|
navigationItem.rightBarButtonItem = nil
|
||||||
|
case .showMore:
|
||||||
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
|
title: NSLocalizedString("status.show-more", comment: ""),
|
||||||
|
image: UIImage(systemName: "eye.slash"),
|
||||||
|
primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleShowMoreForAll() })
|
||||||
|
case .showLess:
|
||||||
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
|
title: NSLocalizedString("status.show-less", comment: ""),
|
||||||
|
image: UIImage(systemName: "eye"),
|
||||||
|
primaryAction: UIAction { [weak self] _ in self?.viewModel.toggleShowMoreForAll() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func share(url: URL) {
|
func share(url: URL) {
|
||||||
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import ServiceLayer
|
||||||
final public class CollectionItemsViewModel: ObservableObject {
|
final public class CollectionItemsViewModel: ObservableObject {
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public private(set) var nextPageMaxId: String?
|
public private(set) var nextPageMaxId: String?
|
||||||
public private(set) var maintainScrollPositionOfItem: CollectionItemIdentifier?
|
|
||||||
|
|
||||||
private let items = CurrentValueSubject<[[CollectionItem]], Never>([])
|
private let items = CurrentValueSubject<[[CollectionItem]], Never>([])
|
||||||
private let collectionService: CollectionService
|
private let collectionService: CollectionService
|
||||||
|
@ -16,6 +15,8 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
private var viewModelCache = [CollectionItem: (viewModel: CollectionItemViewModel, events: AnyCancellable)]()
|
private var viewModelCache = [CollectionItem: (viewModel: CollectionItemViewModel, events: AnyCancellable)]()
|
||||||
private let eventsSubject = PassthroughSubject<CollectionItemEvent, Never>()
|
private let eventsSubject = PassthroughSubject<CollectionItemEvent, Never>()
|
||||||
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
||||||
|
private let showMoreForAllSubject: CurrentValueSubject<ShowMoreForAllState, Never>
|
||||||
|
private var maintainScrollPosition: CollectionItemIdentifier?
|
||||||
private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
|
private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
|
||||||
private var lastSelectedLoadMore: LoadMore?
|
private var lastSelectedLoadMore: LoadMore?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
@ -23,6 +24,9 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
public init(collectionService: CollectionService, identification: Identification) {
|
public init(collectionService: CollectionService, identification: Identification) {
|
||||||
self.collectionService = collectionService
|
self.collectionService = collectionService
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
|
showMoreForAllSubject = CurrentValueSubject(
|
||||||
|
collectionService is ContextService && !identification.identity.preferences.readingExpandSpoilers
|
||||||
|
? .showMore : .hidden)
|
||||||
|
|
||||||
collectionService.sections
|
collectionService.sections
|
||||||
.handleEvents(receiveOutput: { [weak self] in self?.process(items: $0) })
|
.handleEvents(receiveOutput: { [weak self] in self?.process(items: $0) })
|
||||||
|
@ -38,12 +42,20 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CollectionItemsViewModel: CollectionViewModel {
|
extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
public var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> {
|
public var updates: AnyPublisher<CollectionUpdate, Never> {
|
||||||
items.map { $0.map { $0.map(CollectionItemIdentifier.init(item:)) } }.eraseToAnyPublisher()
|
items.map { [weak self] in
|
||||||
|
CollectionUpdate(items: $0.map { $0.map(CollectionItemIdentifier.init(item:)) },
|
||||||
|
maintainScrollPosition: self?.maintainScrollPosition)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var title: AnyPublisher<String, Never> { collectionService.title }
|
public var title: AnyPublisher<String, Never> { collectionService.title }
|
||||||
|
|
||||||
|
public var showMoreForAll: AnyPublisher<ShowMoreForAllState, Never> {
|
||||||
|
showMoreForAllSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
public var alertItems: AnyPublisher<AlertItem, Never> { $alertItem.compactMap { $0 }.eraseToAnyPublisher() }
|
||||||
|
|
||||||
public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
|
public var loading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
|
||||||
|
@ -107,7 +119,9 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
if let cachedViewModel = cachedViewModel as? StatusViewModel {
|
if let cachedViewModel = cachedViewModel as? StatusViewModel {
|
||||||
viewModel = cachedViewModel
|
viewModel = cachedViewModel
|
||||||
} else {
|
} else {
|
||||||
viewModel = .init(statusService: collectionService.navigationService.statusService(status: status))
|
viewModel = .init(
|
||||||
|
statusService: collectionService.navigationService.statusService(status: status),
|
||||||
|
identification: identification)
|
||||||
cache(viewModel: viewModel, forItem: item)
|
cache(viewModel: viewModel, forItem: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +152,31 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func toggleShowMoreForAll() {
|
||||||
|
let statusIds = Set(items.value.reduce([], +).compactMap { item -> Status.Id? in
|
||||||
|
guard case let .status(status, _) = item else { return nil }
|
||||||
|
|
||||||
|
return status.id
|
||||||
|
})
|
||||||
|
|
||||||
|
switch showMoreForAllSubject.value {
|
||||||
|
case .hidden:
|
||||||
|
break
|
||||||
|
case .showMore:
|
||||||
|
(collectionService as? ContextService)?.showMore(ids: statusIds)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.collect()
|
||||||
|
.sink { [weak self] _ in self?.showMoreForAllSubject.send(.showLess) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
case .showLess:
|
||||||
|
(collectionService as? ContextService)?.showLess(ids: statusIds)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.collect()
|
||||||
|
.sink { [weak self] _ in self?.showMoreForAllSubject.send(.showMore) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CollectionItemsViewModel {
|
private extension CollectionItemsViewModel {
|
||||||
|
@ -148,7 +187,7 @@ private extension CollectionItemsViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func process(items: [[CollectionItem]]) {
|
func process(items: [[CollectionItem]]) {
|
||||||
maintainScrollPositionOfItem = identifierForScrollPositionMaintenance(newItems: items)
|
maintainScrollPosition = identifierForScrollPositionMaintenance(newItems: items)
|
||||||
self.items.send(items)
|
self.items.send(items)
|
||||||
|
|
||||||
let itemsSet = Set(items.reduce([], +))
|
let itemsSet = Set(items.reduce([], +))
|
||||||
|
@ -158,6 +197,7 @@ private extension CollectionItemsViewModel {
|
||||||
|
|
||||||
func identifierForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItemIdentifier? {
|
func identifierForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItemIdentifier? {
|
||||||
let flatNewItems = newItems.reduce([], +)
|
let flatNewItems = newItems.reduce([], +)
|
||||||
|
|
||||||
if collectionService is ContextService,
|
if collectionService is ContextService,
|
||||||
items.value.isEmpty || items.value.map(\.count) == [0, 1, 0],
|
items.value.isEmpty || items.value.map(\.count) == [0, 1, 0],
|
||||||
let contextParent = flatNewItems.first(where: {
|
let contextParent = flatNewItems.first(where: {
|
||||||
|
|
|
@ -4,16 +4,17 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol CollectionViewModel {
|
public protocol CollectionViewModel {
|
||||||
var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> { get }
|
var updates: AnyPublisher<CollectionUpdate, Never> { get }
|
||||||
var title: AnyPublisher<String, Never> { get }
|
var title: AnyPublisher<String, Never> { get }
|
||||||
|
var showMoreForAll: AnyPublisher<ShowMoreForAllState, Never> { get }
|
||||||
var alertItems: AnyPublisher<AlertItem, Never> { get }
|
var alertItems: AnyPublisher<AlertItem, Never> { get }
|
||||||
var loading: AnyPublisher<Bool, Never> { get }
|
var loading: AnyPublisher<Bool, Never> { get }
|
||||||
var events: AnyPublisher<CollectionItemEvent, Never> { get }
|
var events: AnyPublisher<CollectionItemEvent, Never> { get }
|
||||||
var nextPageMaxId: String? { get }
|
var nextPageMaxId: String? { get }
|
||||||
var maintainScrollPositionOfItem: CollectionItemIdentifier? { get }
|
|
||||||
func request(maxId: String?, minId: String?)
|
func request(maxId: String?, minId: String?)
|
||||||
func viewedAtTop(indexPath: IndexPath)
|
func viewedAtTop(indexPath: IndexPath)
|
||||||
func select(indexPath: IndexPath)
|
func select(indexPath: IndexPath)
|
||||||
func canSelect(indexPath: IndexPath) -> Bool
|
func canSelect(indexPath: IndexPath) -> Bool
|
||||||
func viewModel(indexPath: IndexPath) -> CollectionItemViewModel
|
func viewModel(indexPath: IndexPath) -> CollectionItemViewModel
|
||||||
|
func toggleShowMoreForAll()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import ServiceLayer
|
||||||
public struct CollectionItemIdentifier: Hashable {
|
public struct CollectionItemIdentifier: Hashable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let kind: Kind
|
public let kind: Kind
|
||||||
public let info: [InfoKey: AnyHashable]
|
public let pinned: Bool
|
||||||
|
public let showMoreToggled: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension CollectionItemIdentifier {
|
public extension CollectionItemIdentifier {
|
||||||
|
@ -15,10 +16,6 @@ public extension CollectionItemIdentifier {
|
||||||
case loadMore
|
case loadMore
|
||||||
case account
|
case account
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InfoKey {
|
|
||||||
case pinned
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CollectionItemIdentifier {
|
extension CollectionItemIdentifier {
|
||||||
|
@ -27,15 +24,22 @@ extension CollectionItemIdentifier {
|
||||||
case let .status(status, configuration):
|
case let .status(status, configuration):
|
||||||
id = status.id
|
id = status.id
|
||||||
kind = .status
|
kind = .status
|
||||||
info = configuration.isPinned ? [.pinned: true] : [:]
|
pinned = configuration.isPinned
|
||||||
|
showMoreToggled = configuration.showMoreToggled
|
||||||
case let .loadMore(loadMore):
|
case let .loadMore(loadMore):
|
||||||
id = loadMore.afterStatusId
|
id = loadMore.afterStatusId
|
||||||
kind = .loadMore
|
kind = .loadMore
|
||||||
info = [:]
|
pinned = false
|
||||||
|
showMoreToggled = false
|
||||||
case let .account(account):
|
case let .account(account):
|
||||||
id = account.id
|
id = account.id
|
||||||
kind = .account
|
kind = .account
|
||||||
info = [:]
|
pinned = false
|
||||||
|
showMoreToggled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func isSameExceptShowMoreToggled(lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.id == rhs.id && lhs.kind == rhs.kind && lhs.pinned == rhs.pinned
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
public struct CollectionUpdate: Hashable {
|
||||||
|
public let items: [[CollectionItemIdentifier]]
|
||||||
|
public let maintainScrollPosition: CollectionItemIdentifier?
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
public enum ShowMoreForAllState {
|
||||||
|
case hidden
|
||||||
|
case showMore
|
||||||
|
case showLess
|
||||||
|
}
|
|
@ -41,14 +41,18 @@ final public class ProfileViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewModel: CollectionViewModel {
|
extension ProfileViewModel: CollectionViewModel {
|
||||||
public var sections: AnyPublisher<[[CollectionItemIdentifier]], Never> {
|
public var updates: AnyPublisher<CollectionUpdate, Never> {
|
||||||
collectionViewModel.flatMap(\.sections).eraseToAnyPublisher()
|
collectionViewModel.flatMap(\.updates).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var title: AnyPublisher<String, Never> {
|
public var title: AnyPublisher<String, Never> {
|
||||||
$accountViewModel.compactMap { $0?.accountName }.eraseToAnyPublisher()
|
$accountViewModel.compactMap { $0?.accountName }.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var showMoreForAll: AnyPublisher<ShowMoreForAllState, Never> {
|
||||||
|
collectionViewModel.flatMap(\.showMoreForAll).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
public var alertItems: AnyPublisher<AlertItem, Never> {
|
public var alertItems: AnyPublisher<AlertItem, Never> {
|
||||||
collectionViewModel.flatMap(\.alertItems).eraseToAnyPublisher()
|
collectionViewModel.flatMap(\.alertItems).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -70,10 +74,6 @@ extension ProfileViewModel: CollectionViewModel {
|
||||||
collectionViewModel.value.nextPageMaxId
|
collectionViewModel.value.nextPageMaxId
|
||||||
}
|
}
|
||||||
|
|
||||||
public var maintainScrollPositionOfItem: CollectionItemIdentifier? {
|
|
||||||
collectionViewModel.value.maintainScrollPositionOfItem
|
|
||||||
}
|
|
||||||
|
|
||||||
public func request(maxId: String?, minId: String?) {
|
public func request(maxId: String?, minId: String?) {
|
||||||
if case .statuses = collection, maxId == nil {
|
if case .statuses = collection, maxId == nil {
|
||||||
profileService.fetchPinnedStatuses()
|
profileService.fetchPinnedStatuses()
|
||||||
|
@ -100,4 +100,8 @@ extension ProfileViewModel: CollectionViewModel {
|
||||||
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
|
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
|
||||||
collectionViewModel.value.viewModel(indexPath: indexPath)
|
collectionViewModel.value.viewModel(indexPath: indexPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func toggleShowMoreForAll() {
|
||||||
|
collectionViewModel.value.toggleShowMoreForAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,15 @@ public struct StatusViewModel: CollectionItemViewModel {
|
||||||
public let pollOptionTitles: [String]
|
public let pollOptionTitles: [String]
|
||||||
public let pollEmoji: [Emoji]
|
public let pollEmoji: [Emoji]
|
||||||
public var configuration = CollectionItem.StatusConfiguration.default
|
public var configuration = CollectionItem.StatusConfiguration.default
|
||||||
public var sensitiveContentToggled = false
|
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
private let statusService: StatusService
|
private let statusService: StatusService
|
||||||
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
private let identification: Identification
|
||||||
|
|
||||||
init(statusService: StatusService) {
|
init(statusService: StatusService, identification: Identification) {
|
||||||
self.statusService = statusService
|
self.statusService = statusService
|
||||||
|
self.identification = identification
|
||||||
content = statusService.status.displayStatus.content.attributed
|
content = statusService.status.displayStatus.content.attributed
|
||||||
contentEmoji = statusService.status.displayStatus.emojis
|
contentEmoji = statusService.status.displayStatus.emojis
|
||||||
displayName = statusService.status.displayStatus.account.displayName == ""
|
displayName = statusService.status.displayStatus.account.displayName == ""
|
||||||
|
@ -47,11 +48,13 @@ public struct StatusViewModel: CollectionItemViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension StatusViewModel {
|
public extension StatusViewModel {
|
||||||
var shouldDisplaySensitiveContent: Bool {
|
var shouldShowMore: Bool {
|
||||||
if statusService.status.displayStatus.sensitive {
|
guard statusService.status.spoilerText != "" else { return true }
|
||||||
return sensitiveContentToggled
|
|
||||||
|
if identification.identity.preferences.readingExpandSpoilers {
|
||||||
|
return !configuration.showMoreToggled
|
||||||
} else {
|
} else {
|
||||||
return true
|
return configuration.showMoreToggled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +107,13 @@ public extension StatusViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleShowMore() {
|
||||||
|
eventsSubject.send(
|
||||||
|
statusService.toggleShowMore()
|
||||||
|
.map { _ in CollectionItemEvent.ignorableOutput }
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
|
||||||
func urlSelected(_ url: URL) {
|
func urlSelected(_ url: URL) {
|
||||||
eventsSubject.send(
|
eventsSubject.send(
|
||||||
statusService.navigationService.item(url: url)
|
statusService.navigationService.item(url: url)
|
||||||
|
|
|
@ -14,14 +14,14 @@ class StatusView: UIView {
|
||||||
@IBOutlet weak var accountLabel: UILabel!
|
@IBOutlet weak var accountLabel: UILabel!
|
||||||
@IBOutlet weak var timeLabel: UILabel!
|
@IBOutlet weak var timeLabel: UILabel!
|
||||||
@IBOutlet weak var spoilerTextLabel: UILabel!
|
@IBOutlet weak var spoilerTextLabel: UILabel!
|
||||||
@IBOutlet weak var toggleSensitiveContentButton: UIButton!
|
@IBOutlet weak var toggleShowMoreButton: UIButton!
|
||||||
@IBOutlet weak var replyButton: UIButton!
|
@IBOutlet weak var replyButton: UIButton!
|
||||||
@IBOutlet weak var reblogButton: UIButton!
|
@IBOutlet weak var reblogButton: UIButton!
|
||||||
@IBOutlet weak var favoriteButton: UIButton!
|
@IBOutlet weak var favoriteButton: UIButton!
|
||||||
@IBOutlet weak var shareButton: UIButton!
|
@IBOutlet weak var shareButton: UIButton!
|
||||||
@IBOutlet weak var attachmentsView: AttachmentsView!
|
@IBOutlet weak var attachmentsView: AttachmentsView!
|
||||||
@IBOutlet weak var cardView: CardView!
|
@IBOutlet weak var cardView: CardView!
|
||||||
@IBOutlet weak var sensitiveContentView: UIStackView!
|
@IBOutlet weak var showMoreView: UIStackView!
|
||||||
@IBOutlet weak var hasReplyFollowingView: UIView!
|
@IBOutlet weak var hasReplyFollowingView: UIView!
|
||||||
@IBOutlet weak var inReplyToView: UIView!
|
@IBOutlet weak var inReplyToView: UIView!
|
||||||
@IBOutlet weak var avatarReplyContextView: UIView!
|
@IBOutlet weak var avatarReplyContextView: UIView!
|
||||||
|
@ -65,7 +65,7 @@ class StatusView: UIView {
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
for button: UIButton in [toggleSensitiveContentButton] where button.frame.height != 0 {
|
for button: UIButton in [toggleShowMoreButton] where button.frame.height != 0 {
|
||||||
button.layer.cornerRadius = button.frame.height / 2
|
button.layer.cornerRadius = button.frame.height / 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +141,10 @@ private extension StatusView {
|
||||||
avatarButton.addAction(accountAction, for: .touchUpInside)
|
avatarButton.addAction(accountAction, for: .touchUpInside)
|
||||||
contextParentAvatarButton.addAction(accountAction, for: .touchUpInside)
|
contextParentAvatarButton.addAction(accountAction, for: .touchUpInside)
|
||||||
|
|
||||||
|
toggleShowMoreButton.addAction(
|
||||||
|
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleShowMore() },
|
||||||
|
for: .touchUpInside)
|
||||||
|
|
||||||
cardView.button.addAction(
|
cardView.button.addAction(
|
||||||
UIAction { [weak self] _ in
|
UIAction { [weak self] _ in
|
||||||
guard
|
guard
|
||||||
|
@ -229,8 +233,8 @@ private extension StatusView {
|
||||||
mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight)
|
mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight)
|
||||||
spoilerTextLabel.attributedText = mutableSpoilerText
|
spoilerTextLabel.attributedText = mutableSpoilerText
|
||||||
spoilerTextLabel.isHidden = !viewModel.sensitive || spoilerTextLabel.text == ""
|
spoilerTextLabel.isHidden = !viewModel.sensitive || spoilerTextLabel.text == ""
|
||||||
toggleSensitiveContentButton.setTitle(
|
toggleShowMoreButton.setTitle(
|
||||||
viewModel.shouldDisplaySensitiveContent
|
viewModel.shouldShowMore
|
||||||
? NSLocalizedString("status.show-less", comment: "")
|
? NSLocalizedString("status.show-less", comment: "")
|
||||||
: NSLocalizedString("status.show-more", comment: ""),
|
: NSLocalizedString("status.show-more", comment: ""),
|
||||||
for: .normal)
|
for: .normal)
|
||||||
|
@ -242,7 +246,7 @@ private extension StatusView {
|
||||||
applicationButton.setTitle(viewModel.applicationName, for: .normal)
|
applicationButton.setTitle(viewModel.applicationName, for: .normal)
|
||||||
applicationButton.isEnabled = viewModel.applicationURL != nil
|
applicationButton.isEnabled = viewModel.applicationURL != nil
|
||||||
avatarImageView.kf.setImage(with: viewModel.avatarURL)
|
avatarImageView.kf.setImage(with: viewModel.avatarURL)
|
||||||
toggleSensitiveContentButton.isHidden = !viewModel.sensitive
|
toggleShowMoreButton.isHidden = !viewModel.sensitive
|
||||||
replyButton.setTitle(viewModel.repliesCount == 0 ? "" : String(viewModel.repliesCount), for: .normal)
|
replyButton.setTitle(viewModel.repliesCount == 0 ? "" : String(viewModel.repliesCount), for: .normal)
|
||||||
reblogButton.setTitle(viewModel.reblogsCount == 0 ? "" : String(viewModel.reblogsCount), for: .normal)
|
reblogButton.setTitle(viewModel.reblogsCount == 0 ? "" : String(viewModel.reblogsCount), for: .normal)
|
||||||
setReblogButtonColor(reblogged: viewModel.reblogged)
|
setReblogButtonColor(reblogged: viewModel.reblogged)
|
||||||
|
@ -303,7 +307,7 @@ private extension StatusView {
|
||||||
cardView.viewModel = viewModel.cardViewModel
|
cardView.viewModel = viewModel.cardViewModel
|
||||||
cardView.isHidden = viewModel.cardViewModel == nil
|
cardView.isHidden = viewModel.cardViewModel == nil
|
||||||
|
|
||||||
sensitiveContentView.isHidden = !viewModel.shouldDisplaySensitiveContent
|
showMoreView.isHidden = !viewModel.shouldShowMore
|
||||||
|
|
||||||
inReplyToView.isHidden = !viewModel.configuration.isReplyInContext
|
inReplyToView.isHidden = !viewModel.configuration.isReplyInContext
|
||||||
|
|
||||||
|
|
|
@ -45,12 +45,12 @@
|
||||||
<outlet property="nameDateView" destination="svp-hj-Xn6" id="SzA-ME-jU0"/>
|
<outlet property="nameDateView" destination="svp-hj-Xn6" id="SzA-ME-jU0"/>
|
||||||
<outlet property="reblogButton" destination="ZKl-Hp-Y42" id="bxe-wr-kB7"/>
|
<outlet property="reblogButton" destination="ZKl-Hp-Y42" id="bxe-wr-kB7"/>
|
||||||
<outlet property="replyButton" destination="6HD-MP-H72" id="5fb-z4-qlm"/>
|
<outlet property="replyButton" destination="6HD-MP-H72" id="5fb-z4-qlm"/>
|
||||||
<outlet property="sensitiveContentView" destination="BXI-3E-NWh" id="sXO-EM-fTI"/>
|
|
||||||
<outlet property="shareButton" destination="zAD-2Z-vhu" id="ZzV-wg-rOZ"/>
|
<outlet property="shareButton" destination="zAD-2Z-vhu" id="ZzV-wg-rOZ"/>
|
||||||
|
<outlet property="showMoreView" destination="BXI-3E-NWh" id="Zsm-ho-TbZ"/>
|
||||||
<outlet property="spoilerTextLabel" destination="5Gq-2q-Cvx" id="owo-VU-cIm"/>
|
<outlet property="spoilerTextLabel" destination="5Gq-2q-Cvx" id="owo-VU-cIm"/>
|
||||||
<outlet property="timeApplicationDividerView" destination="hYj-vy-Net" id="F8o-xH-FFP"/>
|
<outlet property="timeApplicationDividerView" destination="hYj-vy-Net" id="F8o-xH-FFP"/>
|
||||||
<outlet property="timeLabel" destination="FEN-6u-xs5" id="epT-vQ-T9R"/>
|
<outlet property="timeLabel" destination="FEN-6u-xs5" id="epT-vQ-T9R"/>
|
||||||
<outlet property="toggleSensitiveContentButton" destination="XqE-Oj-oxH" id="EQB-cR-eOL"/>
|
<outlet property="toggleShowMoreButton" destination="XqE-Oj-oxH" id="D1n-WP-y9y"/>
|
||||||
<outletCollection property="separatorConstraints" destination="VEz-6j-37B" collectionClass="NSMutableArray" id="liC-8J-DV0"/>
|
<outletCollection property="separatorConstraints" destination="VEz-6j-37B" collectionClass="NSMutableArray" id="liC-8J-DV0"/>
|
||||||
<outletCollection property="separatorConstraints" destination="H9G-jZ-cek" collectionClass="NSMutableArray" id="E4b-tA-2gG"/>
|
<outletCollection property="separatorConstraints" destination="H9G-jZ-cek" collectionClass="NSMutableArray" id="E4b-tA-2gG"/>
|
||||||
</connections>
|
</connections>
|
||||||
|
|
Loading…
Reference in a new issue