[WIP] Add relationships/user to notifications (IOS-192)

This commit is contained in:
Nathan Mattes 2024-01-16 15:17:09 +01:00
parent 127c3167b8
commit 35c017986a
8 changed files with 89 additions and 132 deletions

View File

@ -31,6 +31,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
return
}
//TODO: Update Relationship
_ = try await DataSourceFacade.responseToMenuAction(
dependency: self,
action: action,

View File

@ -57,7 +57,7 @@ extension NotificationTableViewCell {
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
tableView.endUpdates()
}
}
.store(in: &disposeBag)

View File

@ -22,10 +22,12 @@ extension NotificationTimelineViewController: DataSourceProvider {
switch item {
case .feed(let feed):
let managedObjectContext = context.managedObjectContext
let item: DataSourceItem? = {
guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil }
if let notification = feed.notification, let mastodonNotification = MastodonNotification.fromEntity(notification, using: managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain) {
//TODO: Get relationship
if let notification = feed.notification,
let mastodonNotification = MastodonNotification.fromEntity(notification, relationship: nil, domain: authContext.mastodonAuthenticationBox.domain) {
return .notification(record: mastodonNotification)
} else {
return nil

View File

@ -53,18 +53,19 @@ final class NotificationTimelineViewModel {
self.authContext = authContext
self.scope = scope
self.dataController = FeedDataController(context: context, authContext: authContext)
switch scope {
case .everything:
//TODO: I need the relationship here, too
self.dataController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox))?.map({ notification in
MastodonFeed.fromNotification(notification, kind: .notificationAll)
MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationAll)
}) ?? []
case .mentions:
self.dataController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox))?.map({ notification in
MastodonFeed.fromNotification(notification, kind: .notificationMentions)
MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationMentions)
}) ?? []
}
self.dataController.$records
.removeDuplicates()
.receive(on: DispatchQueue.main)

View File

@ -19,17 +19,14 @@ import MastodonSDK
extension NotificationView {
public func configure(feed: MastodonFeed) {
guard
let notification = feed.notification,
let managedObjectContext = viewModel.context?.managedObjectContext
else {
guard let notification = feed.notification else {
assertionFailure()
return
}
MastodonNotification.fromEntity(
notification,
using: managedObjectContext,
relationship: feed.relationship,
domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? ""
).map(configure(notification:))
}
@ -65,56 +62,29 @@ extension NotificationView {
extension NotificationView {
private func configureAuthor(notification: MastodonNotification) {
let author = notification.account
// author avatar
Publishers.CombineLatest(
author.publisher(for: \.avatar),
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)
)
.map { _ in author.avatarImageURL() }
.assign(to: \.authorAvatarImageURL, on: viewModel)
.store(in: &disposeBag)
viewModel.authorAvatarImageURL = author.avatarImageURL()
// author name
Publishers.CombineLatest(
author.publisher(for: \.displayName),
author.publisher(for: \.emojis)
)
.map { _, emojis in
do {
let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis.asDictionary)
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure(error.localizedDescription)
return PlaintextMetaContent(string: author.displayNameWithFallback)
}
do {
let content = MastodonContent(content: author.displayNameWithFallback, emojis: author.emojis.asDictionary)
viewModel.authorName = try MastodonMetaContent.convert(document: content)
} catch {
assertionFailure(error.localizedDescription)
viewModel.authorName = PlaintextMetaContent(string: author.displayNameWithFallback)
}
.assign(to: \.authorName, on: viewModel)
.store(in: &disposeBag)
// author username
author.publisher(for: \.acct)
.map { $0 as String? }
.assign(to: \.authorUsername, on: viewModel)
.store(in: &disposeBag)
// timestamp
viewModel.authorUsername = author.acct
viewModel.timestamp = notification.entity.createdAt
viewModel.visibility = notification.entity.status?.mastodonVisibility ?? ._other("")
// notification type indicator
Publishers.CombineLatest(
author.publisher(for: \.displayName),
author.publisher(for: \.emojis)
)
.sink { [weak self] _, emojis in
guard let self = self else { return }
guard let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) else {
self.viewModel.notificationIndicatorText = nil
return
}
if let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) {
self.viewModel.type = type
// TODO: fix the i18n. The subject should assert place at the string beginning
func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent {
let content = MastodonContent(content: text, emojis: emojis)
guard let metaContent = try? MastodonMetaContent.convert(document: content) else {
@ -122,102 +92,65 @@ extension NotificationView {
}
return metaContent
}
// TODO: fix the i18n. The subject should assert place at the string beginning
switch type {
case .follow:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.followedYou,
emojis: emojis.asDictionary
emojis: author.emojis.asDictionary
)
case .followRequest:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou,
emojis: emojis.asDictionary
emojis: author.emojis.asDictionary
)
case .mention:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.mentionedYou,
emojis: emojis.asDictionary
emojis: author.emojis.asDictionary
)
case .reblog:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost,
emojis: emojis.asDictionary
emojis: author.emojis.asDictionary
)
case .favourite:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost,
emojis: emojis.asDictionary
emojis: author.emojis.asDictionary
)
case .poll:
self.viewModel.notificationIndicatorText = createMetaContent(
text: L10n.Scene.Notification.NotificationDescription.pollHasEnded,
emojis: emojis.asDictionary
emojis: author.emojis.asDictionary
)
case .status:
self.viewModel.notificationIndicatorText = createMetaContent(
text: .empty,
emojis: emojis.asDictionary
emojis: author.emojis.asDictionary
)
case ._other:
self.viewModel.notificationIndicatorText = nil
}
} else {
self.viewModel.notificationIndicatorText = nil
}
.store(in: &disposeBag)
let authContext = viewModel.authContext
// isMuting
author.publisher(for: \.mutingBy)
.map { mutingBy in
guard let authContext = authContext else { return false }
return mutingBy.contains(where: {
$0.id == authContext.mastodonAuthenticationBox.userID
&& $0.domain == authContext.mastodonAuthenticationBox.domain
})
if let me = viewModel.authContext?.mastodonAuthenticationBox.authentication.account() {
viewModel.isMyself = (author == me)
if let relationship = notification.relationship {
viewModel.isMuting = relationship.muting
viewModel.isBlocking = relationship.blocking || relationship.domainBlocking
viewModel.isFollowed = relationship.following
} else {
viewModel.isMuting = false
viewModel.isBlocking = false
viewModel.isFollowed = false
}
.assign(to: \.isMuting, on: viewModel)
.store(in: &disposeBag)
// isBlocking
author.publisher(for: \.blockingBy)
.map { blockingBy in
guard let authContext = authContext else { return false }
return blockingBy.contains(where: {
$0.id == authContext.mastodonAuthenticationBox.userID
&& $0.domain == authContext.mastodonAuthenticationBox.domain
})
}
.assign(to: \.isBlocking, on: viewModel)
.store(in: &disposeBag)
// isMyself
Publishers.CombineLatest(
author.publisher(for: \.domain),
author.publisher(for: \.id)
)
.map { domain, id in
guard let authContext = authContext else { return false }
return authContext.mastodonAuthenticationBox.domain == domain
&& authContext.mastodonAuthenticationBox.userID == id
}
.assign(to: \.isMyself, on: viewModel)
.store(in: &disposeBag)
// follow request state
viewModel.followRequestState = notification.followRequestState
viewModel.transientFollowRequestState = notification.transientFollowRequestState
// Following
author.publisher(for: \.followingBy)
.map { [weak viewModel] followingBy in
guard let viewModel = viewModel else { return false }
guard let authContext = viewModel.authContext else { return false }
return followingBy.contains(where: {
$0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain
})
}
.assign(to: \.isFollowed, on: viewModel)
.store(in: &disposeBag)
}
}

View File

@ -76,16 +76,36 @@ final public class FeedDataController {
private extension FeedDataController {
func load(kind: MastodonFeed.Kind, sinceId: MastodonStatus.ID?) async throws -> [MastodonFeed] {
switch kind {
case .home:
await context.authenticationService.authenticationServiceProvider.fetchAccounts(apiService: context.apiService)
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
case .notificationAll:
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationAll) }
case .notificationMentions:
return try await context.apiService.notifications(maxID: nil, scope: .mentions, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationMentions) }
case .home:
await context.authenticationService.authenticationServiceProvider.fetchAccounts(apiService: context.apiService)
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
case .notificationAll:
return try await getFeeds(with: .everything)
case .notificationMentions:
return try await getFeeds(with: .mentions)
}
}
private func getFeeds(with scope: APIService.MastodonNotificationScope) async throws -> [MastodonFeed] {
let notifications = try await context.apiService.notifications(maxID: nil, scope: scope, authenticationBox: authContext.mastodonAuthenticationBox).value
let accounts = notifications.map { $0.account }
let relationships = try await context.apiService.relationship(forAccounts: accounts, authenticationBox: authContext.mastodonAuthenticationBox).value
let notificationsWithRelationship: [(notification: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?)] = notifications.compactMap { notification in
guard let relationship = relationships.first(where: {$0.id == notification.account.id }) else { return (notification: notification, relationship: nil)}
return (notification: notification, relationship: relationship)
}
let feeds = notificationsWithRelationship.compactMap({ (notification: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?) in
MastodonFeed.fromNotification(notification, relationship: relationship, kind: .notificationAll)
})
return feeds
}
}

View File

@ -16,16 +16,18 @@ public final class MastodonFeed {
public var isLoadingMore: Bool = false
public let status: MastodonStatus?
public let relationship: Mastodon.Entity.Relationship?
public let notification: Mastodon.Entity.Notification?
public let kind: Feed.Kind
init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, kind: Feed.Kind) {
init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, relationship: Mastodon.Entity.Relationship?, kind: Feed.Kind) {
self.id = notification?.id ?? status?.id ?? UUID().uuidString
self.hasMore = hasMore
self.isLoadingMore = isLoadingMore
self.status = status
self.notification = notification
self.relationship = relationship
self.kind = kind
}
}
@ -37,11 +39,12 @@ public extension MastodonFeed {
isLoadingMore: false,
status: status,
notification: nil,
relationship: nil,
kind: kind
)
}
static func fromNotification(_ notification: Mastodon.Entity.Notification, kind: Feed.Kind) -> MastodonFeed {
static func fromNotification(_ notification: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?, kind: Feed.Kind) -> MastodonFeed {
MastodonFeed(
hasMore: false,
isLoadingMore: false,
@ -52,6 +55,7 @@ public extension MastodonFeed {
return .fromEntity(status)
}(),
notification: notification,
relationship: relationship,
kind: kind
)
}

View File

@ -10,30 +10,26 @@ public final class MastodonNotification {
entity.id
}
public let account: MastodonUser
public let account: Mastodon.Entity.Account
public let relationship: Mastodon.Entity.Relationship?
public let status: MastodonStatus?
public let feeds: [MastodonFeed]
public var followRequestState: MastodonFollowRequestState = .init(state: .none)
public var transientFollowRequestState: MastodonFollowRequestState = .init(state: .none)
public init(entity: Mastodon.Entity.Notification, account: MastodonUser, status: MastodonStatus?, feeds: [MastodonFeed]) {
public init(entity: Mastodon.Entity.Notification, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, status: MastodonStatus?, feeds: [MastodonFeed]) {
self.entity = entity
self.account = account
self.relationship = relationship
self.status = status
self.feeds = feeds
}
}
public extension MastodonNotification {
static func fromEntity(_ entity: Mastodon.Entity.Notification, using managedObjectContext: NSManagedObjectContext, domain: String) -> MastodonNotification? {
guard let user = MastodonUser.fetch(in: managedObjectContext, configurationBlock: { request in
request.predicate = MastodonUser.predicate(domain: domain, id: entity.account.id)
}).first else {
assertionFailure()
return nil
}
return MastodonNotification(entity: entity, account: user, status: entity.status.map(MastodonStatus.fromEntity), feeds: [])
static func fromEntity(_ entity: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?, domain: String) -> MastodonNotification? {
return MastodonNotification(entity: entity, account: entity.account, relationship: relationship, status: entity.status.map(MastodonStatus.fromEntity), feeds: [])
}
}