Skip to content

Commit

Permalink
fetch events and nosNotifications on notificationView
Browse files Browse the repository at this point in the history
  • Loading branch information
pelumy committed Jan 17, 2025
1 parent 2fa4555 commit afc211d
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 335 deletions.
6 changes: 6 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@
045EDCF32CAAF47600B67964 /* FlagSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045EDCF22CAAF47600B67964 /* FlagSuccessView.swift */; };
045EDD052CAC025700B67964 /* ScrollViewProxy+Animate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045EDD042CAC025700B67964 /* ScrollViewProxy+Animate.swift */; };
0496D6312C975E6900D29375 /* FlagOptionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0496D6302C975E6900D29375 /* FlagOptionPicker.swift */; };
04C923BD2D3A8BFC0088A97B /* NotificationDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C923BC2D3A8BFC0088A97B /* NotificationDisplayable.swift */; };
04C923BE2D3A8BFC0088A97B /* NotificationDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C923BC2D3A8BFC0088A97B /* NotificationDisplayable.swift */; };
04C9D7272CBF09C200EAAD4D /* TextField+PlaceHolderStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C9D7262CBF09C200EAAD4D /* TextField+PlaceHolderStyle.swift */; };
04C9D7912CC29D5000EAAD4D /* FeaturedAuthor+Cohort1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C9D7902CC29D5000EAAD4D /* FeaturedAuthor+Cohort1.swift */; };
04C9D7932CC29D8300EAAD4D /* FeaturedAuthor+Cohort2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C9D7922CC29D8300EAAD4D /* FeaturedAuthor+Cohort2.swift */; };
Expand Down Expand Up @@ -741,6 +743,7 @@
045EDCF22CAAF47600B67964 /* FlagSuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagSuccessView.swift; sourceTree = "<group>"; };
045EDD042CAC025700B67964 /* ScrollViewProxy+Animate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollViewProxy+Animate.swift"; sourceTree = "<group>"; };
0496D6302C975E6900D29375 /* FlagOptionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagOptionPicker.swift; sourceTree = "<group>"; };
04C923BC2D3A8BFC0088A97B /* NotificationDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDisplayable.swift; sourceTree = "<group>"; };
04C9D7262CBF09C200EAAD4D /* TextField+PlaceHolderStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextField+PlaceHolderStyle.swift"; sourceTree = "<group>"; };
04C9D7902CC29D5000EAAD4D /* FeaturedAuthor+Cohort1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeaturedAuthor+Cohort1.swift"; sourceTree = "<group>"; };
04C9D7922CC29D8300EAAD4D /* FeaturedAuthor+Cohort2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeaturedAuthor+Cohort2.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2004,6 +2007,7 @@
C92E7F652C4EFF2600B80638 /* WebSockets */,
5BCA95D12C8A5F0D00A52D1A /* PreviewEventRepository.swift */,
04368D2A2C99A2C400DEAA2E /* FlagOption.swift */,
04C923BC2D3A8BFC0088A97B /* NotificationDisplayable.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -2701,6 +2705,7 @@
0304D0B22C9B731F001D16C7 /* MockOpenGraphService.swift in Sources */,
030E570D2CC2A05B00A4A51E /* DisplayNameView.swift in Sources */,
C94437E629B0DB83004D8C86 /* NotificationsView.swift in Sources */,
04C923BD2D3A8BFC0088A97B /* NotificationDisplayable.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -2856,6 +2861,7 @@
035729B92BE416A6005FEE85 /* GiftWrapperTests.swift in Sources */,
50E2EB7B2C8617C800D4B360 /* NSRegularExpression+Replacement.swift in Sources */,
C9246C1C2C8A42A0005495CE /* RelaySubscriptionManagerTests.swift in Sources */,
04C923BE2D3A8BFC0088A97B /* NotificationDisplayable.swift in Sources */,
032634702C10C40B00E489B5 /* NostrBuildAPIClientTests.swift in Sources */,
0315B5F02C7E451C0020E707 /* MockMediaService.swift in Sources */,
C9646EAA29B7A506007239A4 /* Analytics.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions Nos/Models/CoreData/Event+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -607,4 +607,12 @@ public class Event: NosManagedObject, VerifiableEvent {
shouldBePublishedTo = Set()
}
}

extension Event: NotificationDisplayable {
/// Returns self since an Event is its own associated event.
var event: Event? {
self
}
}

// swiftlint:enable file_length
88 changes: 87 additions & 1 deletion Nos/Models/CoreData/Event+Fetching.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// swiftlint:disable file_length
import CoreData

extension Event {
Expand Down Expand Up @@ -96,7 +97,90 @@ extension Event {

return fetchRequest
}


/// A request for all out-Of-Network events that the given user should receive.
/// - Parameters:
/// - currentUser: the author you want to view notifications for.
/// - limit: a max number of events to fetch.
/// - Returns: A fetch request for outOfNetwork events.
@nonobjc public class func outOfNetworkRequest(
for currentUser: Author,
limit: Int? = nil
) -> NSFetchRequest<Event> {
let fetchRequest = NSFetchRequest<Event>(entityName: String(describing: Event.self))
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.createdAt, ascending: false)]
if let limit {
fetchRequest.fetchLimit = limit
}

let mentionsPredicate = allMentionsPredicate(for: currentUser)
let repliesPredicate = allRepliesPredicate(for: currentUser)
let zapsPredicate = allZapsPredicate(for: currentUser)

let notificationsPredicate = NSCompoundPredicate(
orPredicateWithSubpredicates: [mentionsPredicate, repliesPredicate, zapsPredicate]
)

// Out of network: has no followers OR not in follows network
let outOfNetworkPredicate = NSPredicate(
format: "author.followers.@count == 0 OR " +
"NOT (ANY author.followers.source IN %@.follows.destination " +
"OR author IN %@.follows.destination)",
currentUser,
currentUser
)

fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
notificationsPredicate,
outOfNetworkPredicate,
NSPredicate(format: "author != %@", currentUser),
NSPredicate(format: "author.muted = false")
])

return fetchRequest
}

/// A request for all in-Network events that the given user should receive.
/// - Parameters:
/// - currentUser: the author you want to view notifications for.
/// - limit: a max number of events to fetch.
/// - Returns: A fetch request for inNetwork events.
@nonobjc public class func inNetworkRequest(
for currentUser: Author,
limit: Int? = nil
) -> NSFetchRequest<Event> {
let fetchRequest = NSFetchRequest<Event>(entityName: String(describing: Event.self))
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.createdAt, ascending: false)]
if let limit {
fetchRequest.fetchLimit = limit
}

let mentionsPredicate = allMentionsPredicate(for: currentUser)
let repliesPredicate = allRepliesPredicate(for: currentUser)
let zapsPredicate = allZapsPredicate(for: currentUser)

let notificationsPredicate = NSCompoundPredicate(
orPredicateWithSubpredicates: [mentionsPredicate, repliesPredicate, zapsPredicate]
)

// In network: in follows network
let inNetworkPredicate = NSPredicate(
format: "(ANY author.followers.source IN %@.follows.destination " +
"OR author IN %@.follows.destination)",
currentUser,
currentUser
)

fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
notificationsPredicate,
inNetworkPredicate,
NSPredicate(format: "author != %@", currentUser),
NSPredicate(format: "author.muted = false")
])

return fetchRequest
}

@nonobjc public class func lastReceived(for user: Author) -> NSFetchRequest<Event> {
let fetchRequest = NSFetchRequest<Event>(entityName: "Event")
fetchRequest.predicate = NSPredicate(format: "author != %@", user)
Expand Down Expand Up @@ -462,3 +546,5 @@ extension Event {
return request
}
}

// swiftlint:enable file_length
58 changes: 7 additions & 51 deletions Nos/Models/CoreData/NosNotification+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,57 +109,6 @@ public class NosNotification: NosManagedObject {
return fetchRequest
}

/// A request for all out-Of-Network notifications that the given user should receive.
/// - Parameters:
/// - currentUser: the author you want to view notifications for.
/// - limit: a max number of notifications to fetch.
/// - Returns: A fetch request for outOfNetwork notifications.
@nonobjc public class func outOfNetworkRequest(
for currentUser: Author,
limit: Int? = nil
) -> NSFetchRequest<NosNotification> {
let fetchRequest = NSFetchRequest<NosNotification>(entityName: String(describing: NosNotification.self))
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NosNotification.createdAt, ascending: false)]
if let limit {
fetchRequest.fetchLimit = limit
}

fetchRequest.predicate = NSPredicate(
format: "follower == nil " +
"AND (event.author.followers.@count == 0 " +
"OR NOT (ANY event.author.followers.source IN %@.follows.destination " +
"OR event.author IN %@.follows.destination))",
currentUser,
currentUser
)
return fetchRequest
}
/// A request for all in-Network notifications that the given user should receive.
/// - Parameters:
/// - currentUser: the author you want to view notifications for.
/// - limit: a max number of notifications to fetch.
/// - Returns: A fetch request for inNetwork notifications.
@nonobjc public class func inNetworkRequest(
for currentUser: Author,
limit: Int? = nil
) -> NSFetchRequest<NosNotification> {
let fetchRequest = NSFetchRequest<NosNotification>(entityName: String(describing: NosNotification.self))
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NosNotification.createdAt, ascending: false)]

if let limit {
fetchRequest.fetchLimit = limit
}

fetchRequest.predicate = NSPredicate(
format: "follower == nil " +
"AND (ANY event.author.followers.source IN %@.follows.destination) " +
"OR event.author IN %@.follows.destination AND follower == nil",
currentUser,
currentUser
)
return fetchRequest
}

/// A request for all follow notifications that the given user should receive.
/// - Parameters:
/// - currentUser: the author you want to view notifications for.
Expand All @@ -180,3 +129,10 @@ public class NosNotification: NosManagedObject {
return fetchRequest
}
}

extension NosNotification: NotificationDisplayable {
/// Returns the follower as the author since they generated the follow notification.
var author: Author? {
follower
}
}
19 changes: 19 additions & 0 deletions Nos/Models/NotificationDisplayable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import CoreData

/// A protocol that defines the common interface for displaying notifications in the app.
/// Both `Event` and `NosNotification` types conform to this protocol to enable unified
/// handling in notification views.
///
/// Conforming types must be `NSManagedObject`s and `Identifiable` to support CoreData
/// persistence and unique identification in SwiftUI lists.
protocol NotificationDisplayable: NSManagedObject, Identifiable {
var createdAt: Date? { get }

/// The associated event, if any. For `Event` types, this is the event itself.
/// For `NosNotification` types, this is the associated event if one exists.
var event: Event? { get }

/// The author associated with this notification. For `Event` types, this is the event author.
/// For `NosNotification` types, this is the follower who generated the notification.
var author: Author? { get }
}
11 changes: 10 additions & 1 deletion Nos/Models/NotificationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ class NotificationViewModel: ObservableObject, Identifiable {
actionText = authorName + AttributedString(String(localized: "startedFollowingYou"))
range = Range(uncheckedBounds: (actionText.startIndex, actionText.endIndex))
actionText[range].foregroundColor = .primaryTxt
self.actionText = actionText

/// For notification content, truncate the text only if more than
/// specified maximum length.
let maxLength = 100
if actionText.characters.count > maxLength {
let truncated = String(actionText.characters.prefix(maxLength)) + "..."
self.actionText = AttributedString(truncated)
} else {
self.actionText = actionText
}

self.content = nil
}
Expand Down
40 changes: 20 additions & 20 deletions Nos/Views/Notifications/NotificationsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ struct NotificationsView: View {
@Dependency(\.pushNotificationService) private var pushNotificationService
@Dependency(\.persistenceController) private var persistenceController

@FetchRequest private var outOfNetworkNotifications: FetchedResults<NosNotification>
@FetchRequest private var inNetworkNotifications: FetchedResults<NosNotification>
@FetchRequest private var outOfNetworkEvents: FetchedResults<Event>
@FetchRequest private var inNetworkEvents: FetchedResults<Event>
@FetchRequest private var followNotifications: FetchedResults<NosNotification>

@State private var relaySubscriptions = SubscriptionCancellables()
Expand All @@ -29,8 +29,8 @@ struct NotificationsView: View {
let networkRequests = Self.createNetworkFetchRequests(for: user, limit: maxNotificationsToShow)

_followNotifications = FetchRequest(fetchRequest: followsRequest)
_outOfNetworkNotifications = FetchRequest(fetchRequest: networkRequests.outOfNetwork)
_inNetworkNotifications = FetchRequest(fetchRequest: networkRequests.inNetwork)
_outOfNetworkEvents = FetchRequest(fetchRequest: networkRequests.outOfNetwork)
_inNetworkEvents = FetchRequest(fetchRequest: networkRequests.inNetwork)
}

/// Creates the follows notification fetch requests for all notifications and follows.
Expand All @@ -51,26 +51,26 @@ struct NotificationsView: View {
}
}

/// Creates the network-specific notification fetch requests.
/// Creates the network-specific events fetch requests.
///
/// This is implemented as a static function because it's used during initialization
/// and doesn't require access to instance properties.
///
/// - Parameters:
/// - user: The user to fetch notifications for. If nil, returns empty requests.
/// - limit: The maximum number of notifications to fetch.
/// - Returns: A tuple containing fetch requests for in-network and out-of-network notifications.
/// - user: The user to fetch events for. If nil, returns empty requests.
/// - limit: The maximum number of events to fetch.
/// - Returns: A tuple containing fetch requests for in-network and out-of-network events.
private static func createNetworkFetchRequests(for user: Author?, limit: Int) -> (
outOfNetwork: NSFetchRequest<NosNotification>,
inNetwork: NSFetchRequest<NosNotification>
outOfNetwork: NSFetchRequest<Event>,
inNetwork: NSFetchRequest<Event>
) {
if let user {
return (
outOfNetwork: NosNotification.outOfNetworkRequest(for: user, limit: limit),
inNetwork: NosNotification.inNetworkRequest(for: user, limit: limit)
outOfNetwork: Event.outOfNetworkRequest(for: user, limit: limit),
inNetwork: Event.inNetworkRequest(for: user, limit: limit)
)
} else {
let emptyRequest = NosNotification.emptyRequest()
let emptyRequest = Event.emptyRequest()
return (outOfNetwork: emptyRequest, inNetwork: emptyRequest)
}
}
Expand Down Expand Up @@ -194,15 +194,15 @@ struct NotificationsView: View {
.tag(0)

NotificationTabView(
notifications: inNetworkNotifications,
notifications: inNetworkEvents,
currentUser: currentUser,
maxNotificationsToShow: maxNotificationsToShow,
tag: 1
)
.tag(1)

NotificationTabView(
notifications: outOfNetworkNotifications,
notifications: outOfNetworkEvents,
currentUser: currentUser,
maxNotificationsToShow: maxNotificationsToShow,
tag: 2
Expand All @@ -215,9 +215,9 @@ struct NotificationsView: View {
}

/// A single notification cell that contains a follow event or a other event types in the notifications list
private struct NotificationCell: View {
private struct NotificationCell<T: NotificationDisplayable>: View {
@Dependency(\.persistenceController) private var persistenceController
let notification: NosNotification
let notification: T
let user: Author

var body: some View {
Expand All @@ -234,7 +234,7 @@ private struct NotificationCell: View {
)
)
.id(event.id)
} else if let followerKey = notification.follower?.hexadecimalPublicKey, let follower = try? Author.find(
} else if let followerKey = notification.author?.hexadecimalPublicKey, let follower = try? Author.find(
by: followerKey,
context: persistenceController.viewContext
) {
Expand Down Expand Up @@ -271,8 +271,8 @@ private struct TabButton: View {

/// A scrollable view that displays a list of notifications for a specific category
/// (follows, in-network, or out-of-network).
private struct NotificationTabView: View {
let notifications: FetchedResults<NosNotification>
private struct NotificationTabView<T: NotificationDisplayable>: View {
let notifications: FetchedResults<T>
let currentUser: CurrentUser
let maxNotificationsToShow: Int
let tag: Int
Expand Down
Loading

0 comments on commit afc211d

Please sign in to comment.