Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI/notifications #10

Merged
merged 48 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e462cf8
Update build-and-test.yml
nriedman Apr 12, 2024
61ced93
Basic Bluetooth infrastructure
nriedman Apr 15, 2024
0df9088
WeightScaleService Characteristics
nriedman Apr 17, 2024
c4dd341
Create FindDevices view
nriedman Apr 17, 2024
658058f
Update Package.resolved
nriedman Apr 22, 2024
ea6390c
Auto Connect Weight Scale
nriedman May 2, 2024
eb11635
Undo temporary entitlement changes
nriedman May 2, 2024
56c673d
Finish LoadMeasurement
nriedman May 3, 2024
7e0f4d5
Save and display recorded weight measurements
nriedman May 12, 2024
cc41782
Delete ENGAGEHFDebug.entitlements
nriedman May 12, 2024
3f7f107
Deleted BPCuffDevice.swift
nriedman May 12, 2024
6689d4e
Center title in measurement recorded veiw
nriedman May 13, 2024
2625528
Create basic notification UI elements
nriedman May 13, 2024
b0946a5
Basic UI Infrastructure
nriedman May 14, 2024
5ff379b
Update Package.resolved
nriedman May 20, 2024
83dfee1
Update ENGAGEHFDelegate.swift
nriedman May 20, 2024
3dc03c2
Implement NotificationManager
nriedman May 21, 2024
19f23d5
Change emulator host to localhost
nriedman May 21, 2024
ab13039
Remove SpeziFHIR as a dependency
nriedman May 22, 2024
098ce67
Update Package.resolved
nriedman May 22, 2024
6d5a422
Merge branch 'main' into UI/notifications
nriedman May 22, 2024
8584fff
Smooth quirks from merge w main
nriedman May 22, 2024
8bb5d71
Update Package.resolved
nriedman May 22, 2024
a62e8fe
Finish UI for notitifications
nriedman May 31, 2024
189c9f9
Credit StudyApplicationListCard
nriedman May 31, 2024
007e019
linting
nriedman May 31, 2024
db368d8
Merge branch 'main' into UI/notifications
PSchmiedmayer May 31, 2024
22fb6d1
Addressing PR comments
nriedman May 31, 2024
6faeb9c
Merge branch 'UI/notifications' of https://github.com/StanfordBDHG/EN…
nriedman May 31, 2024
1a8e9a1
Dev code is ENGAGETEST1 not ENGAGEHFTEST1
nriedman May 31, 2024
a64a7ab
Improve notification card UI
nriedman May 31, 2024
a04ab1e
Further update the notification UI Cards
nriedman May 31, 2024
797fac3
Change notification indexing to id-based, testing
nriedman Jun 11, 2024
e045231
Merge branch 'main' into UI/notifications
nriedman Jun 11, 2024
d9fb0bd
cleanup after merging main
nriedman Jun 11, 2024
1f657e2
more post merge cleanup
nriedman Jun 11, 2024
0080342
UI Testing
nriedman Jun 11, 2024
ae10660
undo development team changes
nriedman Jun 11, 2024
9a71223
Add created and completed to Notification class
nriedman Jun 11, 2024
fe1e2ed
Add "completed" and "created" fields to Notifs
nriedman Jun 11, 2024
9386f95
UI Test debugging
nriedman Jun 13, 2024
b9077ec
UI Tests done
nriedman Jun 13, 2024
7cac1eb
Remove To-dos
nriedman Jun 13, 2024
933f8b0
Remove To-dos
nriedman Jun 13, 2024
83d9fd9
remove non-functional elements
nriedman Jun 13, 2024
3bf9e00
add PI contact to education page
nriedman Jun 13, 2024
671d61b
Merge branch 'main' into UI/notifications
PSchmiedmayer Jun 14, 2024
cbcae79
Fix minor issues
PSchmiedmayer Jun 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 90 additions & 26 deletions ENGAGEHF.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
{
"identity" : "fhirmodels",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/FHIRModels",
"location" : "https://github.com/apple/FHIRModels.git",
"state" : {
"revision" : "861afd5816a98d38f86220eab2f812d76cad84a0",
"version" : "0.5.0"
Expand Down
8 changes: 4 additions & 4 deletions ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1520"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down Expand Up @@ -91,10 +91,10 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--testMockDevices"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--showOnboarding"
argument = "--skipOnboarding"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
Expand All @@ -107,7 +107,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--useFirebaseEmulator"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
Expand Down
41 changes: 23 additions & 18 deletions ENGAGEHF/Dashboard/Dashboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,39 @@ import SwiftUI

struct Dashboard: View {
@Binding var presentingAccount: Bool

@State var showSurvey = false

#if DEBUG || TEST
@Environment(MeasurementManager.self) private var measurementManager
#endif


var body: some View {
NavigationStack {
VStack {
Greeting()
Spacer()
List {
// Notifications
NotificationSection()
}
.navigationTitle("Home")
.toolbar {
if AccountButton.shouldDisplay {
AccountButton(isPresented: $presentingAccount)
}
.studyApplicationList()
.navigationTitle("Home")
.toolbar {
if AccountButton.shouldDisplay {
AccountButton(isPresented: $presentingAccount)
}
}
#if DEBUG || TEST
.toolbar {
if FeatureFlags.testMockDevices {
ToolbarItemGroup(placement: .secondaryAction) {
Button("Trigger Weight Measurement", systemImage: "scalemass.fill") {
measurementManager.loadMockWeightMeasurement()
}
Button("Trigger Blood Pressure Measurement", systemImage: "drop.fill") {
measurementManager.loadMockBloodPressureMeasurement()
}
.toolbar {
if FeatureFlags.testMockDevices {
ToolbarItemGroup(placement: .secondaryAction) {
Button("Trigger Weight Measurement", systemImage: "scalemass.fill") {
measurementManager.loadMockWeightMeasurement()
}
Button("Trigger Blood Pressure Measurement", systemImage: "drop.fill") {
measurementManager.loadMockBloodPressureMeasurement()
}
}
}
}
#endif
}
}
Expand All @@ -50,6 +54,7 @@ struct Dashboard: View {
#Preview {
Dashboard(presentingAccount: .constant(false))
.previewWith(standard: ENGAGEHFStandard()) {
NotificationManager()
MeasurementManager()
}
}
Expand Down
25 changes: 25 additions & 0 deletions ENGAGEHF/Dashboard/FetchingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import Foundation


enum FetchingError: LocalizedError {
case invalidTimestamp
case userNotAuthenticated


var errorDescription: String? {
switch self {
case .invalidTimestamp:
String(localized: "Unable to get notification timestamp.", comment: "Invalid Timestamp")
case .userNotAuthenticated:
String(localized: "User not authenticated. Please sign in an try again.", comment: "User Not Authenticated")
}
}
}
27 changes: 27 additions & 0 deletions ENGAGEHF/Dashboard/Notifications/Notification.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import FirebaseFirestore
import Foundation


// A notification
//
// Mirrors the representation of a notification in firestore
// When assigned to a patient, the title will be displayed
// and the description will be displayed in a drop-down field
//
// Title and Descprition may be markdown text
struct Notification: Identifiable, Equatable, Codable {
@DocumentID var id: String?
var type: String
var title: String
var description: String
var created: Timestamp
var completed: Bool
}
200 changes: 200 additions & 0 deletions ENGAGEHF/Dashboard/Notifications/NotificationManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import FirebaseAuth
import FirebaseFirestore
import Foundation
import OSLog
import Spezi
import SpeziFirebaseConfiguration


//
// Notification manager
//
// Maintains a list of Notifications associated with the current user in firebase
// On configuration of the app, adds a snapshot listener to the user's notification collection
//
@Observable
class NotificationManager: Module, EnvironmentAccessible {
@ObservationIgnored @Dependency private var configureFirebaseApp: ConfigureFirebaseApp
@ObservationIgnored @StandardActor var standard: ENGAGEHFStandard

private var authStateDidChangeListenerHandle: AuthStateDidChangeListenerHandle?
private var snapshotListener: ListenerRegistration?

private let logger = Logger(subsystem: "ENGAGEHF", category: "NotificationManager")

private let expirationDate = 10

var notifications: [Notification] = []


func configure() {
if ProcessInfo.processInfo.isPreviewSimulator {
let dummyNotification = Notification(
id: String(describing: UUID()),
type: "Mock Notification",
title: "Weight Recorded",
description: "A weight measurement has been recorded.",
created: Timestamp(date: .now),
completed: false
)
notifications.append(dummyNotification)
return
}

authStateDidChangeListenerHandle = Auth.auth().addStateDidChangeListener { [weak self] _, user in
self?.registerSnapshotListener(user: user)

// If testing, add 3 notifications to firestore
// Called when a user's sign in status changes
if FeatureFlags.setupTestEnvironment, let user {
// Make sure to not load the mock notifications multiple times
if let notifications = self?.notifications, notifications.isEmpty {
Task {
try await self?.setupNotificationTests(user: user)

Check warning on line 61 in ENGAGEHF/Dashboard/Notifications/NotificationManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test / Test using xcodebuild or run fastlane

reference to captured var 'self' in concurrently-executing code; this is an error in Swift 6
}
}
}
}
self.registerSnapshotListener(user: Auth.auth().currentUser)
}


// Adds three mock notifications to the user's notification collection in firestore
func setupNotificationTests(user: User) async throws {
let firestore = Firestore.firestore()
let notificationsCollection = firestore.collection("users").document(user.uid).collection("notifications")

let querySnapshot = try await notificationsCollection.getDocuments()

guard querySnapshot.documents.isEmpty else {
// Notifications collections exists and is not empty, so skip
self.logger.debug("Notifications already exist, skipping user.")
return
}

self.logger.debug("Adding test notifications for user \(user.uid)")

for idx in 1...3 {
let newNotification = Notification(
type: "Mock Notification \(idx)",
title: "This is a mock notification.",
description: "This is a long string that should be truncated by the expandable text class.",
created: Timestamp(date: .now),
completed: false
)

do {
try notificationsCollection.addDocument(from: newNotification)
} catch {
self.logger.error("Unable to load notifications to firestore: \(error)")
}
}
}


// Call on initialization
//
// Creates a snapshot listener to save new notifications to the manager
// as they are added to the user's directory in Firebase
func registerSnapshotListener(user: User?) {
logger.info("Initializing notifiation snapshot listener...")

// Remove previous snapshot listener for the user before creating new one
snapshotListener?.remove()
guard let uid = user?.uid else {
return
}

let firestore = Firestore.firestore()

// Ignore notifications older than expirationDate
guard let thresholdDate = Calendar.current.date(byAdding: .day, value: -expirationDate, to: .now) else {
logger.error("Unable to get threshold date: \(FetchingError.invalidTimestamp)")
return
}

let thesholdTimeStamp = Timestamp(date: thresholdDate)

// Set a snapshot listener on the query for valid notifications
firestore.collection("users")
.document(uid)
.collection("notifications")
.whereField("created", isGreaterThan: thesholdTimeStamp)
.whereField("completed", isEqualTo: false)
.addSnapshotListener { querySnapshot, error in
guard let documentRefs = querySnapshot?.documents else {
self.logger.error("Error fetching documents: \(error)")
return
}

self.notifications = documentRefs.compactMap {
do {
return try $0.data(as: Notification.self)
} catch {
self.logger.error("Error decoding notifications: \(error)")
return nil
}
}

self.logger.debug("Notifications updated")
}
}

func markComplete(id: String) async {
if ProcessInfo.processInfo.isPreviewSimulator {
notifications.removeAll { $0.id == id }
return
}

logger.debug("Marking notitification complete with the following id: \(id)")

let firestore = Firestore.firestore()

guard let user = Auth.auth().currentUser else {
logger.error("Unable to mark notitificaitons complete: \(FetchingError.userNotAuthenticated)")
return
}

// Mark the notifications as completed in the Firestore
let timestamp = Timestamp(date: .now)

let docRef = firestore.collection("users")
.document(user.uid)
.collection("notifications")
.document(id)

do {
try await docRef.updateData([
"completed": timestamp
])
} catch {
logger.error("Unable to update notification \(id): \(error)")
}

logger.debug("Successfully marked notifications complete!")
}
}


extension NotificationManager {
// Function for adding a mock notification for the preview simulator
func addMock() {
let dummyNotification = Notification(
id: String(describing: UUID()),
type: "Medication Change",
title: "Your dose of XXX was changed.",
description: "Your dose of XXX was changed. You can review medication information in the Education Page.",
created: Timestamp(date: .now),
completed: false
)
notifications.append(dummyNotification)
}
}
Loading
Loading