Skip to content

Commit

Permalink
Merge pull request #1727 from planetary-social/it/127-follow-notifica…
Browse files Browse the repository at this point in the history
…tion

show follow notification in tab
  • Loading branch information
pelumy authored Jan 31, 2025
2 parents 587ac92 + 7180481 commit 5d0ca91
Show file tree
Hide file tree
Showing 22 changed files with 1,380 additions and 219 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved naming of a couple list-related classes.
- Track TestFlight vs AppStore installations in Posthog. [#130](https://github.com/verse-pbc/issues/issues/130)
- Track breadcrumbs in Sentry for all analytics events. [#125](https://github.com/verse-pbc/issues/issues/125)
- Added functionality to get follows notifications in the Notifications tab. [#127](https://github.com/verse-pbc/issues/issues/127)
- Refactored the way the ProfileView downloads data and logs analytics events. [#1748](https://github.com/planetary-social/nos/pull/1748)

## [1.1] - 2025-01-03Z
Expand Down
12 changes: 11 additions & 1 deletion Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@
04368D2B2C99A2C400DEAA2E /* FlagOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04368D2A2C99A2C400DEAA2E /* FlagOption.swift */; };
04368D312C99A78800DEAA2E /* NosRadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04368D302C99A78800DEAA2E /* NosRadioButton.swift */; };
04368D4B2C99CFC700DEAA2E /* ContentFlagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04368D4A2C99CFC700DEAA2E /* ContentFlagView.swift */; };
045027492D318E1300DA9835 /* NosNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045027482D318E1300DA9835 /* NosNotificationTests.swift */; };
045028002D35484400DA9835 /* FollowsNotificationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045027FF2D35484400DA9835 /* FollowsNotificationCard.swift */; };
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 */; };
Expand Down Expand Up @@ -741,6 +743,9 @@
04368D2A2C99A2C400DEAA2E /* FlagOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagOption.swift; sourceTree = "<group>"; };
04368D302C99A78800DEAA2E /* NosRadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NosRadioButton.swift; sourceTree = "<group>"; };
04368D4A2C99CFC700DEAA2E /* ContentFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFlagView.swift; sourceTree = "<group>"; };
045027482D318E1300DA9835 /* NosNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NosNotificationTests.swift; sourceTree = "<group>"; };
045027622D31A08D00DA9835 /* Nos 24.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Nos 24.xcdatamodel"; sourceTree = "<group>"; };
045027FF2D35484400DA9835 /* FollowsNotificationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsNotificationCard.swift; sourceTree = "<group>"; };
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>"; };
Expand Down Expand Up @@ -1387,6 +1392,7 @@
children = (
C94437E529B0DB83004D8C86 /* NotificationsView.swift */,
C98B8B3F29FBF83B009789C8 /* NotificationCard.swift */,
045027FF2D35484400DA9835 /* FollowsNotificationCard.swift */,
);
path = Notifications;
sourceTree = "<group>";
Expand Down Expand Up @@ -1452,6 +1458,7 @@
035729A12BE4167E005FEE85 /* AuthorTests.swift */,
035729A32BE4167E005FEE85 /* EventTests.swift */,
035729A42BE4167E005FEE85 /* FollowTests.swift */,
045027482D318E1300DA9835 /* NosNotificationTests.swift */,
);
path = CoreData;
sourceTree = "<group>";
Expand Down Expand Up @@ -2645,6 +2652,7 @@
C9CDBBA429A8FA2900C555C7 /* GoldenPostView.swift in Sources */,
C92F01582AC4D6F700972489 /* NosTextField.swift in Sources */,
C9C2B77C29E072E400548B4A /* WebSocket+Nos.swift in Sources */,
045028002D35484400DA9835 /* FollowsNotificationCard.swift in Sources */,
503CA9532D19ACCC00805EF8 /* HorizontalLine.swift in Sources */,
C9DEC003298945150078B43A /* String+Lorem.swift in Sources */,
04C9D7912CC29D5000EAAD4D /* FeaturedAuthor+Cohort1.swift in Sources */,
Expand Down Expand Up @@ -2811,6 +2819,7 @@
035729AC2BE4167E005FEE85 /* Bech32Tests.swift in Sources */,
C936B4632A4CB01C00DF1EB9 /* PushNotificationService.swift in Sources */,
C9C5475A2A4F1D8C006B0741 /* NosNotification+CoreDataClass.swift in Sources */,
045027492D318E1300DA9835 /* NosNotificationTests.swift in Sources */,
508133DC2C7A007700DFBF75 /* AttributedString+Quotation.swift in Sources */,
CD09A74929A521210063464F /* Router.swift in Sources */,
C90B16B82AFED96300CB4B85 /* URLExtensionTests.swift in Sources */,
Expand Down Expand Up @@ -4006,6 +4015,7 @@
C936B4572A4C7B7C00DF1EB9 /* Nos.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
045027622D31A08D00DA9835 /* Nos 24.xcdatamodel */,
5022F9472D2188650012FF4B /* Nos 23.xcdatamodel */,
503CAB7B2D1DA6DB00805EF8 /* Nos 22.xcdatamodel */,
0303B11E2D0257D400077929 /* Nos 21.xcdatamodel */,
Expand All @@ -4022,7 +4032,7 @@
C9C547562A4F1D1A006B0741 /* Nos 9.xcdatamodel */,
5BFF66AF2A4B55FC00AA79DD /* Nos 10.xcdatamodel */,
);
currentVersion = 5022F9472D2188650012FF4B /* Nos 23.xcdatamodel */;
currentVersion = 045027622D31A08D00DA9835 /* Nos 24.xcdatamodel */;
path = Nos.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
3 changes: 3 additions & 0 deletions Nos/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Dependencies
import SDWebImage
import SDWebImageWebPCoder

@MainActor
class AppDelegate: NSObject, UIApplicationDelegate {

@Dependency(\.currentUser) private var currentUser
Expand Down Expand Up @@ -45,6 +46,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
do {
Log.info("PushNotifications: Received background notification. Subscribing to relays.")
analytics.receivedNotification()
// Call PushNotificationService to handle follow notifications
await pushNotificationService.application(application, didReceiveRemoteNotification: userInfo)
await currentUser.subscribe()
try await Task.sleep(for: .seconds(10))
Log.info("PushNotifications: Sync complete")
Expand Down
17 changes: 17 additions & 0 deletions Nos/Controller/PersistenceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class PersistenceController {
/// Increment this to delete core data on update
private static let version = 3
private static let versionKey = "NosPersistenceControllerVersion"
private static let createdNosNotifications = "CreatedNosNotifications"

static var preview: PersistenceController = {
let controller = PersistenceController(inMemory: true)
Expand Down Expand Up @@ -41,13 +42,29 @@ final class PersistenceController {
private(set) var container: NSPersistentContainer
private let model: NSManagedObjectModel
private let inMemory: Bool
private var recreateNosNotifications: Bool {
get {
UserDefaults.standard.bool(forKey: Self.createdNosNotifications)
}
set {
UserDefaults.standard.set(newValue, forKey: Self.createdNosNotifications)
}
}

init(containerName: String = "Nos", inMemory: Bool = false, erase: Bool = false) {
self.inMemory = inMemory
let modelURL = Bundle.current.url(forResource: "Nos", withExtension: "momd")!
model = NSManagedObjectModel(contentsOf: modelURL)!
container = NSPersistentContainer(name: containerName, managedObjectModel: model)
setUp(erasingPrevious: erase)

if !recreateNosNotifications {
Task {
let context = newBackgroundContext()
try await DatabaseCleaner.deleteNotificationsAndEvents(in: context)
recreateNosNotifications = true
}
}
}

private func setUp(erasingPrevious: Bool) {
Expand Down
9 changes: 9 additions & 0 deletions Nos/Models/CoreData/Author+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,13 @@ import Logger
// Publish the modified list
await currentUser.publishMuteList(keys: Array(Set(mutedList)))
}

// Checks if this author has received a follow notification from the specified author.
/// - Parameter author: The author to check for a follow relationship
/// - Returns: `true` if the specified author follows this author, `false` otherwise
func hasReceivedFollowNotification( from author: Author) -> Bool {
followNotifications.contains(where: { element in
(element as? NosNotification)?.follower == author
})
}
}
2 changes: 2 additions & 0 deletions Nos/Models/CoreData/Event+Fetching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ extension Event {
let readStoryClause = "(isRead = 1 AND receivedAt > %@)"
let userReportClause = "(kind == \(EventKind.report.rawValue) AND " +
"authorReferences.@count > 0 AND eventReferences.@count == 0)"
let notificationClause = "(notifications.@count = 0)"
let clauses = "\(oldUnreferencedEventsClause) AND" +
"\(notificationClause) AND " +
"\(notOwnEventClause) AND " +
"NOT \(readStoryClause) AND " +
"NOT \(userReportClause)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extension Event {
@NSManaged public var seenOnRelays: Set<Relay>
@NSManaged public var shouldBePublishedTo: Set<Relay>
@NSManaged public var isRead: Bool
@NSManaged public var notifications: NosNotification?
}

// MARK: Generated accessors for authorReferences
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ extension NosNotification {
}

@NSManaged public var isRead: Bool
@NSManaged public var eventID: String?
@NSManaged public var user: Author?
@NSManaged public var follower: Author?
@NSManaged public var createdAt: Date?
@NSManaged public var event: Event?
}

extension NosNotification: Identifiable {}
2 changes: 1 addition & 1 deletion Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Nos 23.xcdatamodel</string>
<string>Nos 24.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Nos.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Author" representedClassName=".Author" syncable="YES" codeGenerationType="category">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="hexadecimalPublicKey" attributeType="String"/>
<attribute name="lastUpdatedContactList" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastUpdatedMetadata" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="nip05" optional="YES" attributeType="String"/>
<attribute name="profilePhotoURL" optional="YES" attributeType="URI"/>
<attribute name="rawMetadata" optional="YES" attributeType="Binary"/>
<relationship name="events" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Event" inverseName="author" inverseEntity="Event"/>
<relationship name="followers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Follow" inverseName="destination" inverseEntity="Follow"/>
<relationship name="follows" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Follow" inverseName="source" inverseEntity="Follow"/>
<relationship name="relays" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
</entity>
<entity name="AuthorReference" representedClassName="AuthorReference" syncable="YES" codeGenerationType="category">
<attribute name="pubkey" optional="YES" attributeType="String"/>
<attribute name="recommendedRelayUrl" optional="YES" attributeType="String"/>
<relationship name="event" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="authorReferences" inverseEntity="Event"/>
</entity>
<entity name="Event" representedClassName=".Event" syncable="YES" codeGenerationType="category">
<attribute name="allTags" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName"/>
<attribute name="content" optional="YES" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="kind" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sendAttempts" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="signature" optional="YES" attributeType="String"/>
<relationship name="author" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="events" inverseEntity="Author"/>
<relationship name="authorReferences" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="AuthorReference" inverseName="event" inverseEntity="AuthorReference"/>
<relationship name="deletedOn" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
<relationship name="eventReferences" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="EventReference" inverseName="referencingEvent" inverseEntity="EventReference"/>
<relationship name="publishedTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
<relationship name="referencingEvents" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="EventReference" inverseName="referencedEvent" inverseEntity="EventReference"/>
</entity>
<entity name="EventReference" representedClassName="EventReference" syncable="YES" codeGenerationType="category">
<attribute name="eventId" optional="YES" attributeType="String"/>
<attribute name="marker" optional="YES" attributeType="String"/>
<attribute name="recommendedRelayUrl" optional="YES" attributeType="String"/>
<relationship name="referencedEvent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="referencingEvents" inverseEntity="Event"/>
<relationship name="referencingEvent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="eventReferences" inverseEntity="Event"/>
</entity>
<entity name="Follow" representedClassName=".Follow" syncable="YES" codeGenerationType="category">
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="petName" optional="YES" attributeType="String"/>
<relationship name="destination" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="followers" inverseEntity="Author"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="follows" inverseEntity="Author"/>
</entity>
<entity name="Relay" representedClassName=".Relay" syncable="YES" codeGenerationType="category">
<attribute name="address" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
</model>
Loading

0 comments on commit 5d0ca91

Please sign in to comment.