mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-21 15:50:59 +00:00
Announcements
This commit is contained in:
parent
e6f95ccd18
commit
3f7a6a26aa
27 changed files with 787 additions and 14 deletions
|
@ -605,6 +605,25 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func announcementCountPublisher() -> AnyPublisher<(total: Int, unread: Int), Error> {
|
||||||
|
ValueObservation.tracking(Announcement.fetchCount)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
|
.combineLatest(ValueObservation.tracking(Announcement.fetchCount)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter))
|
||||||
|
.map { (total: $0, unread: $1) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func announcementsPublisher() -> AnyPublisher<[CollectionSection], Error> {
|
||||||
|
ValueObservation.tracking(Announcement.order(Announcement.Columns.publishedAt).fetchAll)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
|
.map { [CollectionSection(items: $0.map(CollectionItem.announcement))] }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
Emoji.filter(Emoji.Columns.visibleInPicker == true)
|
Emoji.filter(Emoji.Columns.visibleInPicker == true)
|
||||||
|
|
|
@ -9,6 +9,7 @@ public enum CollectionItem: Hashable {
|
||||||
case notification(MastodonNotification, StatusConfiguration?)
|
case notification(MastodonNotification, StatusConfiguration?)
|
||||||
case conversation(Conversation)
|
case conversation(Conversation)
|
||||||
case tag(Tag)
|
case tag(Tag)
|
||||||
|
case announcement(Announcement)
|
||||||
case moreResults(MoreResults)
|
case moreResults(MoreResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +64,8 @@ public extension CollectionItem {
|
||||||
return conversation.id
|
return conversation.id
|
||||||
case let .tag(tag):
|
case let .tag(tag):
|
||||||
return tag.name
|
return tag.name
|
||||||
|
case let .announcement(announcement):
|
||||||
|
return announcement.id
|
||||||
case .moreResults:
|
case .moreResults:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection
|
||||||
conversationCell.viewModel = conversationViewModel
|
conversationCell.viewModel = conversationViewModel
|
||||||
case let (tagCell as TagTableViewCell, tagViewModel as TagViewModel):
|
case let (tagCell as TagTableViewCell, tagViewModel as TagViewModel):
|
||||||
tagCell.viewModel = tagViewModel
|
tagCell.viewModel = tagViewModel
|
||||||
|
case let (announcementCell as AnnouncementTableViewCell, announcementViewModel as AnnouncementViewModel):
|
||||||
|
announcementCell.viewModel = announcementViewModel
|
||||||
case let (_, moreResultsViewModel as MoreResultsViewModel):
|
case let (_, moreResultsViewModel as MoreResultsViewModel):
|
||||||
var configuration = cell.defaultContentConfiguration()
|
var configuration = cell.defaultContentConfiguration()
|
||||||
let statusWord = viewModel.identityContext.appPreferences.statusWord
|
let statusWord = viewModel.identityContext.appPreferences.statusWord
|
||||||
|
|
|
@ -12,6 +12,7 @@ extension CollectionItem {
|
||||||
NotificationTableViewCell.self,
|
NotificationTableViewCell.self,
|
||||||
ConversationTableViewCell.self,
|
ConversationTableViewCell.self,
|
||||||
TagTableViewCell.self,
|
TagTableViewCell.self,
|
||||||
|
AnnouncementTableViewCell.self,
|
||||||
SeparatorConfiguredTableViewCell.self]
|
SeparatorConfiguredTableViewCell.self]
|
||||||
|
|
||||||
var cellClass: AnyClass {
|
var cellClass: AnyClass {
|
||||||
|
@ -28,6 +29,8 @@ extension CollectionItem {
|
||||||
return ConversationTableViewCell.self
|
return ConversationTableViewCell.self
|
||||||
case .tag:
|
case .tag:
|
||||||
return TagTableViewCell.self
|
return TagTableViewCell.self
|
||||||
|
case .announcement:
|
||||||
|
return AnnouncementTableViewCell.self
|
||||||
case .moreResults:
|
case .moreResults:
|
||||||
return SeparatorConfiguredTableViewCell.self
|
return SeparatorConfiguredTableViewCell.self
|
||||||
}
|
}
|
||||||
|
@ -58,6 +61,8 @@ extension CollectionItem {
|
||||||
conversation: conversation)
|
conversation: conversation)
|
||||||
case let .tag(tag):
|
case let .tag(tag):
|
||||||
return TagView.estimatedHeight(width: width, tag: tag)
|
return TagView.estimatedHeight(width: width, tag: tag)
|
||||||
|
case let .announcement(announcement):
|
||||||
|
return AnnouncementView.estimatedHeight(width: width, announcement: announcement)
|
||||||
case .moreResults:
|
case .moreResults:
|
||||||
return UITableView.automaticDimension
|
return UITableView.automaticDimension
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"account.unnotify" = "Turn off notifications";
|
"account.unnotify" = "Turn off notifications";
|
||||||
"activity.open-in-default-browser" = "Open in default browser";
|
"activity.open-in-default-browser" = "Open in default browser";
|
||||||
"add" = "Add";
|
"add" = "Add";
|
||||||
|
"announcement.insert-emoji" = "Insert emoji";
|
||||||
"api-error.unable-to-fetch-remote-status" = "Unable to fetch remote status";
|
"api-error.unable-to-fetch-remote-status" = "Unable to fetch remote status";
|
||||||
"apns-default-message" = "New notification";
|
"apns-default-message" = "New notification";
|
||||||
"app-icon.brutalist" = "Brutalist";
|
"app-icon.brutalist" = "Brutalist";
|
||||||
|
@ -176,6 +177,7 @@
|
||||||
"load-more.above.accessibility.toot" = "Load from toot above";
|
"load-more.above.accessibility.toot" = "Load from toot above";
|
||||||
"load-more.below.accessibility.post" = "Load from post below";
|
"load-more.below.accessibility.post" = "Load from post below";
|
||||||
"load-more.below.accessibility.toot" = "Load from toot below";
|
"load-more.below.accessibility.toot" = "Load from toot below";
|
||||||
|
"main-navigation.announcements" = "Announcements";
|
||||||
"main-navigation.timelines" = "Timelines";
|
"main-navigation.timelines" = "Timelines";
|
||||||
"main-navigation.explore" = "Explore";
|
"main-navigation.explore" = "Explore";
|
||||||
"main-navigation.notifications" = "Notifications";
|
"main-navigation.notifications" = "Notifications";
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Announcement: Codable, Hashable {
|
public struct Announcement: Codable, Hashable {
|
||||||
public let id: String
|
public let id: Id
|
||||||
public let content: HTML
|
public let content: HTML
|
||||||
public let startsAt: Date?
|
public let startsAt: Date?
|
||||||
public let endsAt: Date?
|
public let endsAt: Date?
|
||||||
|
@ -16,3 +16,7 @@ public struct Announcement: Codable, Hashable {
|
||||||
public let emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
public let reactions: [AnnouncementReaction]
|
public let reactions: [AnnouncementReaction]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Announcement {
|
||||||
|
typealias Id = String
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,9 @@ public enum EmptyEndpoint {
|
||||||
case deleteFilter(id: Filter.Id)
|
case deleteFilter(id: Filter.Id)
|
||||||
case blockDomain(String)
|
case blockDomain(String)
|
||||||
case unblockDomain(String)
|
case unblockDomain(String)
|
||||||
|
case dismissAnnouncement(id: Announcement.Id)
|
||||||
|
case addAnnouncementReaction(id: Announcement.Id, name: String)
|
||||||
|
case removeAnnouncementReaction(id: Announcement.Id, name: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EmptyEndpoint: Endpoint {
|
extension EmptyEndpoint: Endpoint {
|
||||||
|
@ -27,6 +30,8 @@ extension EmptyEndpoint: Endpoint {
|
||||||
return defaultContext + ["filters"]
|
return defaultContext + ["filters"]
|
||||||
case .blockDomain, .unblockDomain:
|
case .blockDomain, .unblockDomain:
|
||||||
return defaultContext + ["domain_blocks"]
|
return defaultContext + ["domain_blocks"]
|
||||||
|
case .dismissAnnouncement, .addAnnouncementReaction, .removeAnnouncementReaction:
|
||||||
|
return defaultContext + ["announcements"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,14 +45,20 @@ extension EmptyEndpoint: Endpoint {
|
||||||
return [id]
|
return [id]
|
||||||
case .blockDomain, .unblockDomain:
|
case .blockDomain, .unblockDomain:
|
||||||
return []
|
return []
|
||||||
|
case let .dismissAnnouncement(id):
|
||||||
|
return [id, "dismiss"]
|
||||||
|
case let .addAnnouncementReaction(id, name), let .removeAnnouncementReaction(id, name):
|
||||||
|
return [id, "reactions", name]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var method: HTTPMethod {
|
public var method: HTTPMethod {
|
||||||
switch self {
|
switch self {
|
||||||
case .addAccountsToList, .oauthRevoke, .blockDomain:
|
case .addAccountsToList, .oauthRevoke, .blockDomain, .dismissAnnouncement:
|
||||||
return .post
|
return .post
|
||||||
case .removeAccountsFromList, .deleteList, .deleteFilter, .unblockDomain:
|
case .addAnnouncementReaction:
|
||||||
|
return .put
|
||||||
|
case .removeAccountsFromList, .deleteList, .deleteFilter, .unblockDomain, .removeAnnouncementReaction:
|
||||||
return .delete
|
return .delete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +71,7 @@ extension EmptyEndpoint: Endpoint {
|
||||||
return ["account_ids": Array(accountIds)]
|
return ["account_ids": Array(accountIds)]
|
||||||
case let .blockDomain(domain), let .unblockDomain(domain):
|
case let .blockDomain(domain), let .unblockDomain(domain):
|
||||||
return ["domain": domain]
|
return ["domain": domain]
|
||||||
case .deleteList, .deleteFilter:
|
case .deleteList, .deleteFilter, .dismissAnnouncement, .addAnnouncementReaction, .removeAnnouncementReaction:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,10 @@
|
||||||
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; };
|
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; };
|
||||||
D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; };
|
D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; };
|
||||||
D059376125ABE2E800754FDF /* XMLUnescaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059376025ABE2E800754FDF /* XMLUnescaper.swift */; };
|
D059376125ABE2E800754FDF /* XMLUnescaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059376025ABE2E800754FDF /* XMLUnescaper.swift */; };
|
||||||
|
D05A0A5D26363DC700F9BCE1 /* AnnouncementReactionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A0A5C26363DC700F9BCE1 /* AnnouncementReactionCollectionViewCell.swift */; };
|
||||||
|
D05A0A6726363FEA00F9BCE1 /* AnnouncementReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A0A6626363FEA00F9BCE1 /* AnnouncementReactionView.swift */; };
|
||||||
|
D05A0A6D2636400100F9BCE1 /* AnnouncementReactionContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A0A6C2636400100F9BCE1 /* AnnouncementReactionContentConfiguration.swift */; };
|
||||||
|
D05A0A77263644B600F9BCE1 /* AnnouncementReactionsCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A0A76263644B600F9BCE1 /* AnnouncementReactionsCollectionView.swift */; };
|
||||||
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */; };
|
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */; };
|
||||||
D0625E59250F092900502611 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusTableViewCell.swift */; };
|
D0625E59250F092900502611 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusTableViewCell.swift */; };
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||||
|
@ -195,6 +199,9 @@
|
||||||
D0D4306525F0B93700BE5504 /* AppIconBrutalist@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0D4306325F0B93700BE5504 /* AppIconBrutalist@3x.png */; };
|
D0D4306525F0B93700BE5504 /* AppIconBrutalist@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0D4306325F0B93700BE5504 /* AppIconBrutalist@3x.png */; };
|
||||||
D0D4307025F0BBA900BE5504 /* AppIconRainbowBrutalist@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0D4306E25F0BBA900BE5504 /* AppIconRainbowBrutalist@2x.png */; };
|
D0D4307025F0BBA900BE5504 /* AppIconRainbowBrutalist@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0D4306E25F0BBA900BE5504 /* AppIconRainbowBrutalist@2x.png */; };
|
||||||
D0D4307125F0BBA900BE5504 /* AppIconRainbowBrutalist@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0D4306F25F0BBA900BE5504 /* AppIconRainbowBrutalist@3x.png */; };
|
D0D4307125F0BBA900BE5504 /* AppIconRainbowBrutalist@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D0D4306F25F0BBA900BE5504 /* AppIconRainbowBrutalist@3x.png */; };
|
||||||
|
D0D8B0312622398F00A874A4 /* AnnouncementTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D8B0302622398F00A874A4 /* AnnouncementTableViewCell.swift */; };
|
||||||
|
D0D8B03B262239B100A874A4 /* AnnouncementContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D8B03A262239B100A874A4 /* AnnouncementContentConfiguration.swift */; };
|
||||||
|
D0D8B04126223A1E00A874A4 /* AnnouncementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D8B04026223A1E00A874A4 /* AnnouncementView.swift */; };
|
||||||
D0D93EBA25D9C70400C622ED /* AutocompleteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */; };
|
D0D93EBA25D9C70400C622ED /* AutocompleteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */; };
|
||||||
D0D93EC025D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */; };
|
D0D93EC025D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */; };
|
||||||
D0D93EC525D9C75E00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */; };
|
D0D93EC525D9C75E00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */; };
|
||||||
|
@ -332,6 +339,10 @@
|
||||||
D059373225AAEA7000754FDF /* CompositionPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollView.swift; sourceTree = "<group>"; };
|
D059373225AAEA7000754FDF /* CompositionPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollView.swift; sourceTree = "<group>"; };
|
||||||
D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollOptionView.swift; sourceTree = "<group>"; };
|
D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollOptionView.swift; sourceTree = "<group>"; };
|
||||||
D059376025ABE2E800754FDF /* XMLUnescaper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLUnescaper.swift; sourceTree = "<group>"; };
|
D059376025ABE2E800754FDF /* XMLUnescaper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLUnescaper.swift; sourceTree = "<group>"; };
|
||||||
|
D05A0A5C26363DC700F9BCE1 /* AnnouncementReactionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementReactionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D05A0A6626363FEA00F9BCE1 /* AnnouncementReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementReactionView.swift; sourceTree = "<group>"; };
|
||||||
|
D05A0A6C2636400100F9BCE1 /* AnnouncementReactionContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementReactionContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D05A0A76263644B600F9BCE1 /* AnnouncementReactionsCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementReactionsCollectionView.swift; sourceTree = "<group>"; };
|
||||||
D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVURLAsset+Extensions.swift"; sourceTree = "<group>"; };
|
D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVURLAsset+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0625E58250F092900502611 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
D0625E58250F092900502611 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
@ -438,6 +449,9 @@
|
||||||
D0D4306325F0B93700BE5504 /* AppIconBrutalist@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIconBrutalist@3x.png"; sourceTree = "<group>"; };
|
D0D4306325F0B93700BE5504 /* AppIconBrutalist@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIconBrutalist@3x.png"; sourceTree = "<group>"; };
|
||||||
D0D4306E25F0BBA900BE5504 /* AppIconRainbowBrutalist@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIconRainbowBrutalist@2x.png"; sourceTree = "<group>"; };
|
D0D4306E25F0BBA900BE5504 /* AppIconRainbowBrutalist@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIconRainbowBrutalist@2x.png"; sourceTree = "<group>"; };
|
||||||
D0D4306F25F0BBA900BE5504 /* AppIconRainbowBrutalist@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIconRainbowBrutalist@3x.png"; sourceTree = "<group>"; };
|
D0D4306F25F0BBA900BE5504 /* AppIconRainbowBrutalist@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIconRainbowBrutalist@3x.png"; sourceTree = "<group>"; };
|
||||||
|
D0D8B0302622398F00A874A4 /* AnnouncementTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D0D8B03A262239B100A874A4 /* AnnouncementContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D0D8B04026223A1E00A874A4 /* AnnouncementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementView.swift; sourceTree = "<group>"; };
|
||||||
D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemView.swift; sourceTree = "<group>"; };
|
D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemView.swift; sourceTree = "<group>"; };
|
||||||
D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemContentConfiguration.swift; sourceTree = "<group>"; };
|
D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemCollectionViewCell.swift; sourceTree = "<group>"; };
|
D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -550,6 +564,7 @@
|
||||||
D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */,
|
D0CEC11F25E35FE100FEF5A6 /* EmojiInsertable.swift */,
|
||||||
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */,
|
D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */,
|
||||||
D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */,
|
D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */,
|
||||||
|
D05A0A76263644B600F9BCE1 /* AnnouncementReactionsCollectionView.swift */,
|
||||||
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
||||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||||
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
||||||
|
@ -612,6 +627,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0F0B125251A90F400942152 /* AccountTableViewCell.swift */,
|
D0F0B125251A90F400942152 /* AccountTableViewCell.swift */,
|
||||||
|
D0D8B0302622398F00A874A4 /* AnnouncementTableViewCell.swift */,
|
||||||
D00702282555E51200F38136 /* ConversationTableViewCell.swift */,
|
D00702282555E51200F38136 /* ConversationTableViewCell.swift */,
|
||||||
D021A60925C36B32008A0C0D /* IdentityTableViewCell.swift */,
|
D021A60925C36B32008A0C0D /* IdentityTableViewCell.swift */,
|
||||||
D0B8510B25259E56004E0744 /* LoadMoreTableViewCell.swift */,
|
D0B8510B25259E56004E0744 /* LoadMoreTableViewCell.swift */,
|
||||||
|
@ -626,6 +642,7 @@
|
||||||
D021A66F25C3E1F9008A0C0D /* Collection View Cells */ = {
|
D021A66F25C3E1F9008A0C0D /* Collection View Cells */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D05A0A5C26363DC700F9BCE1 /* AnnouncementReactionCollectionViewCell.swift */,
|
||||||
D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */,
|
D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */,
|
||||||
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
||||||
D09D971725C64682007E6394 /* InstanceCollectionViewCell.swift */,
|
D09D971725C64682007E6394 /* InstanceCollectionViewCell.swift */,
|
||||||
|
@ -639,6 +656,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */,
|
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */,
|
||||||
|
D0D8B03A262239B100A874A4 /* AnnouncementContentConfiguration.swift */,
|
||||||
|
D05A0A6C2636400100F9BCE1 /* AnnouncementReactionContentConfiguration.swift */,
|
||||||
D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */,
|
D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */,
|
||||||
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
||||||
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */,
|
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */,
|
||||||
|
@ -657,6 +676,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||||
|
D05A0A6626363FEA00F9BCE1 /* AnnouncementReactionView.swift */,
|
||||||
|
D0D8B04026223A1E00A874A4 /* AnnouncementView.swift */,
|
||||||
D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */,
|
D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */,
|
||||||
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||||
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
||||||
|
@ -1155,6 +1176,7 @@
|
||||||
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
||||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
||||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||||
|
D0D8B03B262239B100A874A4 /* AnnouncementContentConfiguration.swift in Sources */,
|
||||||
D0BE633725F2D95E001139FA /* AVAudioSession+Extensions.swift in Sources */,
|
D0BE633725F2D95E001139FA /* AVAudioSession+Extensions.swift in Sources */,
|
||||||
D0477F2C25C6EBAD005C5368 /* OpenInDefaultBrowserActivity.swift in Sources */,
|
D0477F2C25C6EBAD005C5368 /* OpenInDefaultBrowserActivity.swift in Sources */,
|
||||||
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */,
|
D07F4D9825D493E300F61133 /* MuteView.swift in Sources */,
|
||||||
|
@ -1198,6 +1220,7 @@
|
||||||
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
||||||
D035F87D25B7F61600DC75ED /* TimelinesViewController.swift in Sources */,
|
D035F87D25B7F61600DC75ED /* TimelinesViewController.swift in Sources */,
|
||||||
D08DFAF725CE20EA0005DA98 /* ScrollableToTop.swift in Sources */,
|
D08DFAF725CE20EA0005DA98 /* ScrollableToTop.swift in Sources */,
|
||||||
|
D05A0A6726363FEA00F9BCE1 /* AnnouncementReactionView.swift in Sources */,
|
||||||
D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
||||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
|
@ -1227,14 +1250,17 @@
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||||
D025B14D25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */,
|
D025B14D25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */,
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
|
D0D8B0312622398F00A874A4 /* AnnouncementTableViewCell.swift in Sources */,
|
||||||
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
||||||
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
||||||
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */,
|
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */,
|
||||||
|
D0D8B04126223A1E00A874A4 /* AnnouncementView.swift in Sources */,
|
||||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
||||||
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */,
|
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */,
|
||||||
D0BE97D725D0863E0057E161 /* ImagePastableTextView.swift in Sources */,
|
D0BE97D725D0863E0057E161 /* ImagePastableTextView.swift in Sources */,
|
||||||
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||||
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
||||||
|
D05A0A6D2636400100F9BCE1 /* AnnouncementReactionContentConfiguration.swift in Sources */,
|
||||||
D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */,
|
D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */,
|
||||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||||
|
@ -1248,6 +1274,7 @@
|
||||||
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
|
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
|
||||||
D0FCD6AD261AB2DD00113701 /* InstancePickerViewController.swift in Sources */,
|
D0FCD6AD261AB2DD00113701 /* InstancePickerViewController.swift in Sources */,
|
||||||
D0070252255921B100F38136 /* AccountFieldView.swift in Sources */,
|
D0070252255921B100F38136 /* AccountFieldView.swift in Sources */,
|
||||||
|
D05A0A77263644B600F9BCE1 /* AnnouncementReactionsCollectionView.swift in Sources */,
|
||||||
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
|
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
|
||||||
D0BE97A325CF44310057E161 /* CGRect+Extensions.swift in Sources */,
|
D0BE97A325CF44310057E161 /* CGRect+Extensions.swift in Sources */,
|
||||||
D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */,
|
D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */,
|
||||||
|
@ -1277,6 +1304,7 @@
|
||||||
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
||||||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
||||||
D09D971825C64682007E6394 /* InstanceCollectionViewCell.swift in Sources */,
|
D09D971825C64682007E6394 /* InstanceCollectionViewCell.swift in Sources */,
|
||||||
|
D05A0A5D26363DC700F9BCE1 /* AnnouncementReactionCollectionViewCell.swift in Sources */,
|
||||||
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */,
|
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct AnnouncementService {
|
||||||
|
public let announcement: Announcement
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
|
init(announcement: Announcement,
|
||||||
|
environment: AppEnvironment,
|
||||||
|
mastodonAPIClient: MastodonAPIClient,
|
||||||
|
contentDatabase: ContentDatabase) {
|
||||||
|
self.announcement = announcement
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
self.contentDatabase = contentDatabase
|
||||||
|
navigationService = NavigationService(environment: environment,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension AnnouncementService {
|
||||||
|
func dismiss() -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(EmptyEndpoint.dismissAnnouncement(id: announcement.id))
|
||||||
|
.flatMap { _ in mastodonAPIClient.request(AnnouncementsEndpoint.announcements) }
|
||||||
|
.flatMap(contentDatabase.update(announcements:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addReaction(name: String) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(EmptyEndpoint.addAnnouncementReaction(id: announcement.id, name: name))
|
||||||
|
.flatMap { _ in mastodonAPIClient.request(AnnouncementsEndpoint.announcements) }
|
||||||
|
.flatMap(contentDatabase.update(announcements:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeReaction(name: String) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(EmptyEndpoint.removeAnnouncementReaction(id: announcement.id, name: name))
|
||||||
|
.flatMap { _ in mastodonAPIClient.request(AnnouncementsEndpoint.announcements) }
|
||||||
|
.flatMap(contentDatabase.update(announcements:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct AnnouncementsService {
|
||||||
|
public let sections: AnyPublisher<[CollectionSection], Error>
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
public let titleLocalizationComponents: AnyPublisher<[String], Never>
|
||||||
|
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
|
init(environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
self.contentDatabase = contentDatabase
|
||||||
|
sections = contentDatabase.announcementsPublisher()
|
||||||
|
navigationService = NavigationService(environment: environment,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
titleLocalizationComponents = Just(["main-navigation.announcements"]).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementsService: CollectionService {
|
||||||
|
public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(AnnouncementsEndpoint.announcements)
|
||||||
|
.flatMap(contentDatabase.update(announcements:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,9 +82,7 @@ public extension IdentityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshAnnouncements() -> AnyPublisher<Never, Error> {
|
func refreshAnnouncements() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(AnnouncementsEndpoint.announcements)
|
announcementsService().request(maxId: nil, minId: nil, search: nil)
|
||||||
.flatMap(contentDatabase.update(announcements:))
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func confirmIdentity() -> AnyPublisher<Never, Error> {
|
func confirmIdentity() -> AnyPublisher<Never, Error> {
|
||||||
|
@ -185,6 +183,10 @@ public extension IdentityService {
|
||||||
contentDatabase.expiredFiltersPublisher()
|
contentDatabase.expiredFiltersPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func announcementCountPublisher() -> AnyPublisher<(total: Int, unread: Int), Error> {
|
||||||
|
contentDatabase.announcementCountPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
||||||
contentDatabase.pickerEmojisPublisher()
|
contentDatabase.pickerEmojisPublisher()
|
||||||
}
|
}
|
||||||
|
@ -296,6 +298,12 @@ public extension IdentityService {
|
||||||
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func announcementsService() -> AnnouncementsService {
|
||||||
|
AnnouncementsService(environment: environment,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
func emojiPickerService() -> EmojiPickerService {
|
func emojiPickerService() -> EmojiPickerService {
|
||||||
EmojiPickerService(contentDatabase: contentDatabase)
|
EmojiPickerService(contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,13 @@ public extension NavigationService {
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func announcementService(announcement: Announcement) -> AnnouncementService {
|
||||||
|
AnnouncementService(announcement: announcement,
|
||||||
|
environment: environment,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
func timelineService(timeline: Timeline) -> TimelineService {
|
func timelineService(timeline: Timeline) -> TimelineService {
|
||||||
TimelineService(timeline: timeline,
|
TimelineService(timeline: timeline,
|
||||||
environment: environment,
|
environment: environment,
|
||||||
|
|
|
@ -9,8 +9,8 @@ final class EmojiPickerViewController: UICollectionViewController {
|
||||||
|
|
||||||
private let viewModel: EmojiPickerViewModel
|
private let viewModel: EmojiPickerViewModel
|
||||||
private let selectionAction: (EmojiPickerViewController, PickerEmoji) -> Void
|
private let selectionAction: (EmojiPickerViewController, PickerEmoji) -> Void
|
||||||
private let deletionAction: (EmojiPickerViewController) -> Void
|
private let deletionAction: ((EmojiPickerViewController) -> Void)?
|
||||||
private let searchPresentationAction: (EmojiPickerViewController, UINavigationController) -> Void
|
private let searchPresentationAction: ((EmojiPickerViewController, UINavigationController) -> Void)?
|
||||||
private let skinToneButton = UIBarButtonItem()
|
private let skinToneButton = UIBarButtonItem()
|
||||||
private let deleteButton = UIBarButtonItem()
|
private let deleteButton = UIBarButtonItem()
|
||||||
private let closeButton = UIBarButtonItem(systemItem: .close)
|
private let closeButton = UIBarButtonItem(systemItem: .close)
|
||||||
|
@ -64,8 +64,8 @@ final class EmojiPickerViewController: UICollectionViewController {
|
||||||
|
|
||||||
init(viewModel: EmojiPickerViewModel,
|
init(viewModel: EmojiPickerViewModel,
|
||||||
selectionAction: @escaping (EmojiPickerViewController, PickerEmoji) -> Void,
|
selectionAction: @escaping (EmojiPickerViewController, PickerEmoji) -> Void,
|
||||||
deletionAction: @escaping (EmojiPickerViewController) -> Void,
|
deletionAction: ((EmojiPickerViewController) -> Void)?,
|
||||||
searchPresentationAction: @escaping (EmojiPickerViewController, UINavigationController) -> Void) {
|
searchPresentationAction: ((EmojiPickerViewController, UINavigationController) -> Void)?) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.selectionAction = selectionAction
|
self.selectionAction = selectionAction
|
||||||
self.deletionAction = deletionAction
|
self.deletionAction = deletionAction
|
||||||
|
@ -98,6 +98,7 @@ final class EmojiPickerViewController: UICollectionViewController {
|
||||||
presentSearchButton.translatesAutoresizingMaskIntoConstraints = false
|
presentSearchButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
presentSearchButton.accessibilityLabel = NSLocalizedString("emoji.search", comment: "")
|
presentSearchButton.accessibilityLabel = NSLocalizedString("emoji.search", comment: "")
|
||||||
presentSearchButton.addAction(UIAction { [weak self] _ in self?.presentSearch() }, for: .touchUpInside)
|
presentSearchButton.addAction(UIAction { [weak self] _ in self?.presentSearch() }, for: .touchUpInside)
|
||||||
|
presentSearchButton.isHidden = searchPresentationAction == nil
|
||||||
|
|
||||||
skinToneButton.accessibilityLabel =
|
skinToneButton.accessibilityLabel =
|
||||||
NSLocalizedString("emoji.default-skin-tone-button.accessibility-label", comment: "")
|
NSLocalizedString("emoji.default-skin-tone-button.accessibility-label", comment: "")
|
||||||
|
@ -111,11 +112,15 @@ final class EmojiPickerViewController: UICollectionViewController {
|
||||||
deleteButton.primaryAction = UIAction(image: UIImage(systemName: "delete.left")) { [weak self] _ in
|
deleteButton.primaryAction = UIAction(image: UIImage(systemName: "delete.left")) { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
self.deletionAction(self)
|
self.deletionAction?(self)
|
||||||
}
|
}
|
||||||
deleteButton.tintColor = .label
|
deleteButton.tintColor = .label
|
||||||
|
|
||||||
navigationItem.rightBarButtonItems = [deleteButton, skinToneButton]
|
if deletionAction != nil {
|
||||||
|
navigationItem.rightBarButtonItems = [deleteButton, skinToneButton]
|
||||||
|
} else {
|
||||||
|
navigationItem.rightBarButtonItem = skinToneButton
|
||||||
|
}
|
||||||
|
|
||||||
closeButton.primaryAction = UIAction { [weak self] _ in
|
closeButton.primaryAction = UIAction { [weak self] _ in
|
||||||
self?.presentingViewController?.dismiss(animated: true)
|
self?.presentingViewController?.dismiss(animated: true)
|
||||||
|
@ -228,7 +233,7 @@ private extension EmojiPickerViewController {
|
||||||
navigationItem.leftBarButtonItem = closeButton
|
navigationItem.leftBarButtonItem = closeButton
|
||||||
navigationItem.rightBarButtonItems = [self.skinToneButton]
|
navigationItem.rightBarButtonItems = [self.skinToneButton]
|
||||||
collectionView.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
collectionView.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||||
searchPresentationAction(self, navigationController)
|
searchPresentationAction?(self, navigationController)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadVisibleItems() {
|
func reloadVisibleItems() {
|
||||||
|
|
|
@ -331,6 +331,13 @@ extension TableViewController: AVPlayerViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TableViewController: UIPopoverPresentationControllerDelegate {
|
||||||
|
func adaptivePresentationStyle(for controller: UIPresentationController,
|
||||||
|
traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||||
|
.none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension TableViewController: ZoomAnimatorDelegate {
|
extension TableViewController: ZoomAnimatorDelegate {
|
||||||
func transitionWillStartWith(zoomAnimator: ZoomAnimator) {
|
func transitionWillStartWith(zoomAnimator: ZoomAnimator) {
|
||||||
view.layoutIfNeeded()
|
view.layoutIfNeeded()
|
||||||
|
@ -533,6 +540,10 @@ private extension TableViewController {
|
||||||
share(url: url)
|
share(url: url)
|
||||||
case let .navigation(navigation):
|
case let .navigation(navigation):
|
||||||
handle(navigation: navigation)
|
handle(navigation: navigation)
|
||||||
|
case let .reload(collectionItem):
|
||||||
|
reload(collectionItem: collectionItem)
|
||||||
|
case let .presentEmojiPicker(sourceViewTag, selectionAction):
|
||||||
|
presentEmojiPicker(sourceViewTag: sourceViewTag, selectionAction: selectionAction)
|
||||||
case let .attachment(attachmentViewModel, statusViewModel):
|
case let .attachment(attachmentViewModel, statusViewModel):
|
||||||
present(attachmentViewModel: attachmentViewModel, statusViewModel: statusViewModel)
|
present(attachmentViewModel: attachmentViewModel, statusViewModel: statusViewModel)
|
||||||
case let .compose(identity, inReplyToViewModel, redraft, redraftWasContextParent, directMessageTo):
|
case let .compose(identity, inReplyToViewModel, redraft, redraftWasContextParent, directMessageTo):
|
||||||
|
@ -582,6 +593,40 @@ private extension TableViewController {
|
||||||
viewModel.select(indexPath: indexPath)
|
viewModel.select(indexPath: indexPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reload(collectionItem: CollectionItem) {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
|
||||||
|
snapshot.reloadItems([collectionItem])
|
||||||
|
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentEmojiPicker(sourceViewTag: Int, selectionAction: @escaping (String) -> Void) {
|
||||||
|
guard let fromView = view.viewWithTag(sourceViewTag) else { return }
|
||||||
|
|
||||||
|
let emojiPickerViewModel = EmojiPickerViewModel(identityContext: viewModel.identityContext)
|
||||||
|
|
||||||
|
let emojiPickerController = EmojiPickerViewController(
|
||||||
|
viewModel: emojiPickerViewModel,
|
||||||
|
selectionAction: { [weak self] in
|
||||||
|
selectionAction($1.name)
|
||||||
|
self?.dismiss(animated: true)
|
||||||
|
},
|
||||||
|
deletionAction: nil,
|
||||||
|
searchPresentationAction: nil)
|
||||||
|
let navigationController = UINavigationController(rootViewController: emojiPickerController)
|
||||||
|
|
||||||
|
navigationController.preferredContentSize = .init(
|
||||||
|
width: view.readableContentGuide.layoutFrame.width,
|
||||||
|
height: view.frame.height / 2)
|
||||||
|
navigationController.modalPresentationStyle = .popover
|
||||||
|
navigationController.popoverPresentationController?.delegate = self
|
||||||
|
navigationController.popoverPresentationController?.sourceView = fromView
|
||||||
|
navigationController.popoverPresentationController?.backgroundColor = .clear
|
||||||
|
|
||||||
|
present(navigationController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
func present(attachmentViewModel: AttachmentViewModel, statusViewModel: StatusViewModel) {
|
func present(attachmentViewModel: AttachmentViewModel, statusViewModel: StatusViewModel) {
|
||||||
switch attachmentViewModel.attachment.type {
|
switch attachmentViewModel.attachment.type {
|
||||||
case .audio, .video:
|
case .audio, .video:
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ViewModels
|
||||||
|
|
||||||
final class TimelinesViewController: UIPageViewController {
|
final class TimelinesViewController: UIPageViewController {
|
||||||
private let segmentedControl = UISegmentedControl()
|
private let segmentedControl = UISegmentedControl()
|
||||||
|
private let announcementsButton = UIBarButtonItem()
|
||||||
private let timelineViewControllers: [TableViewController]
|
private let timelineViewControllers: [TableViewController]
|
||||||
private let viewModel: NavigationViewModel
|
private let viewModel: NavigationViewModel
|
||||||
private let rootViewModel: RootViewModel
|
private let rootViewModel: RootViewModel
|
||||||
|
@ -39,6 +40,23 @@ final class TimelinesViewController: UIPageViewController {
|
||||||
title: NSLocalizedString("main-navigation.timelines", comment: ""),
|
title: NSLocalizedString("main-navigation.timelines", comment: ""),
|
||||||
image: UIImage(systemName: "newspaper"),
|
image: UIImage(systemName: "newspaper"),
|
||||||
selectedImage: nil)
|
selectedImage: nil)
|
||||||
|
|
||||||
|
announcementsButton.primaryAction = UIAction(
|
||||||
|
title: NSLocalizedString("main-navigation.announcements", comment: ""),
|
||||||
|
image: UIImage(systemName: "megaphone")) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let announcementsViewController = TableViewController(viewModel: viewModel.announcementsViewModel(),
|
||||||
|
rootViewModel: rootViewModel)
|
||||||
|
|
||||||
|
self.navigationController?.pushViewController(announcementsViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.$announcementCount
|
||||||
|
.sink { [weak self] in
|
||||||
|
self?.navigationItem.rightBarButtonItem = $0.total > 0 ? self?.announcementsButton : nil
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
|
|
@ -9,6 +9,8 @@ public enum CollectionItemEvent {
|
||||||
case contextParentDeleted
|
case contextParentDeleted
|
||||||
case refresh
|
case refresh
|
||||||
case navigation(Navigation)
|
case navigation(Navigation)
|
||||||
|
case reload(CollectionItem)
|
||||||
|
case presentEmojiPicker(sourceViewTag: Int, selectionAction: (String) -> Void)
|
||||||
case attachment(AttachmentViewModel, StatusViewModel)
|
case attachment(AttachmentViewModel, StatusViewModel)
|
||||||
case compose(identity: Identity? = nil,
|
case compose(identity: Identity? = nil,
|
||||||
inReplyTo: StatusViewModel? = nil,
|
inReplyTo: StatusViewModel? = nil,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public struct AnnouncementReactionViewModel {
|
||||||
|
let identityContext: IdentityContext
|
||||||
|
|
||||||
|
private let announcementReaction: AnnouncementReaction
|
||||||
|
|
||||||
|
public init(announcementReaction: AnnouncementReaction, identityContext: IdentityContext) {
|
||||||
|
self.announcementReaction = announcementReaction
|
||||||
|
self.identityContext = identityContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension AnnouncementReactionViewModel {
|
||||||
|
var name: String { announcementReaction.name }
|
||||||
|
|
||||||
|
var count: Int { announcementReaction.count }
|
||||||
|
|
||||||
|
var me: Bool { announcementReaction.me }
|
||||||
|
|
||||||
|
var url: URL? {
|
||||||
|
if identityContext.appPreferences.animateCustomEmojis {
|
||||||
|
return announcementReaction.url?.url
|
||||||
|
} else {
|
||||||
|
return announcementReaction.staticUrl?.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public final class AnnouncementViewModel: ObservableObject {
|
||||||
|
public let identityContext: IdentityContext
|
||||||
|
|
||||||
|
private let announcementService: AnnouncementService
|
||||||
|
private let eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
|
init(announcementService: AnnouncementService,
|
||||||
|
identityContext: IdentityContext,
|
||||||
|
eventsSubject: PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>) {
|
||||||
|
self.announcementService = announcementService
|
||||||
|
self.identityContext = identityContext
|
||||||
|
self.eventsSubject = eventsSubject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension AnnouncementViewModel {
|
||||||
|
var announcement: Announcement { announcementService.announcement }
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension AnnouncementViewModel {
|
||||||
|
func urlSelected(_ url: URL) {
|
||||||
|
eventsSubject.send(
|
||||||
|
announcementService.navigationService.item(url: url)
|
||||||
|
.map { .navigation($0) }
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
eventsSubject.send(
|
||||||
|
announcementService.dismiss()
|
||||||
|
.map { _ in .ignorableOutput }
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func reload() {
|
||||||
|
eventsSubject.send(Just(.reload(.announcement(announcementService.announcement)))
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func addReaction(name: String) {
|
||||||
|
eventsSubject.send(
|
||||||
|
announcementService.addReaction(name: name)
|
||||||
|
.map { _ in .ignorableOutput }
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeReaction(name: String) {
|
||||||
|
eventsSubject.send(
|
||||||
|
announcementService.removeReaction(name: name)
|
||||||
|
.map { _ in .ignorableOutput }
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentEmojiPicker(sourceViewTag: Int) {
|
||||||
|
eventsSubject.send(Just(.presentEmojiPicker(
|
||||||
|
sourceViewTag: sourceViewTag,
|
||||||
|
selectionAction: { [weak self] in self?.addReaction(name: $0) }))
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
}
|
|
@ -194,6 +194,20 @@ public class CollectionItemsViewModel: ObservableObject {
|
||||||
|
|
||||||
viewModelCache[item] = viewModel
|
viewModelCache[item] = viewModel
|
||||||
|
|
||||||
|
return viewModel
|
||||||
|
case let .announcement(announcement):
|
||||||
|
if let cachedViewModel = cachedViewModel {
|
||||||
|
return cachedViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewModel = AnnouncementViewModel(
|
||||||
|
announcementService: collectionService.navigationService.announcementService(
|
||||||
|
announcement: announcement),
|
||||||
|
identityContext: identityContext,
|
||||||
|
eventsSubject: eventsSubject)
|
||||||
|
|
||||||
|
viewModelCache[item] = viewModel
|
||||||
|
|
||||||
return viewModel
|
return viewModel
|
||||||
case let .moreResults(moreResults):
|
case let .moreResults(moreResults):
|
||||||
if let cachedViewModel = cachedViewModel {
|
if let cachedViewModel = cachedViewModel {
|
||||||
|
@ -300,6 +314,8 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
send(event: .navigation(.collection(collectionService
|
send(event: .navigation(.collection(collectionService
|
||||||
.navigationService
|
.navigationService
|
||||||
.timelineService(timeline: .tag(tag.name)))))
|
.timelineService(timeline: .tag(tag.name)))))
|
||||||
|
case .announcement:
|
||||||
|
break
|
||||||
case let .moreResults(moreResults):
|
case let .moreResults(moreResults):
|
||||||
searchScopeChangesSubject.send(moreResults.scope)
|
searchScopeChangesSubject.send(moreResults.scope)
|
||||||
}
|
}
|
||||||
|
@ -320,6 +336,8 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
return !configuration.isContextParent
|
return !configuration.isContextParent
|
||||||
case .loadMore:
|
case .loadMore:
|
||||||
return !((viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loading ?? false)
|
return !((viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loading ?? false)
|
||||||
|
case .announcement:
|
||||||
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ public final class NavigationViewModel: ObservableObject {
|
||||||
public let navigations: AnyPublisher<Navigation, Never>
|
public let navigations: AnyPublisher<Navigation, Never>
|
||||||
|
|
||||||
@Published public private(set) var recentIdentities = [Identity]()
|
@Published public private(set) var recentIdentities = [Identity]()
|
||||||
|
@Published public private(set) var announcementCount: (total: Int, unread: Int) = (0, 0)
|
||||||
@Published public var presentedNewStatusViewModel: NewStatusViewModel?
|
@Published public var presentedNewStatusViewModel: NewStatusViewModel?
|
||||||
@Published public var presentingSecondaryNavigation = false
|
@Published public var presentingSecondaryNavigation = false
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
|
@ -28,6 +29,10 @@ public final class NavigationViewModel: ObservableObject {
|
||||||
identityContext.service.recentIdentitiesPublisher()
|
identityContext.service.recentIdentitiesPublisher()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$recentIdentities)
|
.assign(to: &$recentIdentities)
|
||||||
|
|
||||||
|
identityContext.service.announcementCountPublisher()
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$announcementCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,4 +196,10 @@ public extension NavigationViewModel {
|
||||||
|
|
||||||
return conversationsViewModel
|
return conversationsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func announcementsViewModel() -> CollectionViewModel {
|
||||||
|
CollectionItemsViewModel(
|
||||||
|
collectionService: identityContext.service.announcementsService(),
|
||||||
|
identityContext: identityContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
53
Views/UIKit/AnnouncementReactionsCollectionView.swift
Normal file
53
Views/UIKit/AnnouncementReactionsCollectionView.swift
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AnnouncementReactionsCollectionView: UICollectionView {
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero, collectionViewLayout: Self.layout())
|
||||||
|
|
||||||
|
backgroundColor = .clear
|
||||||
|
isScrollEnabled = false
|
||||||
|
showsVerticalScrollIndicator = false
|
||||||
|
showsHorizontalScrollIndicator = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
if bounds.size != intrinsicContentSize {
|
||||||
|
invalidateIntrinsicContentSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
CGSize(width: UIView.noIntrinsicMetric, height: max(contentSize.height, .minimumButtonDimension))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AnnouncementReactionsCollectionView {
|
||||||
|
static func layout() -> UICollectionViewLayout {
|
||||||
|
let itemSize = NSCollectionLayoutSize(
|
||||||
|
widthDimension: .estimated(.minimumButtonDimension),
|
||||||
|
heightDimension: .estimated(.minimumButtonDimension))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
let groupSize = NSCollectionLayoutSize(
|
||||||
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .estimated(.minimumButtonDimension))
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
|
|
||||||
|
group.interItemSpacing = .flexible(.defaultSpacing)
|
||||||
|
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
||||||
|
section.interGroupSpacing = .defaultSpacing
|
||||||
|
|
||||||
|
return UICollectionViewCompositionalLayout(section: section)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class AnnouncementReactionCollectionViewCell: UICollectionViewCell {
|
||||||
|
var viewModel: AnnouncementReactionViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = AnnouncementReactionContentConfiguration(viewModel: viewModel)
|
||||||
|
|
||||||
|
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
|
||||||
|
if !state.isHighlighted && !state.isSelected {
|
||||||
|
backgroundConfiguration.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundConfiguration.cornerRadius = .defaultCornerRadius
|
||||||
|
|
||||||
|
self.backgroundConfiguration = backgroundConfiguration
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct AnnouncementContentConfiguration {
|
||||||
|
let viewModel: AnnouncementViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
AnnouncementView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> AnnouncementContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct AnnouncementReactionContentConfiguration {
|
||||||
|
let viewModel: AnnouncementReactionViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementReactionContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
AnnouncementReactionView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> AnnouncementReactionContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
98
Views/UIKit/Content Views/AnnouncementReactionView.swift
Normal file
98
Views/UIKit/Content Views/AnnouncementReactionView.swift
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SDWebImage
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class AnnouncementReactionView: UIView {
|
||||||
|
private let nameLabel = UILabel()
|
||||||
|
private let imageView = SDAnimatedImageView()
|
||||||
|
private let countLabel = UILabel()
|
||||||
|
private var announcementReactionConfiguration: AnnouncementReactionContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: AnnouncementReactionContentConfiguration) {
|
||||||
|
announcementReactionConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyAnnouncementReactionConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementReactionView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { announcementReactionConfiguration }
|
||||||
|
set {
|
||||||
|
guard let announcementReactionConfiguration = newValue as? AnnouncementReactionContentConfiguration else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.announcementReactionConfiguration = announcementReactionConfiguration
|
||||||
|
|
||||||
|
applyAnnouncementReactionConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AnnouncementReactionView {
|
||||||
|
static let meBackgroundColor = UIColor.link.withAlphaComponent(0.5)
|
||||||
|
static let backgroundColor = UIColor.secondarySystemBackground.withAlphaComponent(0.5)
|
||||||
|
func initialSetup() {
|
||||||
|
layer.cornerRadius = .defaultCornerRadius
|
||||||
|
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(imageView)
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(nameLabel)
|
||||||
|
nameLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
nameLabel.textAlignment = .center
|
||||||
|
nameLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
nameLabel.font = .preferredFont(forTextStyle: .body)
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(countLabel)
|
||||||
|
countLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
countLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
countLabel.textColor = .link
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
||||||
|
imageView.widthAnchor.constraint(equalToConstant: .minimumButtonDimension / 2),
|
||||||
|
imageView.heightAnchor.constraint(equalToConstant: .minimumButtonDimension / 2),
|
||||||
|
nameLabel.widthAnchor.constraint(equalToConstant: .minimumButtonDimension / 2),
|
||||||
|
nameLabel.heightAnchor.constraint(equalToConstant: .minimumButtonDimension / 2)
|
||||||
|
])
|
||||||
|
|
||||||
|
isAccessibilityElement = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyAnnouncementReactionConfiguration() {
|
||||||
|
let viewModel = announcementReactionConfiguration.viewModel
|
||||||
|
|
||||||
|
backgroundColor = viewModel.me ? Self.meBackgroundColor : Self.backgroundColor
|
||||||
|
|
||||||
|
nameLabel.text = viewModel.name
|
||||||
|
nameLabel.isHidden = viewModel.url != nil
|
||||||
|
|
||||||
|
imageView.sd_setImage(with: viewModel.url)
|
||||||
|
imageView.isHidden = viewModel.url == nil
|
||||||
|
|
||||||
|
countLabel.text = String(viewModel.count)
|
||||||
|
|
||||||
|
accessibilityLabel = viewModel.name.appendingWithSeparator(String(viewModel.count))
|
||||||
|
}
|
||||||
|
}
|
175
Views/UIKit/Content Views/AnnouncementView.swift
Normal file
175
Views/UIKit/Content Views/AnnouncementView.swift
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Mastodon
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class AnnouncementView: UIView {
|
||||||
|
private let contentTextView = TouchFallthroughTextView()
|
||||||
|
private let reactionButton = UIButton()
|
||||||
|
private let reactionsCollectionView = AnnouncementReactionsCollectionView()
|
||||||
|
private var announcementConfiguration: AnnouncementContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: AnnouncementContentConfiguration) {
|
||||||
|
announcementConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyAnnouncementConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var dataSource: UICollectionViewDiffableDataSource<Int, AnnouncementReaction> = {
|
||||||
|
let cellRegistration = UICollectionView.CellRegistration
|
||||||
|
<AnnouncementReactionCollectionViewCell, AnnouncementReaction> { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
$0.viewModel = AnnouncementReactionViewModel(
|
||||||
|
announcementReaction: $2,
|
||||||
|
identityContext: self.announcementConfiguration.viewModel.identityContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataSource = UICollectionViewDiffableDataSource
|
||||||
|
<Int, AnnouncementReaction>(collectionView: reactionsCollectionView) {
|
||||||
|
$0.dequeueConfiguredReusableCell(using: cellRegistration, for: $1, item: $2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementView {
|
||||||
|
static func estimatedHeight(width: CGFloat, announcement: Announcement) -> CGFloat {
|
||||||
|
UITableView.automaticDimension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { announcementConfiguration }
|
||||||
|
set {
|
||||||
|
guard let announcementConfiguration = newValue as? AnnouncementContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.announcementConfiguration = announcementConfiguration
|
||||||
|
|
||||||
|
applyAnnouncementConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementView: UITextViewDelegate {
|
||||||
|
func textView(
|
||||||
|
_ textView: UITextView,
|
||||||
|
shouldInteractWith URL: URL,
|
||||||
|
in characterRange: NSRange,
|
||||||
|
interaction: UITextItemInteraction) -> Bool {
|
||||||
|
switch interaction {
|
||||||
|
case .invokeDefaultAction:
|
||||||
|
announcementConfiguration.viewModel.urlSelected(URL)
|
||||||
|
return false
|
||||||
|
case .preview: return false
|
||||||
|
case .presentActions: return false
|
||||||
|
@unknown default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementView: UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
|
|
||||||
|
guard let reaction = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
if reaction.me {
|
||||||
|
announcementConfiguration.viewModel.removeReaction(name: reaction.name)
|
||||||
|
} else {
|
||||||
|
announcementConfiguration.viewModel.addReaction(name: reaction.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
UISelectionFeedbackGenerator().selectionChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AnnouncementView {
|
||||||
|
func initialSetup() {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
|
contentTextView.adjustsFontForContentSizeCategory = true
|
||||||
|
contentTextView.backgroundColor = .clear
|
||||||
|
contentTextView.delegate = self
|
||||||
|
stackView.addArrangedSubview(contentTextView)
|
||||||
|
|
||||||
|
let reactionStackView = UIStackView()
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(reactionStackView)
|
||||||
|
reactionStackView.spacing = .defaultSpacing
|
||||||
|
reactionStackView.alignment = .top
|
||||||
|
|
||||||
|
reactionStackView.addArrangedSubview(reactionButton)
|
||||||
|
reactionButton.tag = UUID().hashValue
|
||||||
|
reactionButton.accessibilityLabel = NSLocalizedString("announcement.insert-emoji", comment: "")
|
||||||
|
reactionButton.setImage(
|
||||||
|
UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .large)),
|
||||||
|
for: .normal)
|
||||||
|
reactionButton.addAction(
|
||||||
|
UIAction { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.announcementConfiguration.viewModel.presentEmojiPicker(sourceViewTag: self.reactionButton.tag)
|
||||||
|
},
|
||||||
|
for: .touchUpInside)
|
||||||
|
|
||||||
|
reactionStackView.addArrangedSubview(reactionsCollectionView)
|
||||||
|
reactionsCollectionView.delegate = self
|
||||||
|
reactionsCollectionView.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
|
||||||
|
reactionButton.widthAnchor.constraint(equalToConstant: .minimumButtonDimension),
|
||||||
|
reactionButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyAnnouncementConfiguration() {
|
||||||
|
let viewModel = announcementConfiguration.viewModel
|
||||||
|
let mutableContent = NSMutableAttributedString(attributedString: viewModel.announcement.content.attributed)
|
||||||
|
let contentFont = UIFont.preferredFont(forTextStyle: .callout)
|
||||||
|
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
||||||
|
|
||||||
|
mutableContent.removeAttribute(.font, range: contentRange)
|
||||||
|
mutableContent.addAttributes(
|
||||||
|
[.font: contentFont, .foregroundColor: UIColor.label],
|
||||||
|
range: contentRange)
|
||||||
|
mutableContent.insert(emojis: viewModel.announcement.emojis,
|
||||||
|
view: contentTextView,
|
||||||
|
identityContext: viewModel.identityContext)
|
||||||
|
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
|
||||||
|
contentTextView.attributedText = mutableContent
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Int, AnnouncementReaction>()
|
||||||
|
|
||||||
|
snapshot.appendSections([0])
|
||||||
|
snapshot.appendItems(viewModel.announcement.reactions, toSection: 0)
|
||||||
|
|
||||||
|
if snapshot.itemIdentifiers != dataSource.snapshot().itemIdentifiers {
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false) { viewModel.reload() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if !viewModel.announcement.read {
|
||||||
|
viewModel.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
Views/UIKit/Table View Cells/AnnouncementTableViewCell.swift
Normal file
15
Views/UIKit/Table View Cells/AnnouncementTableViewCell.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class AnnouncementTableViewCell: SeparatorConfiguredTableViewCell {
|
||||||
|
var viewModel: AnnouncementViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = AnnouncementContentConfiguration(viewModel: viewModel).updated(for: state)
|
||||||
|
accessibilityElements = [contentView]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue