mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 01:31:02 +00:00
Notifications wip
This commit is contained in:
parent
5540c959f0
commit
00605ff212
26 changed files with 599 additions and 13 deletions
|
@ -110,6 +110,13 @@ extension ContentDatabase {
|
||||||
t.column("id", .text).notNull()
|
t.column("id", .text).notNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try db.create(table: "notificationRecord") { t in
|
||||||
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
|
t.column("type", .text).notNull()
|
||||||
|
t.column("accountId", .text).notNull().references("accountRecord")
|
||||||
|
t.column("statusId").references("statusRecord")
|
||||||
|
}
|
||||||
|
|
||||||
try db.create(table: "statusAncestorJoin") { t in
|
try db.create(table: "statusAncestorJoin") { t in
|
||||||
t.column("parentId", .text).indexed().notNull()
|
t.column("parentId", .text).indexed().notNull()
|
||||||
.references("statusRecord", onDelete: .cascade)
|
.references("statusRecord", onDelete: .cascade)
|
||||||
|
|
|
@ -292,6 +292,16 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insert(notifications: [MastodonNotification]) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher {
|
||||||
|
for notification in notifications {
|
||||||
|
try notification.save($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
||||||
|
@ -346,6 +356,28 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notificationsPublisher() -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
|
ValueObservation.tracking(
|
||||||
|
NotificationInfo.request(
|
||||||
|
NotificationRecord.order(NotificationRecord.Columns.id.desc)).fetchAll)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
|
.map { [$0.map {
|
||||||
|
let configuration: CollectionItem.StatusConfiguration?
|
||||||
|
|
||||||
|
if $0.record.type == .mention, let statusInfo = $0.statusInfo {
|
||||||
|
configuration = CollectionItem.StatusConfiguration(
|
||||||
|
showContentToggled: statusInfo.showContentToggled,
|
||||||
|
showAttachmentsToggled: statusInfo.showAttachmentsToggled)
|
||||||
|
} else {
|
||||||
|
configuration = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return .notification(MastodonNotification(info: $0), configuration)
|
||||||
|
}] }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func lastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
func lastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
||||||
try? databaseWriter.read {
|
try? databaseWriter.read {
|
||||||
try String.fetchOne(
|
try String.fetchOne(
|
||||||
|
@ -366,6 +398,8 @@ private extension ContentDatabase {
|
||||||
useHomeTimelineLastReadId: Bool,
|
useHomeTimelineLastReadId: Bool,
|
||||||
useNotificationsLastReadId: Bool) throws {
|
useNotificationsLastReadId: Bool) throws {
|
||||||
try databaseWriter.write {
|
try databaseWriter.write {
|
||||||
|
try NotificationRecord.deleteAll($0)
|
||||||
|
|
||||||
if useHomeTimelineLastReadId {
|
if useHomeTimelineLastReadId {
|
||||||
try TimelineRecord.filter(TimelineRecord.Columns.id != Timeline.home.id).deleteAll($0)
|
try TimelineRecord.filter(TimelineRecord.Columns.id != Timeline.home.id).deleteAll($0)
|
||||||
var statusIds = try Status.Id.fetchAll(
|
var statusIds = try Status.Id.fetchAll(
|
||||||
|
|
23
DB/Sources/DB/Content/NotificationInfo.swift
Normal file
23
DB/Sources/DB/Content/NotificationInfo.swift
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
struct NotificationInfo: Codable, Hashable, FetchableRecord {
|
||||||
|
let record: NotificationRecord
|
||||||
|
let accountInfo: AccountInfo
|
||||||
|
let statusInfo: StatusInfo?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationInfo {
|
||||||
|
static func addingIncludes<T: DerivableRequest>(_ request: T) -> T where T.RowDecoder == NotificationRecord {
|
||||||
|
request.including(required: AccountInfo.addingIncludes(NotificationRecord.account)
|
||||||
|
.forKey(CodingKeys.accountInfo))
|
||||||
|
.including(optional: StatusInfo.addingIncludesForNotificationInfo(NotificationRecord.status)
|
||||||
|
.forKey(CodingKeys.statusInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func request(_ request: QueryInterfaceRequest<NotificationRecord>) -> QueryInterfaceRequest<Self> {
|
||||||
|
addingIncludes(request).asRequest(of: self)
|
||||||
|
}
|
||||||
|
}
|
31
DB/Sources/DB/Content/NotificationRecord.swift
Normal file
31
DB/Sources/DB/Content/NotificationRecord.swift
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct NotificationRecord: ContentDatabaseRecord, Hashable {
|
||||||
|
let id: String
|
||||||
|
let type: MastodonNotification.NotificationType
|
||||||
|
let accountId: Account.Id
|
||||||
|
let statusId: Status.Id?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationRecord {
|
||||||
|
enum Columns {
|
||||||
|
static let id = Column(NotificationRecord.CodingKeys.id)
|
||||||
|
static let type = Column(NotificationRecord.CodingKeys.type)
|
||||||
|
static let accountId = Column(NotificationRecord.CodingKeys.accountId)
|
||||||
|
static let statusId = Column(NotificationRecord.CodingKeys.statusId)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let account = belongsTo(AccountRecord.self)
|
||||||
|
static let status = belongsTo(StatusRecord.self)
|
||||||
|
|
||||||
|
init(notification: MastodonNotification) {
|
||||||
|
id = notification.id
|
||||||
|
type = notification.type
|
||||||
|
accountId = notification.account.id
|
||||||
|
statusId = notification.status?.id
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,15 +16,17 @@ struct StatusInfo: Codable, Hashable, FetchableRecord {
|
||||||
|
|
||||||
extension StatusInfo {
|
extension StatusInfo {
|
||||||
static func addingIncludes<T: DerivableRequest>(_ request: T) -> T where T.RowDecoder == StatusRecord {
|
static func addingIncludes<T: DerivableRequest>(_ request: T) -> T where T.RowDecoder == StatusRecord {
|
||||||
request.including(required: AccountInfo.addingIncludes(StatusRecord.account).forKey(CodingKeys.accountInfo))
|
addingOptionalIncludes(
|
||||||
.including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount)
|
request
|
||||||
.forKey(CodingKeys.reblogAccountInfo))
|
.including(required: AccountInfo.addingIncludes(StatusRecord.account).forKey(CodingKeys.accountInfo)))
|
||||||
.including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord))
|
}
|
||||||
.including(optional: StatusRecord.showContentToggle.forKey(CodingKeys.showContentToggle))
|
|
||||||
.including(optional: StatusRecord.reblogShowContentToggle.forKey(CodingKeys.reblogShowContentToggle))
|
// Hack, remove once GRDB supports chaining a required association behind an optional association
|
||||||
.including(optional: StatusRecord.showAttachmentsToggle.forKey(CodingKeys.showAttachmentsToggle))
|
static func addingIncludesForNotificationInfo<T: DerivableRequest>(
|
||||||
.including(optional: StatusRecord.reblogShowAttachmentsToggle
|
_ request: T) -> T where T.RowDecoder == StatusRecord {
|
||||||
.forKey(CodingKeys.reblogShowAttachmentsToggle))
|
addingOptionalIncludes(
|
||||||
|
request
|
||||||
|
.including(optional: AccountInfo.addingIncludes(StatusRecord.account).forKey(CodingKeys.accountInfo)))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func request(_ request: QueryInterfaceRequest<StatusRecord>) -> QueryInterfaceRequest<Self> {
|
static func request(_ request: QueryInterfaceRequest<StatusRecord>) -> QueryInterfaceRequest<Self> {
|
||||||
|
@ -43,3 +45,16 @@ extension StatusInfo {
|
||||||
showAttachmentsToggle != nil || reblogShowAttachmentsToggle != nil
|
showAttachmentsToggle != nil || reblogShowAttachmentsToggle != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension StatusInfo {
|
||||||
|
static func addingOptionalIncludes<T: DerivableRequest>(_ request: T) -> T where T.RowDecoder == StatusRecord {
|
||||||
|
request.including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount)
|
||||||
|
.forKey(CodingKeys.reblogAccountInfo))
|
||||||
|
.including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord))
|
||||||
|
.including(optional: StatusRecord.showContentToggle.forKey(CodingKeys.showContentToggle))
|
||||||
|
.including(optional: StatusRecord.reblogShowContentToggle.forKey(CodingKeys.reblogShowContentToggle))
|
||||||
|
.including(optional: StatusRecord.showAttachmentsToggle.forKey(CodingKeys.showAttachmentsToggle))
|
||||||
|
.including(optional: StatusRecord.reblogShowAttachmentsToggle
|
||||||
|
.forKey(CodingKeys.reblogShowAttachmentsToggle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ public enum CollectionItem: Hashable {
|
||||||
case status(Status, StatusConfiguration)
|
case status(Status, StatusConfiguration)
|
||||||
case loadMore(LoadMore)
|
case loadMore(LoadMore)
|
||||||
case account(Account)
|
case account(Account)
|
||||||
|
case notification(MastodonNotification, StatusConfiguration?)
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension CollectionItem {
|
public extension CollectionItem {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension MastodonNotification {
|
||||||
|
func save(_ db: Database) throws {
|
||||||
|
try account.save(db)
|
||||||
|
try status?.save(db)
|
||||||
|
try NotificationRecord(notification: self).save(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(info: NotificationInfo) {
|
||||||
|
let status: Status?
|
||||||
|
|
||||||
|
if let statusInfo = info.statusInfo {
|
||||||
|
status = .init(info: statusInfo)
|
||||||
|
} else {
|
||||||
|
status = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
id: info.record.id,
|
||||||
|
type: info.record.type,
|
||||||
|
account: .init(info: info.accountInfo),
|
||||||
|
status: status)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,8 @@ final class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionIt
|
||||||
accountListCell.viewModel = accountViewModel
|
accountListCell.viewModel = accountViewModel
|
||||||
case let (loadMoreCell as LoadMoreCell, loadMoreViewModel as LoadMoreViewModel):
|
case let (loadMoreCell as LoadMoreCell, loadMoreViewModel as LoadMoreViewModel):
|
||||||
loadMoreCell.viewModel = loadMoreViewModel
|
loadMoreCell.viewModel = loadMoreViewModel
|
||||||
|
case let (notificationListCell as NotificationListCell, notificationViewModel as NotificationViewModel):
|
||||||
|
notificationListCell.viewModel = notificationViewModel
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
extension CollectionItem {
|
extension CollectionItem {
|
||||||
static let cellClasses = [StatusListCell.self, AccountListCell.self, LoadMoreCell.self]
|
static let cellClasses = [
|
||||||
|
StatusListCell.self,
|
||||||
|
AccountListCell.self,
|
||||||
|
LoadMoreCell.self,
|
||||||
|
NotificationListCell.self]
|
||||||
|
|
||||||
var cellClass: AnyClass {
|
var cellClass: AnyClass {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -13,6 +18,8 @@ extension CollectionItem {
|
||||||
return AccountListCell.self
|
return AccountListCell.self
|
||||||
case .loadMore:
|
case .loadMore:
|
||||||
return LoadMoreCell.self
|
return LoadMoreCell.self
|
||||||
|
case let .notification(_, statusConfiguration):
|
||||||
|
return statusConfiguration == nil ? NotificationListCell.self : StatusListCell.self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
@ -21,4 +22,25 @@ extension String {
|
||||||
|
|
||||||
return attributed
|
return attributed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func localizedBolding(displayName: String, emoji: [Emoji], label: UILabel) -> NSAttributedString {
|
||||||
|
let mutableString = NSMutableAttributedString(
|
||||||
|
string: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString(self, comment: ""),
|
||||||
|
displayName))
|
||||||
|
|
||||||
|
let range = (mutableString.string as NSString).range(of: displayName)
|
||||||
|
|
||||||
|
if range.location != NSNotFound,
|
||||||
|
let boldFontDescriptor = label.font.fontDescriptor.withSymbolicTraits([.traitBold]) {
|
||||||
|
let boldFont = UIFont(descriptor: boldFontDescriptor, size: label.font.pointSize)
|
||||||
|
|
||||||
|
mutableString.setAttributes([NSAttributedString.Key.font: boldFont], range: range)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableString.insert(emoji: emoji, view: label)
|
||||||
|
mutableString.resizeAttachments(toLineHeight: label.font.lineHeight)
|
||||||
|
|
||||||
|
return mutableString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,13 @@
|
||||||
"filter.context.thread" = "Conversations";
|
"filter.context.thread" = "Conversations";
|
||||||
"filter.context.account" = "Profiles";
|
"filter.context.account" = "Profiles";
|
||||||
"filter.context.unknown" = "Unknown context";
|
"filter.context.unknown" = "Unknown context";
|
||||||
|
"notifications" = "Notifications";
|
||||||
|
"notifications.reblogged-your-status" = "%@ boosted your status";
|
||||||
|
"notifications.favourited-your-status" = "%@ favorited your status";
|
||||||
|
"notifications.followed-you" = "%@ followed you";
|
||||||
|
"notifications.poll-ended" = "A poll you have voted in has ended";
|
||||||
|
"notifications.your-poll-ended" = "Your poll has ended";
|
||||||
|
"notifications.unknown" = "Notification from %@";
|
||||||
"status.reblogged-by" = "%@ boosted";
|
"status.reblogged-by" = "%@ boosted";
|
||||||
"status.pinned-post" = "Pinned post";
|
"status.pinned-post" = "Pinned post";
|
||||||
"status.show-more" = "Show More";
|
"status.show-more" = "Show More";
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct MastodonNotification: Codable, Hashable {
|
||||||
|
public let id: String
|
||||||
|
public let type: NotificationType
|
||||||
|
public let account: Account
|
||||||
|
public let status: Status?
|
||||||
|
|
||||||
|
public init(id: String, type: MastodonNotification.NotificationType, account: Account, status: Status?) {
|
||||||
|
self.id = id
|
||||||
|
self.type = type
|
||||||
|
self.account = account
|
||||||
|
self.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification {
|
||||||
|
public enum NotificationType: String, Codable, Unknowable {
|
||||||
|
case follow
|
||||||
|
case mention
|
||||||
|
case reblog
|
||||||
|
case favourite
|
||||||
|
case poll
|
||||||
|
case followRequest = "follow_request"
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
public static var unknownCase: Self { .unknown }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum NotificationsEndpoint {
|
||||||
|
case notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationsEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = [MastodonNotification]
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
["notifications"]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .notifications:
|
||||||
|
return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,9 @@
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
||||||
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; };
|
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; };
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
||||||
|
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; };
|
||||||
|
D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; };
|
||||||
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; };
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
||||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
||||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||||
|
@ -118,6 +121,9 @@
|
||||||
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
|
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
|
||||||
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsView.swift; sourceTree = "<group>"; };
|
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsView.swift; sourceTree = "<group>"; };
|
||||||
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||||
|
D036AA01254B6101009094DF /* NotificationListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListCell.swift; sourceTree = "<group>"; };
|
||||||
|
D036AA06254B6118009094DF /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
|
||||||
|
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAndSyncingPreferencesView.swift; sourceTree = "<group>"; };
|
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAndSyncingPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -339,7 +345,10 @@
|
||||||
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
|
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
|
||||||
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
|
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
|
||||||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
||||||
|
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */,
|
||||||
|
D036AA01254B6101009094DF /* NotificationListCell.swift */,
|
||||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||||
|
D036AA06254B6118009094DF /* NotificationView.swift */,
|
||||||
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
||||||
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */,
|
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */,
|
||||||
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */,
|
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */,
|
||||||
|
@ -599,6 +608,7 @@
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
||||||
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */,
|
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */,
|
||||||
|
D036AA07254B6118009094DF /* NotificationView.swift in Sources */,
|
||||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||||
|
@ -609,6 +619,7 @@
|
||||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||||
|
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */,
|
||||||
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
|
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||||
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||||
|
@ -636,6 +647,7 @@
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||||
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
||||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
||||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
||||||
|
|
|
@ -208,6 +208,10 @@ public extension IdentityService {
|
||||||
func service(timeline: Timeline) -> TimelineService {
|
func service(timeline: Timeline) -> TimelineService {
|
||||||
TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notificationsService() -> NotificationsService {
|
||||||
|
NotificationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentityService {
|
private extension IdentityService {
|
||||||
|
|
|
@ -72,6 +72,13 @@ public extension NavigationService {
|
||||||
func loadMoreService(loadMore: LoadMore) -> LoadMoreService {
|
func loadMoreService(loadMore: LoadMore) -> LoadMoreService {
|
||||||
LoadMoreService(loadMore: loadMore, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
LoadMoreService(loadMore: loadMore, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notificationService(notification: MastodonNotification) -> NotificationService {
|
||||||
|
NotificationService(
|
||||||
|
notification: notification,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension NavigationService {
|
private extension NavigationService {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct NotificationService {
|
||||||
|
public let notification: MastodonNotification
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
|
init(notification: MastodonNotification, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
|
self.notification = notification
|
||||||
|
self.navigationService = NavigationService(
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase,
|
||||||
|
status: nil)
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
self.contentDatabase = contentDatabase
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct NotificationsService {
|
||||||
|
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||||
|
public let nextPageMaxId: AnyPublisher<String, Never>
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
private let contentDatabase: ContentDatabase
|
||||||
|
private let nextPageMaxIdSubject: CurrentValueSubject<String, Never>
|
||||||
|
|
||||||
|
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
self.contentDatabase = contentDatabase
|
||||||
|
|
||||||
|
let nextPageMaxIdSubject = CurrentValueSubject<String, Never>(String(Int.max))
|
||||||
|
|
||||||
|
self.nextPageMaxIdSubject = nextPageMaxIdSubject
|
||||||
|
sections = contentDatabase.notificationsPublisher()
|
||||||
|
.handleEvents(receiveOutput: {
|
||||||
|
guard case let .notification(notification, _) = $0.last?.last,
|
||||||
|
notification.id < nextPageMaxIdSubject.value
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
nextPageMaxIdSubject.send(notification.id)
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||||
|
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationsService: CollectionService {
|
||||||
|
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.pagedRequest(NotificationsEndpoint.notifications, maxId: maxId, minId: minId)
|
||||||
|
.handleEvents(receiveOutput: {
|
||||||
|
guard let maxId = $0.info.maxId, maxId < nextPageMaxIdSubject.value else { return }
|
||||||
|
|
||||||
|
nextPageMaxIdSubject.send(maxId)
|
||||||
|
})
|
||||||
|
.flatMap { contentDatabase.insert(notifications: $0.result) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,9 @@ public extension AccountViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayName: String { accountService.account.displayName }
|
var displayName: String {
|
||||||
|
accountService.account.displayName.isEmpty ? accountService.account.acct : accountService.account.displayName
|
||||||
|
}
|
||||||
|
|
||||||
var accountName: String { "@".appending(accountService.account.acct) }
|
var accountName: String { "@".appending(accountService.account.acct) }
|
||||||
|
|
||||||
|
@ -36,6 +38,8 @@ public extension AccountViewModel {
|
||||||
|
|
||||||
var emoji: [Emoji] { accountService.account.emojis }
|
var emoji: [Emoji] { accountService.account.emojis }
|
||||||
|
|
||||||
|
var isSelf: Bool { accountService.account.id == identification.identity.account?.id }
|
||||||
|
|
||||||
func avatarURL(profile: Bool = false) -> URL {
|
func avatarURL(profile: Bool = false) -> URL {
|
||||||
if !identification.appPreferences.shouldReduceMotion,
|
if !identification.appPreferences.shouldReduceMotion,
|
||||||
(identification.appPreferences.animateAvatars == .everywhere
|
(identification.appPreferences.animateAvatars == .everywhere
|
||||||
|
|
|
@ -126,6 +126,18 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
.navigation(.profile(collectionService
|
.navigation(.profile(collectionService
|
||||||
.navigationService
|
.navigationService
|
||||||
.profileService(account: account))))
|
.profileService(account: account))))
|
||||||
|
case let .notification(notification, _):
|
||||||
|
if let status = notification.status {
|
||||||
|
eventsSubject.send(
|
||||||
|
.navigation(.collection(collectionService
|
||||||
|
.navigationService
|
||||||
|
.contextService(id: status.displayStatus.id))))
|
||||||
|
} else {
|
||||||
|
eventsSubject.send(
|
||||||
|
.navigation(.profile(collectionService
|
||||||
|
.navigationService
|
||||||
|
.profileService(account: notification.account))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,6 +207,27 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
|
|
||||||
cache(viewModel: viewModel, forItem: item)
|
cache(viewModel: viewModel, forItem: item)
|
||||||
|
|
||||||
|
return viewModel
|
||||||
|
case let .notification(notification, statusConfiguration):
|
||||||
|
let viewModel: CollectionItemViewModel
|
||||||
|
|
||||||
|
if let cachedViewModel = cachedViewModel {
|
||||||
|
viewModel = cachedViewModel
|
||||||
|
} else if let status = notification.status, let statusConfiguration = statusConfiguration {
|
||||||
|
let statusViewModel = StatusViewModel(
|
||||||
|
statusService: collectionService.navigationService.statusService(status: status),
|
||||||
|
identification: identification)
|
||||||
|
statusViewModel.configuration = statusConfiguration
|
||||||
|
viewModel = statusViewModel
|
||||||
|
cache(viewModel: viewModel, forItem: item)
|
||||||
|
} else {
|
||||||
|
viewModel = NotificationViewModel(
|
||||||
|
notificationService: collectionService.navigationService.notificationService(
|
||||||
|
notification: notification),
|
||||||
|
identification: identification)
|
||||||
|
cache(viewModel: viewModel, forItem: item)
|
||||||
|
}
|
||||||
|
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,23 @@ public final class NavigationViewModel: ObservableObject {
|
||||||
@Published public private(set) var timelinesAndLists: [Timeline]
|
@Published public private(set) var timelinesAndLists: [Timeline]
|
||||||
@Published public var presentingSecondaryNavigation = false
|
@Published public var presentingSecondaryNavigation = false
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public var selectedTab: Tab? = .timelines
|
|
||||||
public private(set) var timelineViewModel: CollectionItemsViewModel
|
public private(set) var timelineViewModel: CollectionItemsViewModel
|
||||||
|
|
||||||
|
public var notificationsViewModel: CollectionViewModel? {
|
||||||
|
if identification.identity.authenticated {
|
||||||
|
if _notificationsViewModel == nil {
|
||||||
|
_notificationsViewModel = CollectionItemsViewModel(
|
||||||
|
collectionService: identification.service.notificationsService(),
|
||||||
|
identification: identification)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _notificationsViewModel
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _notificationsViewModel: CollectionViewModel?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(identification: Identification) {
|
public init(identification: Identification) {
|
||||||
|
|
31
ViewModels/Sources/ViewModels/NotificationViewModel.swift
Normal file
31
ViewModels/Sources/ViewModels/NotificationViewModel.swift
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public final class NotificationViewModel: CollectionItemViewModel, ObservableObject {
|
||||||
|
public let accountViewModel: AccountViewModel
|
||||||
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
|
private let notificationService: NotificationService
|
||||||
|
private let identification: Identification
|
||||||
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
|
||||||
|
init(notificationService: NotificationService, identification: Identification) {
|
||||||
|
self.notificationService = notificationService
|
||||||
|
self.identification = identification
|
||||||
|
self.accountViewModel = AccountViewModel(
|
||||||
|
accountService: notificationService.navigationService.accountService(
|
||||||
|
account: notificationService.notification.account),
|
||||||
|
identification: identification)
|
||||||
|
self.events = eventsSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension NotificationViewModel {
|
||||||
|
var type: MastodonNotification.NotificationType {
|
||||||
|
notificationService.notification.type
|
||||||
|
}
|
||||||
|
}
|
18
Views/NotificationContentConfiguration.swift
Normal file
18
Views/NotificationContentConfiguration.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct NotificationContentConfiguration {
|
||||||
|
let viewModel: NotificationViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
NotificationView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> NotificationContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
26
Views/NotificationListCell.swift
Normal file
26
Views/NotificationListCell.swift
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
class NotificationListCell: UITableViewCell {
|
||||||
|
var viewModel: NotificationViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = NotificationContentConfiguration(viewModel: viewModel).updated(for: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
separatorInset.left = 0
|
||||||
|
separatorInset.right = 0
|
||||||
|
} else {
|
||||||
|
separatorInset.left = layoutMargins.left
|
||||||
|
separatorInset.right = layoutMargins.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
120
Views/NotificationView.swift
Normal file
120
Views/NotificationView.swift
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Mastodon
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class NotificationView: UIView {
|
||||||
|
private let iconImageView = UIImageView()
|
||||||
|
private let typeLabel = UILabel()
|
||||||
|
private var notificationConfiguration: NotificationContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: NotificationContentConfiguration) {
|
||||||
|
notificationConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyNotificationConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { notificationConfiguration }
|
||||||
|
set {
|
||||||
|
guard let notificationConfiguration = newValue as? NotificationContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.notificationConfiguration = notificationConfiguration
|
||||||
|
|
||||||
|
applyNotificationConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NotificationView {
|
||||||
|
func initialSetup() {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.spacing = .compactSpacing
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(iconImageView)
|
||||||
|
iconImageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(typeLabel)
|
||||||
|
typeLabel.font = .preferredFont(forTextStyle: .body)
|
||||||
|
typeLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
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)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyNotificationConfiguration() {
|
||||||
|
let viewModel = notificationConfiguration.viewModel
|
||||||
|
|
||||||
|
switch viewModel.type {
|
||||||
|
case .follow:
|
||||||
|
typeLabel.attributedText = "notifications.followed-you".localizedBolding(
|
||||||
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
|
emoji: viewModel.accountViewModel.emoji,
|
||||||
|
label: typeLabel)
|
||||||
|
iconImageView.tintColor = nil
|
||||||
|
case .reblog:
|
||||||
|
typeLabel.attributedText = "notifications.reblogged-your-status".localizedBolding(
|
||||||
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
|
emoji: viewModel.accountViewModel.emoji,
|
||||||
|
label: typeLabel)
|
||||||
|
iconImageView.tintColor = .systemGreen
|
||||||
|
case .favourite:
|
||||||
|
typeLabel.attributedText = "notifications.favourited-your-status".localizedBolding(
|
||||||
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
|
emoji: viewModel.accountViewModel.emoji,
|
||||||
|
label: typeLabel)
|
||||||
|
iconImageView.tintColor = .systemYellow
|
||||||
|
case .poll:
|
||||||
|
typeLabel.text = NSLocalizedString(
|
||||||
|
viewModel.accountViewModel.isSelf
|
||||||
|
? "notifications.your-poll-ended"
|
||||||
|
: "notifications.poll-ended",
|
||||||
|
comment: "")
|
||||||
|
iconImageView.tintColor = nil
|
||||||
|
default:
|
||||||
|
typeLabel.attributedText = "notifications.unknown".localizedBolding(
|
||||||
|
displayName: viewModel.accountViewModel.displayName,
|
||||||
|
emoji: viewModel.accountViewModel.emoji,
|
||||||
|
label: typeLabel)
|
||||||
|
iconImageView.tintColor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
iconImageView.image = UIImage(
|
||||||
|
systemName: viewModel.type.systemImageName,
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(scale: .medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonNotification.NotificationType {
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .follow, .followRequest:
|
||||||
|
return "person.badge.plus"
|
||||||
|
case .reblog:
|
||||||
|
return "arrow.2.squarepath"
|
||||||
|
case .favourite:
|
||||||
|
return "star.fill"
|
||||||
|
case .poll:
|
||||||
|
return "chart.bar.doc.horizontal"
|
||||||
|
case .mention, .unknown:
|
||||||
|
return "at"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,13 +8,14 @@ struct TabNavigationView: View {
|
||||||
@ObservedObject var viewModel: NavigationViewModel
|
@ObservedObject var viewModel: NavigationViewModel
|
||||||
@EnvironmentObject var rootViewModel: RootViewModel
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
@Environment(\.displayScale) var displayScale: CGFloat
|
@Environment(\.displayScale) var displayScale: CGFloat
|
||||||
|
@State var selectedTab = NavigationViewModel.Tab.timelines
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if viewModel.identification.identity.pending {
|
if viewModel.identification.identity.pending {
|
||||||
pendingView
|
pendingView
|
||||||
} else {
|
} else {
|
||||||
TabView(selection: $viewModel.selectedTab) {
|
TabView(selection: $selectedTab) {
|
||||||
ForEach(viewModel.tabs) { tab in
|
ForEach(viewModel.tabs) { tab in
|
||||||
NavigationView {
|
NavigationView {
|
||||||
view(tab: tab)
|
view(tab: tab)
|
||||||
|
@ -91,6 +92,15 @@ private extension TabNavigationView {
|
||||||
Image(systemName: viewModel.timeline.systemImageName)
|
Image(systemName: viewModel.timeline.systemImageName)
|
||||||
.padding([.leading, .top, .bottom])
|
.padding([.leading, .top, .bottom])
|
||||||
})
|
})
|
||||||
|
case .notifications:
|
||||||
|
if let notificationsViewModel = viewModel.notificationsViewModel {
|
||||||
|
TableView(viewModel: notificationsViewModel)
|
||||||
|
.id(tab)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.navigationTitle("notifications")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarItems(leading: secondaryNavigationButton)
|
||||||
|
}
|
||||||
default: Text(tab.title)
|
default: Text(tab.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue