diff --git a/ios/App/Podfile b/ios/App/Podfile index 3737cdf1..34bebd79 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -11,6 +11,8 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorFirebaseAnalytics', :path => '../../node_modules/@capacitor-firebase/analytics' + pod 'CapacitorFirebaseMessaging', :path => '../../node_modules/@capacitor-firebase/messaging' pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' @@ -19,8 +21,8 @@ def capacitor_pods pod 'CapacitorInappbrowser', :path => '../../node_modules/@capacitor/inappbrowser' pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' pod 'CapacitorLiveUpdates', :path => '../../node_modules/@capacitor/live-updates' + pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications' pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' - pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' @@ -29,8 +31,10 @@ end target 'PyCon US' do capacitor_pods - # Add your Pods here - pod 'FirebaseMessaging' + # FirebaseMessaging is pulled transitively by CapacitorFirebaseMessaging + # (the @capacitor-firebase/messaging plugin) — no explicit pod needed. + # The plugin pins to ~> 11.7.0 on its v7 line; pinning our own version + # here causes "could not find compatible versions" in pod install. end post_install do |installer| diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 0d65d3bb..7bda9cd9 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -9,6 +9,9 @@ PODS: - CapacitorFilesystem (7.1.6): - Capacitor - IONFilesystemLib (~> 1.0.1) + - CapacitorFirebaseMessaging (7.5.0): + - Capacitor + - FirebaseMessaging (~> 11.7.0) - CapacitorHaptics (7.0.3): - Capacitor - CapacitorInappbrowser (2.5.3): @@ -19,6 +22,8 @@ PODS: - CapacitorLiveUpdates (0.4.0): - Capacitor - IonicLiveUpdates (~> 0.5.6) + - CapacitorLocalNotifications (7.0.6): + - Capacitor - CapacitorMlkitBarcodeScanning (7.5.0): - Capacitor - GoogleMLKit/BarcodeScanning (= 7.0.0) @@ -34,25 +39,25 @@ PODS: - Capacitor - EbarooniCapacitorCalendar (7.2.0): - Capacitor - - FirebaseCore (12.7.0): - - FirebaseCoreInternal (~> 12.7.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/Logger (~> 8.1) - - FirebaseCoreInternal (12.7.0): - - "GoogleUtilities/NSData+zlib (~> 8.1)" - - FirebaseInstallations (12.7.0): - - FirebaseCore (~> 12.7.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) + - FirebaseCore (11.7.0): + - FirebaseCoreInternal (~> 11.7.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreInternal (11.7.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.7.0): + - FirebaseCore (~> 11.7.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) - - FirebaseMessaging (12.7.0): - - FirebaseCore (~> 12.7.0) - - FirebaseInstallations (~> 12.7.0) - - GoogleDataTransport (~> 10.1) - - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/Reachability (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) + - FirebaseMessaging (11.7.0): + - FirebaseCore (~> 11.7.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) - nanopb (~> 3.30910.0) - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) @@ -125,10 +130,12 @@ DEPENDENCIES: - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorDevice (from `../../node_modules/@capacitor/device`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" + - "CapacitorFirebaseMessaging (from `../../node_modules/@capacitor-firebase/messaging`)" - "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)" - "CapacitorInappbrowser (from `../../node_modules/@capacitor/inappbrowser`)" - "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)" - "CapacitorLiveUpdates (from `../../node_modules/@capacitor/live-updates`)" + - "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)" - "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)" - "CapacitorNetwork (from `../../node_modules/@capacitor/network`)" - "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)" @@ -136,7 +143,6 @@ DEPENDENCIES: - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" - "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)" - "EbarooniCapacitorCalendar (from `../../node_modules/@ebarooni/capacitor-calendar`)" - - FirebaseMessaging SPEC REPOS: trunk: @@ -170,6 +176,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/device" CapacitorFilesystem: :path: "../../node_modules/@capacitor/filesystem" + CapacitorFirebaseMessaging: + :path: "../../node_modules/@capacitor-firebase/messaging" CapacitorHaptics: :path: "../../node_modules/@capacitor/haptics" CapacitorInappbrowser: @@ -178,6 +186,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/keyboard" CapacitorLiveUpdates: :path: "../../node_modules/@capacitor/live-updates" + CapacitorLocalNotifications: + :path: "../../node_modules/@capacitor/local-notifications" CapacitorMlkitBarcodeScanning: :path: "../../node_modules/@capacitor-mlkit/barcode-scanning" CapacitorNetwork: @@ -199,10 +209,12 @@ SPEC CHECKSUMS: CapacitorCordova: bf648a636f3c153f652d312ae145fb508b6ffced CapacitorDevice: b64654eb4d404373e733c01d835055ba59286e5c CapacitorFilesystem: 66f05ee0d8b1ccdc00d509091273a9b3b57c4a0b + CapacitorFirebaseMessaging: 1ac12c0dab16d1e4edcf9cb878620507ac868a4e CapacitorHaptics: ce15be8f287fa2c61c7d2d9e958885b90cf0bebc CapacitorInappbrowser: e1dc727db99127c2908b69408f7da8118d854a84 CapacitorKeyboard: 5660c760113bfa48962817a785879373cf5339c3 CapacitorLiveUpdates: 4a2ebeb2788f787d3133877ee159a3e900443b14 + CapacitorLocalNotifications: 2e3f5b717ec9cd2bd64d93c99a9470941e1c80be CapacitorMlkitBarcodeScanning: afd6fc431b550026a2c052e11ab2b71c7ae30011 CapacitorNetwork: 1a22460c6f900686f12ffa52f3074ee313821b08 CapacitorPushNotifications: 7604512af53bbfdf1dd8fb2256b9ef1e8a69cdc2 @@ -210,10 +222,10 @@ SPEC CHECKSUMS: CapacitorSplashScreen: 000f7591d546907dda5cbb514bd6b6d9e6048cf8 CapacitorStatusBar: d0e0151c89c001a9c7125bac59ddedf76b664768 EbarooniCapacitorCalendar: 4568a8a318b940245bf6e257a8b115626581bb7d - FirebaseCore: c7b57863ce0859281a66d16ca36d665c45d332b5 - FirebaseCoreInternal: 571a2dd8c975410966199623351db3a3265c874d - FirebaseInstallations: 6d05424a046b68ca146b4de4376f05b4e9262fc3 - FirebaseMessaging: b5f7bdc62b91b6102015991fb7bc6fa75f643908 + FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4 + FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881 + FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9 + FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 @@ -229,6 +241,6 @@ SPEC CHECKSUMS: OSInAppBrowserLib: bf7cdcc072394ae827f8cfe3f5f9c807691834c0 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 -PODFILE CHECKSUM: 4f7de5e223497bbd68a0c069de289c2f8059d888 +PODFILE CHECKSUM: 1572db56c7cc15934c703c7d85119a9d625577d8 COCOAPODS: 1.16.2 diff --git a/package-lock.json b/package-lock.json index 8d6fccce..f07fed7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ionic-conference-app", - "version": "0.0.0", + "version": "26.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ionic-conference-app", - "version": "0.0.0", + "version": "26.0.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -17,6 +17,8 @@ "@angular/platform-browser-dynamic": "^17.3.12", "@angular/router": "^17.3.12", "@angular/service-worker": "^17.3.12", + "@capacitor-firebase/analytics": "^7.5.0", + "@capacitor-firebase/messaging": "^7.5.0", "@capacitor-mlkit/barcode-scanning": "^7.0.0", "@capacitor/android": "^7.0.0", "@capacitor/app": "^7.0.0", @@ -28,8 +30,8 @@ "@capacitor/ios": "^7.0.0", "@capacitor/keyboard": "^7.0.0", "@capacitor/live-updates": "^0.4.0", + "@capacitor/local-notifications": "^7.0.6", "@capacitor/network": "^7.0.0", - "@capacitor/push-notifications": "^7.0.0", "@capacitor/share": "^7.0.0", "@capacitor/splash-screen": "^7.0.0", "@capacitor/status-bar": "^7.0.0", @@ -41,6 +43,7 @@ "@types/crypto-js": "^4.2.2", "core-js": "^3.6.4", "crypto-js": "^4.2.0", + "firebase": "^11.10.0", "js-base64": "^3.7.7", "markdown-to-txt": "^2.0.1", "patch-package": "^8.0.0", @@ -3000,6 +3003,56 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor-firebase/analytics": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@capacitor-firebase/analytics/-/analytics-7.5.0.tgz", + "integrity": "sha512-PxMTltu0wa7uT/tpXEPos8mentU4kDoU7yJeSzLIeO6ysB2Qp7N2/4GamKP1ITpZ0OvUuU7fjG5zqAQj5xX2jg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/capawesome-team/" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/capawesome" + } + ], + "license": "Apache-2.0", + "peerDependencies": { + "@capacitor/core": ">=7.0.0", + "firebase": "^11.2.0" + }, + "peerDependenciesMeta": { + "firebase": { + "optional": true + } + } + }, + "node_modules/@capacitor-firebase/messaging": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@capacitor-firebase/messaging/-/messaging-7.5.0.tgz", + "integrity": "sha512-mQ4542IX7HWyGnHZLAkOvqzYYpaH47v6ijOsBO/CdyA5hfTdGc5fbxzdS5xMYKTxfpFiecb1qgEDmB5XFWvWOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/capawesome-team/" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/capawesome" + } + ], + "license": "Apache-2.0", + "peerDependencies": { + "@capacitor/core": ">=7.0.0", + "firebase": "^11.2.0" + }, + "peerDependenciesMeta": { + "firebase": { + "optional": true + } + } + }, "node_modules/@capacitor-mlkit/barcode-scanning": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@capacitor-mlkit/barcode-scanning/-/barcode-scanning-7.5.0.tgz", @@ -3460,19 +3513,19 @@ "@capacitor/core": "^7.0.0" } }, - "node_modules/@capacitor/network": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@capacitor/network/-/network-7.0.3.tgz", - "integrity": "sha512-v1dP2GN7Vwwc6W1jJnzTE9jdXNVz/vMscqT3Gvc2jJy6v4Kpw3vHnc1JUfM4g78VkbqdwO/ProR3glTamZ9MDg==", + "node_modules/@capacitor/local-notifications": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/local-notifications/-/local-notifications-7.0.6.tgz", + "integrity": "sha512-RuZQ9P5aAlw6VU2i0i6g4i1XYZbbeDE9oaBg87gggHaUUscX7b1lltTlC1t11/JCPoyjECnZ/N8xfDNZ26hKhw==", "license": "MIT", "peerDependencies": { "@capacitor/core": ">=7.0.0" } }, - "node_modules/@capacitor/push-notifications": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-7.0.4.tgz", - "integrity": "sha512-ygTRlA9OiTNfwMZwk7/AUhtVb0/AoCDedsg5RLZUFs5g9abW0ePPzhLl4rSIYwp+4njNjXyIUsQvhnh344/HZA==", + "node_modules/@capacitor/network": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/network/-/network-7.0.3.tgz", + "integrity": "sha512-v1dP2GN7Vwwc6W1jJnzTE9jdXNVz/vMscqT3Gvc2jJy6v4Kpw3vHnc1JUfM4g78VkbqdwO/ProR3glTamZ9MDg==", "license": "MIT", "peerDependencies": { "@capacitor/core": ">=7.0.0" @@ -4102,6 +4155,645 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@firebase/ai": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-1.4.1.tgz", + "integrity": "sha512-bcusQfA/tHjUjBTnMx6jdoPMpDl3r8K15Z+snHz9wq0Foox0F/V+kNLXucEOHoTL2hTc9l+onZCyBJs2QoIC3g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.17.tgz", + "integrity": "sha512-n5vfBbvzduMou/2cqsnKrIes4auaBjdhg8QNA2ZQZ59QgtO2QiwBaXQZQE4O4sgB0Ds1tvLgUUkY+pwzu6/xEg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.23.tgz", + "integrity": "sha512-3AdO10RN18G5AzREPoFgYhW6vWXr3u+OYQv6pl3CX6Fky8QRk0AHurZlY3Q1xkXO0TDxIsdhO3y65HF7PBOJDw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.17", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", + "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.10.1.tgz", + "integrity": "sha512-MgNdlms9Qb0oSny87pwpjKush9qUwCJhfmTJHDfrcKo4neLGiSeVE4qJkzP7EQTIUFKp84pbTxobSAXkiuQVYQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.26.tgz", + "integrity": "sha512-PkX+XJMLDea6nmnopzFKlr+s2LMQGqdyT2DHdbx1v1dPSqOol2YzgpgymmhC67vitXVpNvS3m/AiWQWWhhRRPQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.10.1", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", + "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.13.2", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.8.tgz", + "integrity": "sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.5.28", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.28.tgz", + "integrity": "sha512-HpMSo/cc6Y8IX7bkRIaPPqT//Jt83iWy5rmDWeThXQCAImstkdNo3giFLORJwrZw2ptiGkOij64EH1ztNJzc7Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.10.8", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.18.tgz", + "integrity": "sha512-n28kPCkE2dL2U28fSxZJjzPPVpKsQminJ6NrzcKXAI0E/lYC8YhfwpyllScqVEvAI3J2QgJZWYgrX+1qGI+SQQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.10.tgz", + "integrity": "sha512-VMVk7zxIkgwlVQIWHOKFahmleIjiVFwFOjmakXPd/LDgaB/5vzwsB5DWIYo+3KhGxWpidQlR8geCIn39YflJIQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.20.tgz", + "integrity": "sha512-H9Rpj1pQ1yc9+4HQOotFGLxqAXwOzCHsRSRjcQFNOr8lhUt6LeYjf0NSRL04sc4X0dWe8DsCvYKxMYvFG/iOJw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.11.tgz", + "integrity": "sha512-itEsHARSsYS95+udF/TtIzNeQ0Uhx4uIna0sk4E0wQJBUnLc/G1X6D7oRljoOuwwCezRLGvWBRyNrugv/esOEw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/database": "1.0.20", + "@firebase/database-types": "1.0.15", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.15.tgz", + "integrity": "sha512-XWHJ0VUJ0k2E9HDMlKxlgy/ZuTa9EvHCGLjaKSUvrQnwhgZuRU5N3yX6SZ+ftf2hTzZmfRkv+b3QRvGg40bKNw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.12.1" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.8.0.tgz", + "integrity": "sha512-QSRk+Q1/CaabKyqn3C32KSFiOdZpSqI9rpLK5BHPcooElumOBooPFa6YkDdiT+/KhJtel36LdAacha9BptMj2A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "@firebase/webchannel-wrapper": "1.0.3", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.53", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.53.tgz", + "integrity": "sha512-qI3yZL8ljwAYWrTousWYbemay2YZa+udLWugjdjju2KODWtLG94DfO4NALJgPLv8CVGcDHNFXoyQexdRA0Cz8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/firestore": "4.8.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.9.tgz", + "integrity": "sha512-FG95w6vjbUXN84Ehezc2SDjGmGq225UYbHrb/ptkRT7OTuCiQRErOQuyt1jI1tvcDekdNog+anIObihNFz79Lg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.26.tgz", + "integrity": "sha512-A798/6ff5LcG2LTWqaGazbFYnjBW8zc65YfID/en83ALmkhu2b0G8ykvQnLtakbV9ajrMYPn7Yc/XcYsZIUsjA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/functions": "0.12.9", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.18.tgz", + "integrity": "sha512-NQ86uGAcvO8nBRwVltRL9QQ4Reidc/3whdAasgeWCPIcrhOKDuNpAALa6eCVryLnK14ua2DqekCOX5uC9XbU/A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.18.tgz", + "integrity": "sha512-aLFohRpJO5kKBL/XYL4tN+GdwEB/Q6Vo9eZOM/6Kic7asSUgmSfGPpGUZO1OAaSRGwF4Lqnvi1f/f9VZnKzChw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.22", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.22.tgz", + "integrity": "sha512-GJcrPLc+Hu7nk+XQ70Okt3M1u1eRr2ZvpMbzbc54oTPJZySHcX9ccZGVFcsZbSZ6o1uqumm8Oc7OFkD3Rn1/og==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.22.tgz", + "integrity": "sha512-5ZHtRnj6YO6f/QPa/KU6gryjmX4Kg33Kn4gRpNU6M1K47Gm8kcQwPkX7erRUYEH1mIWptfvjvXMHWoZaWjkU7A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/messaging": "0.12.22", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.7.tgz", + "integrity": "sha512-JTlTQNZKAd4+Q5sodpw6CN+6NmwbY72av3Lb6wUKTsL7rb3cuBIhQSrslWbVz0SwK3x0ZNcqX24qtRbwKiv+6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.20.tgz", + "integrity": "sha512-XkFK5NmOKCBuqOKWeRgBUFZZGz9SzdTZp4OqeUg+5nyjapTiZ4XoiiUL8z7mB2q+63rPmBl7msv682J3rcDXIQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/performance": "0.7.7", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.5.tgz", + "integrity": "sha512-fU0c8HY0vrVHwC+zQ/fpXSqHyDMuuuglV94VF6Yonhz8Fg2J+KOowPGANM0SZkLvVOYpTeWp3ZmM+F6NjwWLnw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.18.tgz", + "integrity": "sha512-YiETpldhDy7zUrnS8e+3l7cNs0sL7+tVAxvVYU0lu7O+qLHbmdtAxmgY+wJqWdW2c9nDvBFec7QiF58pEUu0qQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/remote-config": "0.6.5", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.14.tgz", + "integrity": "sha512-xTq5ixxORzx+bfqCpsh+o3fxOsGoDjC1nO0Mq2+KsOcny3l7beyBhP/y1u5T6mgsFQwI1j6oAkbT5cWdDBx87g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.24.tgz", + "integrity": "sha512-XHn2tLniiP7BFKJaPZ0P8YQXKiVJX+bMyE2j2YWjYfaddqiJnROJYqSomwW6L3Y+gZAga35ONXUJQju6MB6SOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/storage": "0.13.14", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.1.tgz", + "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz", + "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -5916,6 +6608,70 @@ "prettier": ">=2.4.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", @@ -6845,7 +7601,6 @@ "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/node-forge": { @@ -7905,7 +8660,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9027,7 +9781,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -10591,7 +11344,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/emojis-list": { @@ -10913,7 +11665,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11657,7 +12408,6 @@ "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" @@ -11813,6 +12563,42 @@ "micromatch": "^4.0.2" } }, + "node_modules/firebase": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.10.0.tgz", + "integrity": "sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "1.4.1", + "@firebase/analytics": "0.10.17", + "@firebase/analytics-compat": "0.2.23", + "@firebase/app": "0.13.2", + "@firebase/app-check": "0.10.1", + "@firebase/app-check-compat": "0.3.26", + "@firebase/app-compat": "0.4.2", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.10.8", + "@firebase/auth-compat": "0.5.28", + "@firebase/data-connect": "0.3.10", + "@firebase/database": "1.0.20", + "@firebase/database-compat": "2.0.11", + "@firebase/firestore": "4.8.0", + "@firebase/firestore-compat": "0.3.53", + "@firebase/functions": "0.12.9", + "@firebase/functions-compat": "0.3.26", + "@firebase/installations": "0.6.18", + "@firebase/installations-compat": "0.2.18", + "@firebase/messaging": "0.12.22", + "@firebase/messaging-compat": "0.2.22", + "@firebase/performance": "0.7.7", + "@firebase/performance-compat": "0.2.20", + "@firebase/remote-config": "0.6.5", + "@firebase/remote-config-compat": "0.2.18", + "@firebase/storage": "0.13.14", + "@firebase/storage-compat": "0.3.24", + "@firebase/util": "1.12.1" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -12116,7 +12902,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -12925,7 +13710,6 @@ "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, "license": "MIT" }, "node_modules/http-proxy": { @@ -13047,6 +13831,12 @@ "postcss": "^8.1.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -13348,7 +14138,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14740,6 +15529,12 @@ "lodash.keys": "^3.0.0" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -15018,6 +15813,12 @@ "node": ">=8.0" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -17547,6 +18348,45 @@ "node": ">=6" } }, + "node_modules/protobufjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/protobufjs/node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, "node_modules/protractor": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz", @@ -18887,7 +19727,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20671,7 +21510,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -20702,7 +21540,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -21997,6 +22834,12 @@ "defaults": "^1.0.3" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webdriver-js-extender": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz", @@ -22571,7 +23414,6 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", @@ -22586,7 +23428,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=0.8.0" @@ -22703,7 +23544,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -22892,7 +23732,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -22924,7 +23763,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -22953,7 +23791,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 003b8a05..ae50adb0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@angular/platform-browser-dynamic": "^17.3.12", "@angular/router": "^17.3.12", "@angular/service-worker": "^17.3.12", + "@capacitor-firebase/analytics": "^7.5.0", + "@capacitor-firebase/messaging": "^7.5.0", "@capacitor-mlkit/barcode-scanning": "^7.0.0", "@capacitor/android": "^7.0.0", "@capacitor/app": "^7.0.0", @@ -38,8 +40,8 @@ "@capacitor/ios": "^7.0.0", "@capacitor/keyboard": "^7.0.0", "@capacitor/live-updates": "^0.4.0", + "@capacitor/local-notifications": "^7.0.6", "@capacitor/network": "^7.0.0", - "@capacitor/push-notifications": "^7.0.0", "@capacitor/share": "^7.0.0", "@capacitor/splash-screen": "^7.0.0", "@capacitor/status-bar": "^7.0.0", @@ -51,6 +53,7 @@ "@types/crypto-js": "^4.2.2", "core-js": "^3.6.4", "crypto-js": "^4.2.0", + "firebase": "^11.10.0", "js-base64": "^3.7.7", "markdown-to-txt": "^2.0.1", "patch-package": "^8.0.0", diff --git a/src/app/app.component.html b/src/app/app.component.html index 59ab0f36..d3664abe 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -194,6 +194,13 @@

Install Latest Update

+ + + + Notifications + + + { this.checkNotifications(); this.liveUpdateService.checkForUpdate(); + // Load saved prefs and (re)schedule local sign-up reminders. Re-runs + // every cold start so missed/expired reminders get cleaned up and + // anything still in the future gets re-armed. + this.notifications.getPrefs().then(() => this.notifications.applyPrefs()); }, 5000); } checkNotifications() { - PushNotifications.addListener('registration', token => { - console.info('Registration token: ', token.value); + // Single-stack push: @capacitor-firebase/messaging owns APNs/FCM + // registration, topic subscription, and foreground delivery. The + // older @capacitor/push-notifications plugin was removed because + // Capawesome explicitly warns the two cannot coexist (duplicate + // APNs swizzling, conflicting FirebaseMessagingService receivers). + FirebaseMessaging.addListener('tokenReceived', (event) => { + console.info('Registration token: ', event.token); }); - PushNotifications.requestPermissions().then(result => { - if (result.receive === 'granted') { - PushNotifications.register(); - } + FirebaseMessaging.requestPermissions().then((result) => { + if (result.receive !== 'granted') return; + // Calling getToken() also triggers APNs registration on iOS, so + // we don't need a separate register() step. + FirebaseMessaging.getToken().catch((err) => { + console.warn('FirebaseMessaging.getToken failed', err); + }); + }); + FirebaseMessaging.addListener('notificationReceived', async (event) => { + // FCM pushes (emergency, announcements, schedule changes) — the + // toggle in Settings opts the device out of the underlying topic + // so users who turn a category off don't receive the push at all. + // This check is a defense in case staff send to all devices + // instead of a topic. + const showed = this.notifications.shouldShowPushBanner(); + // Log every received push so we can compare delivery counts to + // opt-in rates in Firebase Analytics. The `topic` data field is + // attached server-side when staff fire a campaign via topic. + FirebaseAnalytics.logEvent({ + name: 'notification_received', + params: { + topic: event?.notification?.data?.['topic'] ?? 'unknown', + banner_shown: showed, + }, + }).catch(() => undefined); + if (!showed) return; + const notification = event?.notification; + const title = notification?.title ?? 'PyCon US'; + const body = notification?.body ?? ''; + this.toastCtrl.create({ + message: body ? `${title}: ${body}` : title, + position: 'top', + buttons: [{ + icon: 'close', + side: 'end', + role: 'cancel', + }], + }).then((toast) => toast.present()); }); - PushNotifications.addListener( - 'pushNotificationReceived', - async (notification: PushNotificationSchema) => { - this.toastCtrl.create({ - message: `${notification.title}: ${notification.body}`, - position: 'top', - buttons: [{ - icon: 'close', - side: 'end', - role: 'cancel' - }] - }).then(toast => toast.present()); - } - ); } initializeApp() { diff --git a/src/app/location-map/room-locations.ts b/src/app/location-map/room-locations.ts index 9781bcab..37074e90 100644 --- a/src/app/location-map/room-locations.ts +++ b/src/app/location-map/room-locations.ts @@ -86,6 +86,7 @@ const ROOM_LOCATIONS_RAW: Record = { // tilted parallelogram in the upper right. '101a': concourse('Room 101A', 14, 50, 'Tutorials / PSF Members Lunch / PyLadies Lunch'), '101b': concourse('Room 101B', 19, 50, 'Tutorials / PSF Members Lunch / PyLadies Lunch'), + '101ab': concourse('Room 101AB', 17, 50, 'Tutorials / PSF Members Lunch / PyLadies Lunch'), '102a': concourse('Room 102A', 23, 50, 'Tutorials / Open Spaces'), '102b': concourse('Room 102B', 27, 50, 'Tutorials / Open Spaces'), '102c': concourse('Room 102C', 31, 50, 'Tutorials / Open Spaces'), diff --git a/src/app/pages/keynote-speakers/keynote-speakers.page.html b/src/app/pages/keynote-speakers/keynote-speakers.page.html index 299af666..473643d7 100644 --- a/src/app/pages/keynote-speakers/keynote-speakers.page.html +++ b/src/app/pages/keynote-speakers/keynote-speakers.page.html @@ -27,7 +27,7 @@

{{ speaker.name }}

{{ speaker.session.name }}

-

{{ speaker.session.day }} {{ speaker.session.timeStart }} — {{ speaker.session.location }}

+

{{ speaker.session.day }} {{ speaker.session.timeStart }} — {{ speaker.session.displayLocation || speaker.session.location }}

@@ -35,14 +35,49 @@

{{ speaker.session.name }}

-
-
- - {{ member.name }} +

{{ steeringCouncil.name }}

+
+
+ + {{ member.name }}
-

{{ steeringCouncil.name }}

{{ steeringCouncil.bio }}

+ + + +

{{ steeringCouncil.session.name }}

+

{{ steeringCouncil.session.day }} {{ steeringCouncil.session.timeStart }} — {{ steeringCouncil.session.displayLocation || steeringCouncil.session.location }}

+
+
+ + + + + +

{{ diversityPanel.eyebrow }}

+

{{ diversityPanel.name }}

+

{{ diversityPanel.intro }}

+
+
+ +
+

{{ member.name }}

+

{{ member.bio }}

+
+
+
+ + + +

{{ diversityPanel.session.name }}

+

{{ diversityPanel.session.day }} {{ diversityPanel.session.timeStart }} — {{ diversityPanel.session.displayLocation || diversityPanel.session.location }}

+
+
diff --git a/src/app/pages/keynote-speakers/keynote-speakers.page.scss b/src/app/pages/keynote-speakers/keynote-speakers.page.scss index eab42019..8239625c 100644 --- a/src/app/pages/keynote-speakers/keynote-speakers.page.scss +++ b/src/app/pages/keynote-speakers/keynote-speakers.page.scss @@ -148,3 +148,97 @@ ion-title { max-width: 56px; line-height: 1.2; } + +.ks-council-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 16px; + margin: 16px 0 20px; +} + +.ks-council-cell { + display: flex; + flex-direction: column; + align-items: center; + width: 110px; +} + +.ks-council-photo-large { + width: 100px; + height: 100px; + border-radius: 8px; + object-fit: cover; + border: 2px solid #3B3EA9; +} + +.ks-council-name-large { + margin-top: 8px; + font-size: 0.78rem; + font-weight: 600; + text-align: center; + line-height: 1.25; + color: var(--ion-text-color); +} + +.ks-card-panel { + .ks-panel-eyebrow { + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--ion-color-medium); + margin: 0 0 4px; + } + + h2 { + font-size: 1.1rem; + font-weight: 700; + margin: 0 0 8px; + } + + .ks-panel-intro { + font-size: 0.92rem; + color: var(--ion-color-step-700, #4a4a4a); + margin: 0 0 16px; + } +} + +.ks-panel-members { + display: flex; + flex-direction: column; + gap: 14px; +} + +.ks-panel-member { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.ks-panel-photo { + width: 64px; + height: 64px; + border-radius: 6px; + object-fit: cover; + flex-shrink: 0; + border: 1px solid var(--ion-color-step-150, #e0e0e0); +} + +.ks-panel-text { + flex: 1; + min-width: 0; + + h3 { + margin: 0 0 4px; + font-size: 0.95rem; + font-weight: 600; + } + + p { + margin: 0; + font-size: 0.82rem; + color: var(--ion-color-medium); + line-height: 1.4; + } +} diff --git a/src/app/pages/keynote-speakers/keynote-speakers.page.ts b/src/app/pages/keynote-speakers/keynote-speakers.page.ts index 60945f20..6eeef954 100644 --- a/src/app/pages/keynote-speakers/keynote-speakers.page.ts +++ b/src/app/pages/keynote-speakers/keynote-speakers.page.ts @@ -19,6 +19,21 @@ interface SteeringCouncil { name: string; members: SteeringCouncilMember[]; bio: string; + session?: any; +} + +interface PanelMember { + name: string; + photo: string | null; + bio: string; +} + +interface Panel { + name: string; + eyebrow: string; + members: PanelMember[]; + intro: string; + session?: any; } @Component({ @@ -70,6 +85,55 @@ export class KeynoteSpeakersPage implements OnInit { bio: 'The Python Steering Council is a 5-person elected committee that assumes a mandate to maintain the quality and stability of the Python language and CPython interpreter, improve the contributor experience, formalize and maintain a relationship between the Python core team and the PSF, establish decision making processes for Python Enhancement Proposals, seek consensus among contributors and the Python core team, and resolve decisions and disputes in decision making among the language.', }; + diversityPanel: Panel = { + name: 'D&I Panel: Python is for Everyone — Growing the Community Without Limits', + eyebrow: 'Hosted by the PSF Diversity & Inclusion Workgroup', + intro: + 'A panel from the PSF Diversity & Inclusion Workgroup on growing the Python community without limits — bringing together organizers and contributors from PyLadies chapters across Brazil, the U.S., Ghana, and Malaysia.', + members: [ + { + name: 'Jules', + photo: null, + bio: + 'Jules (they/them, she/her) is a nonbinary Brazilian who is PyLadies Recife and PyLadies Brasil Co-organizer. Fullstack developer by daylight and artist by moonlight, they are always eager to support event organizers and help provide a more inclusive community at the Diversity and Inclusion Workgroup from PSF. Former board member from Python Brazil Association (APyB) from 2022 to 2026. AuDHD and STEMinist.', + }, + { + name: 'Débora Azevedo', + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/deborah.original.png', + bio: + 'Débora is a public school teacher in Brazil, and one of the cofounders of PyLadies Brazil, the largest PyLadies chapter in the world. She’s a PhD student and she researches educational software development. She’s currently one of the organizers of Python Nordeste, a regional Python conference in Brazil, and a former PSF board member (2021–2024).', + }, + { + name: 'Alla Barbalat', + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/alla.original.png', + bio: + 'Alla Barbalat began her career as a lawyer before transitioning into tech. She is the lead organizer of PyLadies San Francisco, an avid Python user, and a speaker on topics at the intersection of Python, AI, and law.', + }, + { + name: 'Georgi Ker', + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/georgi.original.jpg', + bio: + 'Georgi Ker is the Director and a Fellow of the Python Software Foundation. She co-organizes PyLadiesCon and chairs the D&I Workgroup within the PSF. She is also one of the co-hosts of the podcast series "The Hidden Figures of Python" alongside Mariatta Wijaya, Cheuk Ting Ho, and Tereza Iofciu.', + }, + { + name: 'Theresa Seyram Agbenyegah (Stancy)', + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/Stancy-Portrait.original.jpg', + bio: + 'Theresa Seyram Agbenyegah (mostly referred to in the Tech community as Stancy) is a Software Engineer, Open-Source advocate, and Social Entrepreneur. She currently serves as the Programmes and Events Lead for PyLadies Ghana and is a member of Python Ghana. She is a DSF member and a member of the DSF event support working group, a PSF Diversity and Inclusion workgroup member, an Outreach ambassador for the CHAOSS DEI workgroup, and a Django Girls organizer.', + }, + { + name: 'Abhijeet Mote', + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/MNTN_Matt-Lief-Anderson_2856-Edit.original.jpg', + bio: + 'Abhijeet is a Lead Python AI Engineer and Fellow of the Python Software Foundation. He founded Python Penang, Malaysia, where he has helped grow the local developer community. He has spoken at international conferences including PyCon Italy, runs workshops, and mentors students and underrepresented groups in technology. His work focuses on scalable Python AI systems, distributed systems, data pipelines, and LLM-based applications across adtech, semiconductor, and healthcare.', + }, + ], + }; + + // Fallback avatar for panelists without an uploaded photo. Reuses the + // global default so the placeholder matches the rest of the app. + panelistFallback = 'assets/img/person-circle-outline.png'; + constructor( public liveUpdateService: LiveUpdateService, private confData: ConferenceData, @@ -89,6 +153,24 @@ export class KeynoteSpeakersPage implements OnInit { speaker.session = match; } }); + + // The D&I Panel ships as a kind="plenary" slot named + // "Diversity & Inclusion Panel" — not as a keynote — so look it + // up across all sessions, not just `keynoteSessions` above. + this.diversityPanel.session = data.sessions.find( + (s: any) => + typeof s.name === 'string' && + /diversity\s*(?:&|and)\s*inclusion\s+panel/i.test(s.name), + ); + // Steering Council session link. The schedule slot is literally + // titled "Steering Council Panel" (after track-prefix stripping), + // not "Python Steering Council" — match flexibly so future title + // tweaks don't break the link. + this.steeringCouncil.session = data.sessions.find( + (s: any) => + typeof s.name === 'string' && + /steering\s+council/i.test(s.name), + ); } }); } diff --git a/src/app/pages/notification-settings/notification-settings-routing.module.ts b/src/app/pages/notification-settings/notification-settings-routing.module.ts new file mode 100644 index 00000000..a55d0b76 --- /dev/null +++ b/src/app/pages/notification-settings/notification-settings-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { NotificationSettingsPage } from './notification-settings.page'; + +const routes: Routes = [ + { + path: '', + component: NotificationSettingsPage, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class NotificationSettingsPageRoutingModule {} diff --git a/src/app/pages/notification-settings/notification-settings.module.ts b/src/app/pages/notification-settings/notification-settings.module.ts new file mode 100644 index 00000000..7d62550f --- /dev/null +++ b/src/app/pages/notification-settings/notification-settings.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; + +import { NotificationSettingsPageRoutingModule } from './notification-settings-routing.module'; +import { NotificationSettingsPage } from './notification-settings.page'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + NotificationSettingsPageRoutingModule, + ], + declarations: [NotificationSettingsPage], +}) +export class NotificationSettingsPageModule {} diff --git a/src/app/pages/notification-settings/notification-settings.page.html b/src/app/pages/notification-settings/notification-settings.page.html new file mode 100644 index 00000000..5bf062af --- /dev/null +++ b/src/app/pages/notification-settings/notification-settings.page.html @@ -0,0 +1,127 @@ + + + + + + Notifications + + + + +
+ +

Notifications

+

Choose which alerts you want from the PyCon US app.

+
+ + + + +

All notifications

+

{{ allDisabled ? 'Everything muted.' : (allEnabled ? 'Everything on.' : 'Mixed — toggle to set them all at once.') }}

+
+ + +
+ + + +

Lightning Talk sign-ups

+

Reminds you 15 min before each lightning-talk signup window opens.

+
+ + +
+ + + +

Open Space sign-ups

+

Reminds you 15 min before each day's open-space signup window opens.

+
+ + +
+ + + +

Announcements

+

General conference updates — "keynote starting now," "opening reception in 20 minutes," etc.

+
+ + +
+ + + +

Schedule changes

+

Push when a session is rescheduled, moved to a different room, or cancelled.

+
+ + +
+ + + +

Daily digest

+

One push per day with the day's highlights, linking to the recap on us.pycon.org.

+
+ + +
+ + + +

Emergency & safety alerts

+

Push when staff send a safety or emergency notice.

+
+ + +
+
+ +
+ + +

If you previously denied notification permission for this app, enable it from your device settings to receive alerts.

+
+
+ + + + Diagnostics + + + +

FCM registration token

+

{{ fcmToken }}

+
+
+ + + + Copy token + + +
+
diff --git a/src/app/pages/notification-settings/notification-settings.page.scss b/src/app/pages/notification-settings/notification-settings.page.scss new file mode 100644 index 00000000..5006a0bc --- /dev/null +++ b/src/app/pages/notification-settings/notification-settings.page.scss @@ -0,0 +1,139 @@ +ion-header { + background: linear-gradient(180deg, #3B3EA9 0%, #3B3EA9 100%); + &::after { display: none; } +} + +ion-toolbar { + --background: transparent; + --border-color: transparent; + --color: #ffffff; +} + +ion-toolbar ion-menu-button { + --color: #ffffff; +} + +ion-title { + opacity: 0; + transition: opacity 0.25s ease; + + &.title-visible { + opacity: 1; + } +} + +.notif-hero { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px 16px 40px; + background: linear-gradient(180deg, #3B3EA9 23.5%, #101136 53.29%); + color: #fff; + text-align: center; + + .notif-hero-icon { + font-size: 72px; + margin-bottom: 16px; + color: #FFD779; + } + + h1 { + margin: 0; + font-size: 1.6rem; + font-weight: 700; + } + + p { + margin: 6px 16px 0; + font-size: 0.9rem; + opacity: 0.8; + } +} + +.notif-list { + margin: -24px 16px 0; + border-radius: 16px; + background: var(--ion-card-background, #ffffff); + box-shadow: 0 8px 32px rgba(16, 17, 54, 0.15); + position: relative; + z-index: 1; + overflow: hidden; + + ion-item { + --background: transparent; + } + + .notif-master { + --background: var(--ion-color-step-50, #f7f8fb); + --inner-padding-top: 14px; + --inner-padding-bottom: 14px; + + .notif-label h2 { + font-weight: 700; + } + } + + .notif-label { + h2 { + font-size: 1rem; + font-weight: 600; + margin: 0 0 0.25rem; + white-space: normal; + } + p { + font-size: 0.8rem; + color: var(--ion-color-medium); + margin: 0; + white-space: normal; + } + } +} + +.notif-token-card { + margin: 16px; + border-radius: 12px; + background: var(--ion-card-background, #ffffff); + box-shadow: 0 2px 8px rgba(16, 17, 54, 0.08); + overflow: hidden; + + ion-item { + --background: transparent; + } + + ion-item-divider { + --background: var(--ion-color-step-50, #f4f4f4); + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ion-color-medium); + min-height: 32px; + } + + .notif-token { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.7rem; + word-break: break-all; + user-select: all; + -webkit-user-select: all; + color: var(--ion-color-medium); + } +} + +.notif-note { + margin: 24px 16px 16px; + display: flex; + align-items: center; + gap: 8px; + justify-content: center; + + ion-icon { + font-size: 1.1rem; + color: var(--ion-color-medium); + } + + p { + margin: 0; + font-size: 0.8rem; + } +} diff --git a/src/app/pages/notification-settings/notification-settings.page.ts b/src/app/pages/notification-settings/notification-settings.page.ts new file mode 100644 index 00000000..6cca2a0a --- /dev/null +++ b/src/app/pages/notification-settings/notification-settings.page.ts @@ -0,0 +1,128 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ToastController } from '@ionic/angular'; +import { Subscription } from 'rxjs'; +import { + NotificationsService, + NotificationCategory, + NotificationPrefs, +} from '../../providers/notifications.service'; + +@Component({ + selector: 'app-notification-settings', + templateUrl: './notification-settings.page.html', + styleUrls: ['./notification-settings.page.scss'], +}) +export class NotificationSettingsPage implements OnInit, OnDestroy { + prefs: NotificationPrefs = { + lightning: true, + openSpace: true, + emergency: true, + announcements: true, + scheduleChanges: true, + dailyDigest: true, + }; + loaded = false; + showTitle = false; + fcmToken: string | null = null; + private tokenSub?: Subscription; + + onScroll(event: any) { + this.showTitle = event.detail.scrollTop > 100; + } + + constructor( + private notifications: NotificationsService, + private toastCtrl: ToastController, + ) {} + + async ngOnInit() { + this.prefs = await this.notifications.getPrefs(); + this.loaded = true; + this.tokenSub = this.notifications.fcmToken$.subscribe((token) => { + this.fcmToken = token; + }); + } + + ngOnDestroy() { + this.tokenSub?.unsubscribe(); + } + + async onToggle(key: NotificationCategory, value: boolean) { + this.prefs = { ...this.prefs, [key]: value }; + await this.notifications.setPref(key, value); + const toast = await this.toastCtrl.create({ + message: value ? 'Notifications enabled' : 'Notifications disabled', + duration: 1500, + position: 'bottom', + }); + toast.present(); + } + + // True iff every category is currently enabled. Drives the master + // toggle's checked state. Indeterminate (some on, some off) reads as + // false so flipping it pushes everything on. + get allEnabled(): boolean { + return ( + this.prefs.lightning && + this.prefs.openSpace && + this.prefs.announcements && + this.prefs.scheduleChanges && + this.prefs.dailyDigest && + this.prefs.emergency + ); + } + + // True iff every category is currently disabled. Drives copy on the + // master toggle's subtitle so users can tell which state they're in + // when categories are mixed. + get allDisabled(): boolean { + return ( + !this.prefs.lightning && + !this.prefs.openSpace && + !this.prefs.announcements && + !this.prefs.scheduleChanges && + !this.prefs.dailyDigest && + !this.prefs.emergency + ); + } + + async onMasterToggle(value: boolean) { + this.prefs = { + ...this.prefs, + lightning: value, + openSpace: value, + announcements: value, + scheduleChanges: value, + dailyDigest: value, + emergency: value, + }; + await this.notifications.setAllPrefs(value); + const toast = await this.toastCtrl.create({ + message: value ? 'All notifications enabled' : 'All notifications muted', + duration: 1500, + position: 'bottom', + }); + toast.present(); + } + + async copyToken() { + if (!this.fcmToken) return; + try { + await navigator.clipboard.writeText(this.fcmToken); + const toast = await this.toastCtrl.create({ + message: 'FCM token copied', + duration: 1500, + position: 'bottom', + color: 'success', + }); + toast.present(); + } catch { + const toast = await this.toastCtrl.create({ + message: 'Could not copy — long-press the token to select.', + duration: 2500, + position: 'bottom', + }); + toast.present(); + } + } +} diff --git a/src/app/pages/schedule-list/schedule-list.page.html b/src/app/pages/schedule-list/schedule-list.page.html index fcd67111..b1b35ac7 100644 --- a/src/app/pages/schedule-list/schedule-list.page.html +++ b/src/app/pages/schedule-list/schedule-list.page.html @@ -117,6 +117,14 @@

+ + + + + View on us.pycon.org + + + diff --git a/src/app/pages/schedule-list/schedule-list.page.scss b/src/app/pages/schedule-list/schedule-list.page.scss index ca3258ab..66451aca 100644 --- a/src/app/pages/schedule-list/schedule-list.page.scss +++ b/src/app/pages/schedule-list/schedule-list.page.scss @@ -165,3 +165,17 @@ ion-title { border-radius: 4px; object-fit: cover; } + +.open-space-site-link { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.85rem; + color: var(--ion-color-primary); + text-decoration: none; + margin-top: 4px; + + ion-icon { font-size: 0.9rem; } + + &:hover { text-decoration: underline; } +} diff --git a/src/app/pages/schedule-list/schedule-list.page.ts b/src/app/pages/schedule-list/schedule-list.page.ts index 5254ce10..fefe45d6 100644 --- a/src/app/pages/schedule-list/schedule-list.page.ts +++ b/src/app/pages/schedule-list/schedule-list.page.ts @@ -159,6 +159,18 @@ export class ScheduleListPage implements OnInit { loader.present(); this.displaySessions = []; this.confData.getSessions(this.sessionQueryText, this.excludeTracks).subscribe((sessions: any[]) => { + // Belt-and-suspenders: on the open-spaces page, restrict to + // sessions whose track is literally 'Open Space'. The + // excludeTracks filter already does this in theory, but if the + // initial async ordering ever gets restored (or a session has a + // missing/empty `tracks` array) we don't want posters leaking + // back onto this page. Equivalent guard for any other view that + // expects only one track is left for future cleanup. + if (this.isOpenSpaceView) { + sessions = sessions.filter( + (s: any) => s?.track === 'Open Space' || s?.tracks?.includes('Open Space'), + ); + } this.sessions = sessions; this.generateSessions(); setTimeout(() => {loader.dismiss()}, 100); @@ -184,6 +196,12 @@ export class ScheduleListPage implements OnInit { this.confData.load().subscribe((data: any) => { if (data.sessions) { + // Build excludeTracks BEFORE fetching the filtered session list. + // Previously resetSessions() ran outside this subscribe, so by the + // time the filter executed excludeTracks was still empty and + // every session leaked through (e.g. posters showing on the + // open-spaces page). Move resetSessions inside so the filter + // sees a populated excludeTracks. PYMOBIL-bug. this.confData.getTracks().subscribe((tracks: any[]) => { tracks.forEach((track, index, arr) => { const trackNameToCompare = typeof track === 'string' ? track : track.name; @@ -208,8 +226,8 @@ export class ScheduleListPage implements OnInit { this.excludeTracks.splice(i, 1); } } + this.resetSessions(); }); - this.resetSessions(); } }); } diff --git a/src/app/pages/session-detail/session-detail.html b/src/app/pages/session-detail/session-detail.html index 3b28ef1f..7180d61f 100644 --- a/src/app/pages/session-detail/session-detail.html +++ b/src/app/pages/session-detail/session-detail.html @@ -99,6 +99,13 @@

{{ keynoteAbstr (click)="onDescriptionClick($event)">

+ +

Posters ({{posters.length}})

= { @@ -91,6 +136,54 @@ export class SessionDetailPage implements OnDestroy { photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/Pablo_Galindo_Salgado.original.jpg', bio: 'CPython core developer and Theoretical Physicist. Currently serving on the Python Steering Council in his 6th term and release manager for Python 3.10 and 3.11.', }, + // D&I Panel — six panelists. The session-detail enrichment matches by + // substring on the session name; for the panel, the talk name is + // "Diversity & Inclusion Panel" and the panelist names below all + // appear in the abstract paragraphs above. Keep these names in lowercase + // for case-insensitive matching when needed. + 'Jules': { + photo: 'assets/img/person-circle-outline.png', + bio: 'Jules (they/them, she/her) is a nonbinary Brazilian who is PyLadies Recife and PyLadies Brasil Co-organizer. Fullstack developer by daylight and artist by moonlight, they are always eager to support event organizers and help provide a more inclusive community at the Diversity and Inclusion Workgroup from PSF. Former board member from Python Brazil Association (APyB) from 2022 to 2026. AuDHD and STEMinist.', + }, + 'Débora Azevedo': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/deborah.original.png', + bio: 'Débora is a public school teacher in Brazil, and one of the cofounders of PyLadies Brazil, the largest PyLadies chapter in the world. She’s a PhD student and she researches educational software development. She’s currently one of the organizers of Python Nordeste, a regional Python conference in Brazil, and a former PSF board member (2021–2024).', + }, + 'Alla Barbalat': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/alla.original.png', + bio: 'Alla Barbalat began her career as a lawyer before transitioning into tech. She is the lead organizer of PyLadies San Francisco, an avid Python user, and a speaker on topics at the intersection of Python, AI, and law.', + }, + 'Georgi Ker': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/georgi.original.jpg', + bio: 'Georgi Ker is the Director and a Fellow of the Python Software Foundation. She co-organizes PyLadiesCon and chairs the D&I Workgroup within the PSF. She is also one of the co-hosts of the podcast series "The Hidden Figures of Python" alongside Mariatta Wijaya, Cheuk Ting Ho, and Tereza Iofciu.', + }, + 'Theresa Seyram Agbenyegah': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/Stancy-Portrait.original.jpg', + bio: 'Theresa Seyram Agbenyegah (mostly referred to in the Tech community as Stancy) is a Software Engineer, Open-Source advocate, and Social Entrepreneur. She currently serves as the Programmes and Events Lead for PyLadies Ghana and is a member of Python Ghana. She is a DSF member and a member of the DSF event support working group, a PSF Diversity and Inclusion workgroup member, an Outreach ambassador for the CHAOSS DEI workgroup, and a Django Girls organizer.', + }, + 'Abhijeet Mote': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/MNTN_Matt-Lief-Anderson_2856-Edit.original.jpg', + bio: 'Abhijeet is a Lead Python AI Engineer and Fellow of the Python Software Foundation. He founded Python Penang, Malaysia, where he has helped grow the local developer community. He has spoken at international conferences including PyCon Italy, runs workshops, and mentors students and underrepresented groups in technology. His work focuses on scalable Python AI systems, distributed systems, data pipelines, and LLM-based applications across adtech, semiconductor, and healthcare.', + }, + // Python Steering Council panelists. Pablo is already above with his + // standalone keynote bio — kept that copy; the Steering Council panel + // pulls all five via the abstract's `panelists` list. + 'Barry Warsaw': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/Barry_PyCon.max-165x165.jpg', + bio: 'Python Steering Council member.', + }, + 'Donghee Na': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/donghee_na.max-165x165.jpg', + bio: 'Python Steering Council member and CPython core developer.', + }, + 'Savannah Ostrowski': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/savannah.max-165x165.jpg', + bio: 'Python Steering Council member.', + }, + 'Thomas Wouters': { + photo: 'https://pycon-assets.s3.amazonaws.com/2026/media/images/Thomas_Wouters.max-165x165.jpg', + bio: 'Python Steering Council member, CPython core developer, and release manager.', + }, }; constructor( @@ -133,6 +226,12 @@ export class SessionDetailPage implements OnDestroy { this.session = foundSession; this.isOpenSpace = this.session?.track === 'Open Space' || this.session?.tracks?.includes('Open Space'); this.isKeynote = this.session?.tracks?.includes('keynote') || this.session?.track === 'Keynote'; + // Panels (D&I, Steering Council) ship as kind="plenary" not + // "keynote", so isKeynote is false — but they still need the + // panelist photos + abstract treatment. Detect by name pattern. + const isPanel = + typeof this.session?.name === 'string' && + /(?:diversity\s*(?:&|and)\s*inclusion\s+panel|steering\s+council\s+panel)/i.test(this.session.name); // Only the *collapsed* "Posters" schedule slot lists every poster; // individual poster session-detail pages show their own description. this.isPosters = this.session?.track === 'Poster' && this.session?.name === 'Posters'; @@ -148,17 +247,29 @@ export class SessionDetailPage implements OnDestroy { this.keynoteData = []; this.keynoteAbstract = null; - // Enrich keynote sessions with speaker photo/bio. Collect every - // matching speaker so co-hosted keynotes (e.g. "Rachell Calhoun & - // Tim Schilling") render all speakers, not just the first match. - if (this.isKeynote) { + // Enrich keynote and panel sessions with speaker photo/bio. + // Keynotes match by substring on the session title (works because + // a keynote's title typically includes the speaker's name). + // Panels (D&I, Steering Council) match by abstract: the abstract + // entry carries an explicit `panelists` list of names to render, + // since the session title doesn't name them. + if (this.isKeynote || isPanel) { const sessionName = (this.session?.name || '').toLowerCase(); - this.keynoteData = Object.entries(this.keynoteSpeakers) - .filter(([name]) => sessionName.includes(name.toLowerCase())) - .map(([name, data]) => ({ name, ...data })); this.keynoteAbstract = this.keynoteAbstracts.find( (a) => a.match.some((m) => sessionName.includes(m.toLowerCase())) ) || null; + if (this.keynoteAbstract?.panelists?.length) { + this.keynoteData = this.keynoteAbstract.panelists + .map((name) => { + const data = this.keynoteSpeakers[name]; + return data ? { name, ...data } : null; + }) + .filter((entry): entry is { name: string; photo: string; bio: string } => entry !== null); + } else { + this.keynoteData = Object.entries(this.keynoteSpeakers) + .filter(([name]) => sessionName.includes(name.toLowerCase())) + .map(([name, data]) => ({ name, ...data })); + } } if (this.session?.id != null) { diff --git a/src/app/pages/tabs-page/tabs-page-routing.module.ts b/src/app/pages/tabs-page/tabs-page-routing.module.ts index 9656360b..bc014033 100644 --- a/src/app/pages/tabs-page/tabs-page-routing.module.ts +++ b/src/app/pages/tabs-page/tabs-page-routing.module.ts @@ -125,6 +125,18 @@ const routes: Routes = [ } ] }, + { + path: 'notifications', + children: [ + { + path: '', + loadChildren: () => + import('../notification-settings/notification-settings.module').then( + m => m.NotificationSettingsPageModule, + ), + }, + ], + }, { path: 'rooms', children: [ diff --git a/src/app/pages/wifi/wifi.page.html b/src/app/pages/wifi/wifi.page.html index 8f41e2ee..b2abf3d5 100644 --- a/src/app/pages/wifi/wifi.page.html +++ b/src/app/pages/wifi/wifi.page.html @@ -33,10 +33,6 @@

Connect to Wi-Fi

Copy Password - - - Open Wi-Fi Settings -
{ + // Skip submissions that haven't been scheduled yet — un-roomed or + // un-timed entries are *proposals*, not confirmed sessions, and + // shouldn't appear on the schedule. The pretalx feed includes all + // submissions regardless of state. + if (!openSpace?.start || !openSpace?.end) return; + const startMs = new Date(openSpace.start).getTime(); + const endMs = new Date(openSpace.end).getTime(); + if (Number.isNaN(startMs) || Number.isNaN(endMs)) return; + const room = openSpace.room_display || openSpace.room || ''; + if (!room.trim()) return; var start = new Date(openSpace.start); var end = new Date(openSpace.end); var session = { @@ -228,6 +247,12 @@ export class ConferenceData { "id": openSpace.conf_key + 9000, "day": start.toLocaleDateString('en-us', {timeZone: environment.timezone, weekday: 'short'}), "imageUrl": this.resolveOpenSpaceImage(openSpace.image_url), + // Deep-link to the open-space modal on us.pycon.org. The website + // opens a matching `#OpenSpace-` on hash change + // (see pycon-site/static/js/lib/dialog.js, PR #712). conf_key is + // the OpenSpacesSignup PK on the website side. The page lives at + // /2026/schedule/open-spaces/, NOT /2026/schedule/conference/. + "siteUrl": `${environment.baseUrl}/2026/schedule/open-spaces/#OpenSpace-${openSpace.conf_key}`, } this.data.sessions.push(session); @@ -749,6 +774,19 @@ export class ConferenceData { session.displayLocation = session.displayLocationOverride || (links.length > 0 ? links[0].name : (session.location || '')); }); + // Seed empty entries for sprint-only rooms. Sprints aren't in + // data.sessions (the API ships them as a separate `sprints` array, not + // as schedule slots), so without this the Seaside S-rooms / ballrooms + // never appear in the Rooms list — even though they're real venues + // attendees need to find. PYMOBIL-120. + Object.values(ROOM_LOCATIONS).forEach((loc: any) => { + if (!loc?.sublabel || !/sprints/i.test(loc.sublabel)) return; + const slug = this.slugifyRoom(loc.label); + if (!roomMap.has(slug)) { + roomMap.set(slug, { name: loc.label, slug, sessions: [] }); + } + }); + roomMap.forEach((room: any) => { room.sessions.sort( (a: any, b: any) => diff --git a/src/app/providers/notifications.service.ts b/src/app/providers/notifications.service.ts new file mode 100644 index 00000000..cef8e252 --- /dev/null +++ b/src/app/providers/notifications.service.ts @@ -0,0 +1,332 @@ +import { Injectable } from '@angular/core'; +import { Platform, ToastController } from '@ionic/angular'; +import { Storage } from '@ionic/storage-angular'; +import { + LocalNotifications, + ScheduleOptions, + PermissionStatus, +} from '@capacitor/local-notifications'; +import { FirebaseMessaging } from '@capacitor-firebase/messaging'; +import { FirebaseAnalytics } from '@capacitor-firebase/analytics'; +import { BehaviorSubject } from 'rxjs'; + + +export type NotificationCategory = + | 'lightning' + | 'openSpace' + | 'emergency' + | 'announcements' + | 'scheduleChanges' + | 'dailyDigest'; + +export interface NotificationPrefs { + lightning: boolean; + openSpace: boolean; + emergency: boolean; + announcements: boolean; + scheduleChanges: boolean; + dailyDigest: boolean; +} + +interface ScheduledReminder { + id: number; + category: 'lightning' | 'openSpace'; + title: string; + body: string; + fireAt: Date; +} + +const PREF_KEY = 'notification_prefs'; + +// Default: opt-in by default for the conference. Users who don't want them +// can flip the toggles off in Settings. +const DEFAULT_PREFS: NotificationPrefs = { + lightning: true, + openSpace: true, + emergency: true, + announcements: true, + scheduleChanges: true, + dailyDigest: true, +}; + +// Map of toggle category → FCM topic name. Only push-driven categories +// appear here; client-scheduled local notifications (lightning, openSpace) +// are absent. Topic strings must be alphanumeric + dashes/underscores per +// FCM rules. Send pushes from Firebase Console → New campaign → +// Notifications → Target → Topic → enter the topic name. +const TOPIC_BY_CATEGORY: Partial> = { + emergency: 'emergency', + announcements: 'announcements', + scheduleChanges: 'schedule-changes', + dailyDigest: 'daily-digest', +}; + +// All times PDT (America/Los_Angeles, UTC-7). Reminders fire 15 minutes +// before the signup window opens. Edit this table if the published +// signup times change — the service rebuilds its schedule from this list +// each time `applyPrefs()` runs. +// +// PyCon US 2026 calendar: Thu = May 14, Fri = May 15, Sat = May 16, Sun = May 17. +// (PYMOBIL-106's issue body had off-by-one dates — fixed here.) +// Fri/Sat open-space times were flagged as needing verification by staff; +// the 5:00 AM placeholder kept here so the infra ships — adjust if staff +// publish different times. +const REMINDERS: Array> = [ + // Lightning talks + { + category: 'lightning', + title: 'Lightning Talk sign-ups open soon', + body: 'Friday morning slot — signup opens at 9:00 AM, deadline 1:00 PM.', + fireAt: new Date('2026-05-15T08:45:00-07:00'), + }, + { + category: 'lightning', + title: 'Lightning Talk sign-ups open soon', + body: 'Friday evening slot — signup opens at 5:00 AM, deadline 9:00 AM.', + fireAt: new Date('2026-05-15T04:45:00-07:00'), + }, + { + category: 'lightning', + title: 'Lightning Talk sign-ups open soon', + body: 'Saturday morning slot — signup opens at 9:00 AM, deadline 1:00 PM.', + fireAt: new Date('2026-05-16T08:45:00-07:00'), + }, + { + category: 'lightning', + title: 'Lightning Talk sign-ups open soon', + body: 'Saturday afternoon slot — signup opens at 5:00 AM, deadline 9:00 AM.', + fireAt: new Date('2026-05-16T04:45:00-07:00'), + }, + // Open spaces + { + category: 'openSpace', + title: 'Open Space sign-ups open soon', + body: 'Thursday slots open at 5:00 AM PDT.', + fireAt: new Date('2026-05-14T04:45:00-07:00'), + }, + { + category: 'openSpace', + title: 'Open Space sign-ups open soon', + body: 'Friday slots open at 5:00 AM PDT.', + fireAt: new Date('2026-05-15T04:45:00-07:00'), + }, + { + category: 'openSpace', + title: 'Open Space sign-ups open soon', + body: 'Saturday slots open at 5:00 AM PDT.', + fireAt: new Date('2026-05-16T04:45:00-07:00'), + }, +]; + +// Stable IDs in the 8000–8099 range. Local-notification IDs must be 32-bit +// signed ints; using a deterministic offset means rescheduling cancels the +// prior copy instead of stacking duplicates. +const REMINDER_ID_BASE = 8000; + +@Injectable({ providedIn: 'root' }) +export class NotificationsService { + private prefs: NotificationPrefs = { ...DEFAULT_PREFS }; + private storageReady: Promise; + + // Surfaces the current FCM registration token so the Notifications page + // can let staff copy it out for testing — Web Inspector is unavailable on + // App Store builds, so without this they have no way to retrieve it. + // Updated whenever the OS fires the registration event. + readonly fcmToken$ = new BehaviorSubject(null); + + constructor( + private storage: Storage, + private platform: Platform, + private toastCtrl: ToastController, + ) { + this.storageReady = this.storage.create().then(() => undefined); + this.attachTokenListener(); + } + + private attachTokenListener() { + if (!this.platform.is('hybrid')) return; + FirebaseMessaging.addListener('tokenReceived', (event) => { + if (event?.token) this.fcmToken$.next(event.token); + }).catch(() => undefined); + // tokenReceived only fires on rotation; on a fresh launch we may + // already have a token cached server-side. Pull it eagerly so the + // Diagnostics card on the Notifications page shows it immediately + // instead of waiting for the next rotation. + FirebaseMessaging.getToken() + .then((res) => { + if (res?.token) this.fcmToken$.next(res.token); + }) + .catch(() => undefined); + } + + async getPrefs(): Promise { + await this.storageReady; + const saved = await this.storage.get(PREF_KEY); + if (saved && typeof saved === 'object') { + this.prefs = { ...DEFAULT_PREFS, ...saved }; + } + return { ...this.prefs }; + } + + async setPref(key: NotificationCategory, value: boolean): Promise { + await this.storageReady; + this.prefs = { ...this.prefs, [key]: value }; + await this.storage.set(PREF_KEY, this.prefs); + const topic = TOPIC_BY_CATEGORY[key]; + if (topic) { + await this.syncTopic(topic, value); + } + await this.applyPrefs(); + this.logToggle(key, value); + this.setUserProperty(key, value); + } + + // Bulk setter for the "Mute all" / "Enable all" master control. Flips + // every category to the given value, persists once, then runs a single + // applyPrefs (which handles topic + local-notification reconciliation). + async setAllPrefs(value: boolean): Promise { + await this.storageReady; + const next: NotificationPrefs = { ...this.prefs }; + (Object.keys(next) as NotificationCategory[]).forEach((k) => { + next[k] = value; + }); + this.prefs = next; + await this.storage.set(PREF_KEY, this.prefs); + await this.applyPrefs(); + this.logEvent('toggle_all_notifications', { enabled: value }); + (Object.keys(this.prefs) as NotificationCategory[]).forEach((k) => { + this.setUserProperty(k, value); + }); + } + + // Push the current pref state into Firebase user properties so audience + // segmentation in the Firebase / GA console reports today's snapshot, + // not just deltas. Called from setPref/setAllPrefs (above) and once on + // startup (in applyPrefs) so pre-existing installs report state too. + private snapshotPrefsToAnalytics(): void { + (Object.keys(this.prefs) as NotificationCategory[]).forEach((k) => { + this.setUserProperty(k, this.prefs[k]); + }); + } + + private logToggle(key: NotificationCategory, value: boolean): void { + this.logEvent('toggle_notification', { + // Stick with snake_case keys — Firebase Analytics' default schema. + category: key, + enabled: value, + }); + } + + private logEvent(name: string, params: Record): void { + if (!this.platform.is('hybrid')) return; + FirebaseAnalytics.logEvent({ name, params }).catch((err) => { + console.warn(`FirebaseAnalytics.logEvent(${name}) failed`, err); + }); + } + + private setUserProperty(key: NotificationCategory, value: boolean): void { + if (!this.platform.is('hybrid')) return; + // User-property keys must be ≤24 chars and start with a letter; the + // `notif_` prefix keeps them grouped in Firebase Analytics' UI. + FirebaseAnalytics.setUserProperty({ + key: `notif_${key}`.slice(0, 24), + value: value ? 'on' : 'off', + }).catch(() => undefined); + } + + // Subscribe/unsubscribe the device from an FCM topic so the toggle + // actually opts the user out of OS-level pushes — not just the in-app + // banner. Idempotent; safe to call repeatedly. No-op on web. + private async syncTopic(topic: string, enabled: boolean): Promise { + if (!this.platform.is('hybrid')) return; + try { + if (enabled) { + await FirebaseMessaging.subscribeToTopic({ topic }); + } else { + await FirebaseMessaging.unsubscribeFromTopic({ topic }); + } + } catch (err) { + console.warn(`NotificationsService: topic sync failed for ${topic}`, err); + } + } + + // Re-evaluate scheduled local notifications against the current prefs. + // Called on app startup and after every toggle change. Push-driven + // categories (emergency) don't need rescheduling — they're filtered at + // receive time in handleEmergencyPush(). + async applyPrefs(): Promise { + if (!this.platform.is('hybrid')) return; + // Snapshot current prefs into Firebase Analytics user properties on + // every cold-start apply — Audiences / segments in the Firebase + // console will reflect today's state for every active install, not + // just users who flip a toggle. + this.snapshotPrefsToAnalytics(); + // Re-assert FCM topic subscriptions on every apply — covers cases + // where the device's topic state drifts from prefs (fresh install, + // token rotation, app reinstall) by always pushing local state up to + // Firebase. + for (const [category, topic] of Object.entries(TOPIC_BY_CATEGORY)) { + if (!topic) continue; + const enabled = this.prefs[category as NotificationCategory]; + await this.syncTopic(topic, enabled); + } + try { + const granted = await this.ensurePermission(); + if (!granted) return; + + // Cancel anything we previously scheduled. Safe to call with IDs that + // aren't currently scheduled — the plugin no-ops them. + const allIds = REMINDERS.map((_, idx) => ({ id: REMINDER_ID_BASE + idx })); + await LocalNotifications.cancel({ notifications: allIds }); + + const now = Date.now(); + const toSchedule: ScheduleOptions['notifications'] = []; + REMINDERS.forEach((r, idx) => { + if (!this.prefs[r.category]) return; + if (r.fireAt.getTime() <= now) return; // skip past windows + toSchedule.push({ + id: REMINDER_ID_BASE + idx, + title: r.title, + body: r.body, + schedule: { at: r.fireAt, allowWhileIdle: true }, + }); + }); + if (toSchedule.length > 0) { + await LocalNotifications.schedule({ notifications: toSchedule }); + } + } catch (err) { + console.warn('NotificationsService: applyPrefs failed', err); + } + } + + // With FCM topics handling opt-out at the server level, devices that + // have toggled a category off won't receive the push at all. We keep + // this check as a safety net for pushes that staff send to "all + // devices" rather than via a topic — those still reach everyone, so + // we suppress the in-app banner if every push category is disabled. + shouldShowPushBanner(): boolean { + return ( + this.prefs.emergency || + this.prefs.announcements || + this.prefs.scheduleChanges || + this.prefs.dailyDigest + ); + } + + private async ensurePermission(): Promise { + let status: PermissionStatus; + try { + status = await LocalNotifications.checkPermissions(); + } catch { + return false; + } + if (status.display === 'granted') return true; + if (status.display === 'denied') return false; + try { + const requested = await LocalNotifications.requestPermissions(); + return requested.display === 'granted'; + } catch { + return false; + } + } +}