Add Clean Air Forum selfie filter, sticker, and wall display#3753
Add Clean Air Forum selfie filter, sticker, and wall display#37532phonebabykeem wants to merge 5 commits into
Conversation
…nd IG sticker Lets Clean Air Forum attendees generate a branded selfie filter or transparent Instagram sticker from their live AQI reading and optionally submit it to the conference wall display, so the feature drives visible engagement at the booth. - Filter/sticker/share-card templates match the approved Figma spec exactly (proportional scaling off a 1080px reference so text/branding stay pixel-accurate at any capture resolution) - Live camera screen overlays the filter chrome so users can frame their shot before capturing, falling back to the system camera or gallery - Share sheet, selfie-source picker, and consent toggle reuse the Exposure/ Learn design tokens (buttons, switch, drag-handle chrome) for visual consistency with the rest of the app instead of one-off styling - Copies the sticker straight to the clipboard (via `pasteboard`) so it can be pasted into an Instagram Story without a separate save/share step - Card and filter shares now include the app link in the share text for conversion tracking Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backs the mobile app's new selfie filter feature with a `/selfies` wall page for the conference venue display (auto-polling grid, double-tap + staff PIN to moderate) and a temporary in-memory submissions API, so the feature is demoable end-to-end before the real AirQo backend has equivalent endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds Clean Air Forum selfie sharing on mobile and a website wall for viewing, polling, and PIN-protected hiding of submissions, plus shared config, auth, and submission plumbing across both apps. ChangesMobile Clean Air Forum sharing
Estimated code review effort: 4 (Complex) | ~75 minutes Website selfies wall
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ShareSheet as AirQualityShareSheet
participant CameraScreen as CleanAirForumCameraScreen
participant ShareService as AirQualityShareService
participant SubmissionService as CleanAirForumSubmissionService
participant WallAPI as Selfies API
User->>ShareSheet: Open filter tab, tap "Take selfie"
ShareSheet->>CameraScreen: Push CleanAirForumCameraScreen
CameraScreen-->>ShareSheet: Return captured File
User->>ShareSheet: Tap "Share"
ShareSheet->>ShareSheet: _captureBoundary (PNG bytes)
ShareSheet->>ShareService: shareCleanAirForumFilter(bytes, measurement)
ShareService-->>User: Native share sheet
ShareSheet->>SubmissionService: submitSelfie(bytes, measurement) [if consent]
SubmissionService->>SubmissionService: uploadToCloudinary(bytes)
SubmissionService->>WallAPI: POST /api/clean-air-forum/selfies
WallAPI-->>SubmissionService: 201 submission
SubmissionService-->>ShareSheet: success/failure
sequenceDiagram
participant Visitor
participant WallPage as SelfiesWallPage
participant WallAPI as Selfies API
participant Store as cleanAirForumSelfiesStore
Visitor->>WallPage: Load /selfies
loop Poll interval
WallPage->>WallAPI: GET /api/clean-air-forum/selfies
WallAPI->>Store: listSubmissions(eventId)
Store-->>WallAPI: active submissions
WallAPI-->>WallPage: JSON submissions
end
Visitor->>WallPage: Double-tap or long-press photo
WallPage->>WallPage: Show PIN modal
Visitor->>WallPage: Confirm removal with PIN
WallPage->>WallAPI: PATCH /api/clean-air-forum/selfies/{id} {pin}
WallAPI->>Store: hideSubmission(id)
Store-->>WallAPI: updated submission or null
WallAPI-->>WallPage: 200/401/404
WallPage-->>Visitor: Remove tile or show PIN error
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
New azure website changes available for preview here |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart (1)
39-57: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winMissing
didUpdateWidgetto handle a changedfocusNode.Disposal logic correctly skips external nodes, but there's no
didUpdateWidgetoverride to re-subscribe_handleFocusChangeifwidget.focusNodechanges identity across rebuilds (e.g., swapped to a different external node, or toggled between external/internal). The listener attached ininitStatestays on the old node, so focus changes on the new node silently stop updating_focused/border styling.No current caller passes
focusNode(peredit_place_sheet.dart,label_picker_sheet.dart,learn_quiz_free_text.dart), so this is latent today, but worth closing before other call sites adopt the new parameter.♻️ Suggested fix
`@override` void initState() { super.initState(); _focusNode.addListener(_handleFocusChange); } + `@override` + void didUpdateWidget(covariant ExposurePlaceNameTextField oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.focusNode != widget.focusNode) { + final oldNode = oldWidget.focusNode ?? _internalFocusNode; + oldNode?.removeListener(_handleFocusChange); + if (oldWidget.focusNode == null) { + _internalFocusNode?.dispose(); + _internalFocusNode = null; + } + _focusNode.addListener(_handleFocusChange); + } + } + void _handleFocusChange() { setState(() => _focused = _focusNode.hasFocus); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart` around lines 39 - 57, The focus listener setup in `ExposurePlaceNameTextField` only subscribes once in `initState`, so `_handleFocusChange` will keep watching the old node if `widget.focusNode` changes or switches between external and internal nodes. Add a `didUpdateWidget` override to detect `focusNode` identity changes, remove the listener from the previous node, attach it to the new one, and keep `_focused` in sync. Make sure the change is localized around `_focusNode`, `initState`, `_handleFocusChange`, and `dispose` so the subscription lifecycle stays correct.src/website/src/services/cleanAirForumSelfiesStore.ts (1)
60-68: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueBare
crypto.randomUUID()relies on the runtime polyfill.
globalThis.cryptoisn't native in Node.js 18 (only Node 19+); Next.js polyfills it for the server runtime, so this should work in practice, but it's worth confirming the deployment's Node/Next combination guarantees the polyfill rather than depending on it implicitly.🛡️ Optional defensive import
+import { randomUUID } from 'node:crypto'; + export function createSubmission( input: CreateSubmissionInput, ): CleanAirForumSelfieSubmission { const submission: CleanAirForumSelfieSubmission = { ...input, - id: crypto.randomUUID(), + id: randomUUID(),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/website/src/services/cleanAirForumSelfiesStore.ts` around lines 60 - 68, The createSubmission helper currently uses bare crypto.randomUUID(), which depends on an implicit runtime polyfill. Update the CleanAirForumSelfieSubmission ID generation in createSubmission to use an explicit, guaranteed source of randomness or a defensive import/polynomial-safe approach, and ensure the code path is clearly tied to the runtime support assumptions so it does not rely on globalThis.crypto being present implicitly.src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart (1)
62-64: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDecode the selfie at display size instead of full resolution.
Image.file(selfieFile!, fit: BoxFit.cover)decodes the full camera/gallery resolution just to render inside a bounded, RepaintBoundary-captured card. ConsidercacheWidth/cacheHeightscaled to the rendered card size to cut memory pressure, especially since this widget gets rebuilt for both live preview and share export.🖼️ Proposed fix
selfieFile != null - ? Image.file(selfieFile!, fit: BoxFit.cover) + ? Image.file( + selfieFile!, + fit: BoxFit.cover, + cacheWidth: kCafReferenceWidth.round(), + ) : _SelfiePlaceholder(scale: scale),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart` around lines 62 - 64, The selfie image in the clean_air_forum_filter_card widget is being decoded at full source resolution via Image.file, which is unnecessary for the bounded card and share capture use case. Update the image rendering in the selfie branch to pass cacheWidth and/or cacheHeight based on the rendered card size (using the existing scale/layout context in this widget) so the decoded bitmap matches display size. Keep the change localized to the selfie image path in the dashboard widget and preserve the current fit/placeholder behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart`:
- Around line 79-96: Clear the camera controller when the app becomes inactive
so rebuilds do not try to render CameraPreview from a disposed instance. Update
the lifecycle handling in CleanAirForumCameraScreen so the
AppLifecycleState.inactive path nulls _controller (or uses an explicit active
flag) before dispose runs, and ensure _openCamera still assigns and initializes
a fresh CameraController before resuming preview.
In
`@src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart`:
- Around line 83-120: The Cloudinary upload in _uploadToCloudinary uses an
exposed unsigned preset, which can be reused from the mobile binary; update the
flow so the app does not depend on a freely reusable preset. Either switch
CleanAirForumSubmissionService to call a backend endpoint that generates signed
Cloudinary uploads, or keep the preset only if it is locked down server-side to
the strictest allowed folder, file type, and source restrictions.
In `@src/website/.env.sample`:
- Around line 8-18: The new website config variables are present in the sample
env file but are not passed through deployment, so update the relevant workflow
env blocks to include CLEAN_AIR_FORUM_WALL_PIN and
NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID. Locate the website GitHub workflow files
under .github/workflows and add these variables alongside the existing website
env entries so the deploy jobs and runtime config stay in sync with the
.env.sample surface.
In `@src/website/src/app/api/clean-air-forum/selfies/`[id]/route.ts:
- Around line 19-27: The PIN validation in the route handler currently skips
auth when CLEAN_AIR_FORUM_WALL_PIN is unset, which can leave the PATCH path
effectively open. Update the logic in the selfie route handler to fail closed in
production by rejecting requests when the env var is missing (or at minimum
emitting a clear warning), while keeping the existing PIN comparison behavior in
the request-processing code that uses request.json() and NextResponse.json().
In `@src/website/src/app/api/clean-air-forum/selfies/route.ts`:
- Around line 35-79: The POST handler in selfied route currently accepts any
non-empty imageUrl, which leaves the public write endpoint open to abuse. Update
POST to validate imageUrl as a well-formed https URL and, if possible, restrict
it to the expected upload host before calling createSubmission; also add basic
per-IP rate limiting/throttling in the same route or shared middleware to reduce
spam. Use the existing POST, request.json, and createSubmission flow as the main
place to enforce these checks.
---
Nitpick comments:
In `@src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart`:
- Around line 62-64: The selfie image in the clean_air_forum_filter_card widget
is being decoded at full source resolution via Image.file, which is unnecessary
for the bounded card and share capture use case. Update the image rendering in
the selfie branch to pass cacheWidth and/or cacheHeight based on the rendered
card size (using the existing scale/layout context in this widget) so the
decoded bitmap matches display size. Keep the change localized to the selfie
image path in the dashboard widget and preserve the current fit/placeholder
behavior.
In `@src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart`:
- Around line 39-57: The focus listener setup in `ExposurePlaceNameTextField`
only subscribes once in `initState`, so `_handleFocusChange` will keep watching
the old node if `widget.focusNode` changes or switches between external and
internal nodes. Add a `didUpdateWidget` override to detect `focusNode` identity
changes, remove the listener from the previous node, attach it to the new one,
and keep `_focused` in sync. Make sure the change is localized around
`_focusNode`, `initState`, `_handleFocusChange`, and `dispose` so the
subscription lifecycle stays correct.
In `@src/website/src/services/cleanAirForumSelfiesStore.ts`:
- Around line 60-68: The createSubmission helper currently uses bare
crypto.randomUUID(), which depends on an implicit runtime polyfill. Update the
CleanAirForumSelfieSubmission ID generation in createSubmission to use an
explicit, guaranteed source of randomness or a defensive import/polynomial-safe
approach, and ensure the code path is clearly tied to the runtime support
assumptions so it does not rely on globalThis.crypto being present implicitly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 89b8b113-e776-466b-8cbe-ef15fcebd06e
⛔ Files ignored due to path filters (6)
src/mobile/assets/icons/camera.svgis excluded by!**/*.svgsrc/mobile/assets/icons/check-circle.svgis excluded by!**/*.svgsrc/mobile/assets/icons/copy.svgis excluded by!**/*.svgsrc/mobile/assets/icons/gallery.svgis excluded by!**/*.svgsrc/mobile/assets/images/shared/airqo_icon_mark.svgis excluded by!**/*.svgsrc/mobile/pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (21)
src/mobile/android/app/build.gradlesrc/mobile/android/app/src/main/AndroidManifest.xmlsrc/mobile/android/app/src/main/res/xml/provider_paths.xmlsrc/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dartsrc/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dartsrc/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dartsrc/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dartsrc/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dartsrc/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dartsrc/mobile/lib/src/app/dashboard/widgets/clean_air_forum_sticker_frame.dartsrc/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dartsrc/mobile/lib/src/app/shared/services/air_quality_share_service.dartsrc/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dartsrc/mobile/pubspec.yamlsrc/website/.env.samplesrc/website/src/app/api/clean-air-forum/selfies/[id]/route.tssrc/website/src/app/api/clean-air-forum/selfies/route.tssrc/website/src/app/selfies/page.tsxsrc/website/src/configs/cleanAirForumConfig.tssrc/website/src/services/cleanAirForumSelfiesStore.tssrc/website/src/views/selfies/SelfiesWallPage.tsx
| # Shared PIN required to remove a photo from the /selfies conference wall. | ||
| # Leave unset locally to skip the PIN check during development. | ||
| CLEAN_AIR_FORUM_WALL_PIN= | ||
|
|
||
| # Client-side variables (these need NEXT_PUBLIC_ prefix) | ||
| # Comma-separated site URLs. The first entry is used as the canonical base. | ||
| NEXT_PUBLIC_SITE_URL= | ||
| NEXT_PUBLIC_GA_MEASUREMENT_ID= | ||
| NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN= No newline at end of file | ||
| NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN= | ||
| # Which Clean Air Forum edition's submissions the /selfies wall shows. | ||
| NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID=clean-air-forum No newline at end of file |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether CI workflows reference the newly added env vars.
rg -n 'CLEAN_AIR_FORUM_WALL_PIN|NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID' .github/workflowsRepository: airqo-platform/AirQo-frontend
Length of output: 167
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List workflow files and inspect them for any references or env wiring.
git ls-files '.github/workflows/*' || true
printf '\n---\n'
rg -n 'CLEAN_AIR_FORUM_WALL_PIN|NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID|CLEAN_AIR_FORUM|NEXT_PUBLIC_' .github/workflows || trueRepository: airqo-platform/AirQo-frontend
Length of output: 8126
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find every usage of the new env vars in the repository.
rg -n 'CLEAN_AIR_FORUM_WALL_PIN|NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID' .Repository: airqo-platform/AirQo-frontend
Length of output: 669
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the website workflow env wiring around existing NEXT_PUBLIC_* variables.
for f in \
.github/workflows/deploy-frontend-to-azurestaging.yml \
.github/workflows/deploy-frontend-to-azureprod.yml \
.github/workflows/deploy-frontend-pr-previews.yml \
.github/workflows/deploy-frontend-azurepreview.yml \
.github/workflows/deploy-frontends-to-staging.yml \
.github/workflows/deploy-frontends-to-production.yml
do
echo "===== $f ====="
rg -n -C 3 'NEXT_PUBLIC_|WEBSITE_.*(SECRET|API_URL|OPENCAGE_API_KEY|API_TOKEN|SLACK_WEBHOOK_URL|SLACK_CHANNEL|GOOGLE_SITE_VERIFICATION|HCAPTCHA_SITE_KEY)' "$f" || true
echo
doneRepository: airqo-platform/AirQo-frontend
Length of output: 14495
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,120p' src/website/src/configs/cleanAirForumConfig.ts
printf '\n---\n'
sed -n '1,120p' src/website/src/app/api/clean-air-forum/selfies/[id]/route.tsRepository: airqo-platform/AirQo-frontend
Length of output: 2126
Add the new env vars to the website deployment workflows
CLEAN_AIR_FORUM_WALL_PIN and NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID are present in src/website/.env.sample, but they still aren’t wired through the website CI/deploy workflows. Add them to the relevant .github/workflows/*.yml env blocks so the deployed site matches the new config surface.
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 10-10: [UnorderedKey] The CLEAN_AIR_FORUM_WALL_PIN key should go before the GOOGLE_SITE_VERIFICATION key
(UnorderedKey)
[warning] 15-15: [UnorderedKey] The NEXT_PUBLIC_GA_MEASUREMENT_ID key should go before the NEXT_PUBLIC_SITE_URL key
(UnorderedKey)
[warning] 16-16: [UnorderedKey] The NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN key should go before the NEXT_PUBLIC_SITE_URL key
(UnorderedKey)
[warning] 18-18: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
[warning] 18-18: [UnorderedKey] The NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID key should go before the NEXT_PUBLIC_GA_MEASUREMENT_ID key
(UnorderedKey)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/website/.env.sample` around lines 8 - 18, The new website config
variables are present in the sample env file but are not passed through
deployment, so update the relevant workflow env blocks to include
CLEAN_AIR_FORUM_WALL_PIN and NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID. Locate the
website GitHub workflow files under .github/workflows and add these variables
alongside the existing website env entries so the deploy jobs and runtime config
stay in sync with the .env.sample surface.
Source: Coding guidelines
|
@2phonebabykeem handle coderabbit comments |
There was a problem hiding this comment.
Pull request overview
This PR adds an end-to-end “Clean Air Forum” selfie sharing flow across the Flutter mobile app and the Next.js website, including a conference wall display fed by a temporary in-memory API.
Changes:
- Mobile: adds a Clean Air Forum branded selfie filter (live camera preview + composited share image) and a transparent sticker that can be copied to the clipboard.
- Mobile: updates share text to include an AirQo app link and refactors card rendering helpers for consistent color/label/text handling.
- Website: adds a
/selfieswall display page with polling + PIN-gated moderation, backed by temporary in-memory API routes and config/env wiring.
Reviewed changes
Copilot reviewed 21 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/website/src/views/selfies/SelfiesWallPage.tsx | Client wall UI: polling grid, double-tap moderation modal, and removal flow. |
| src/website/src/services/cleanAirForumSelfiesStore.ts | Temporary in-memory submissions store (list/create/hide) to demo wall end-to-end. |
| src/website/src/configs/cleanAirForumConfig.ts | Shared config for “current event” ID and wall polling interval. |
| src/website/src/app/selfies/page.tsx | Next.js route wrapper for /selfies with noindex metadata. |
| src/website/src/app/api/clean-air-forum/selfies/route.ts | Temporary API for listing/creating wall submissions. |
| src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts | Temporary API for PIN-gated hiding (soft-removing) a submission. |
| src/website/.env.sample | Adds sample env vars for wall PIN and event ID. |
| src/mobile/pubspec.yaml | Adds camera + pasteboard dependencies required for live capture and clipboard sticker copy. |
| src/mobile/pubspec.lock | Updates dependency lockfile reflecting new packages and resolved SDK minimums. |
| src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart | Uploads composited image to Cloudinary and submits metadata to the website’s temporary API. |
| src/mobile/lib/src/app/shared/services/air_quality_share_service.dart | Adds tracked app link to share text; new share helpers for filter/sticker images. |
| src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart | Allows injecting a FocusNode from callers (for keyboard/focus management). |
| src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_sticker_frame.dart | New transparent, square sticker widget that renders AQI/location info. |
| src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart | New selfie + branded overlay card widget intended for capture/share. |
| src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart | New live camera preview screen with overlaid guide chrome for framing. |
| src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart | Expands share sheet into tabs (Forum filter/Card/Sticker), adds camera/gallery selection, consent-gated wall submission, and sticker clipboard copy. |
| src/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dart | Refactors card rendering to reuse shared sanitization and AQI helpers; tweaks styling for consistency. |
| src/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dart | Adds CAF brand constants and shared header/icon widgets for templates. |
| src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart | New shared helpers for sanitization, AQI color/label/icon, and pill backgrounds. |
| src/mobile/assets/images/shared/airqo_icon_mark.svg | Adds AirQo mark asset used in CAF templates. |
| src/mobile/assets/icons/gallery.svg | Adds gallery icon used in selfie source sheet. |
| src/mobile/assets/icons/copy.svg | Adds copy icon used for sticker clipboard CTA. |
| src/mobile/assets/icons/check-circle.svg | Adds check icon used for “Copied!” state. |
| src/mobile/assets/icons/camera.svg | Adds camera icon used across CAF capture/share UI. |
| src/mobile/android/app/src/main/res/xml/provider_paths.xml | Adds FileProvider paths needed by pasteboard clipboard image writes. |
| src/mobile/android/app/src/main/AndroidManifest.xml | Adds CAMERA permission/features and a FileProvider for pasteboard. |
| src/mobile/android/app/build.gradle | Updates Android build settings (pins NDK version and raises minSdkVersion). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| FocusNode get _focusNode => widget.focusNode ?? (_internalFocusNode ??= FocusNode()); | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _focusNode = FocusNode(); | ||
| _focusNode.addListener(() { | ||
| setState(() => _focused = _focusNode.hasFocus); | ||
| }); | ||
| _focusNode.addListener(_handleFocusChange); | ||
| } |
| @override | ||
| void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| final controller = _controller; | ||
| if (controller == null || !controller.value.isInitialized) return; | ||
|
|
||
| if (state == AppLifecycleState.inactive) { | ||
| controller.dispose(); | ||
| } else if (state == AppLifecycleState.resumed) { | ||
| _openCamera(_cameraIndex); | ||
| } | ||
| } |
| final cloudName = dotenv.env['NEXT_PUBLIC_CLOUDINARY_NAME'] ?? ''; | ||
| final uploadPreset = dotenv.env['NEXT_PUBLIC_CLOUDINARY_PRESET'] ?? ''; | ||
| final cloudinaryUrl = | ||
| 'https://api.cloudinary.com/v1_1/$cloudName/image/upload'; | ||
|
|
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ pin }), | ||
| }, |
| const body = await request | ||
| .json() | ||
| .catch(() => ({}) as Record<string, unknown>); | ||
| const expectedPin = process.env.CLEAN_AIR_FORUM_WALL_PIN; | ||
|
|
||
| if (expectedPin && body?.pin !== expectedPin) { | ||
| return NextResponse.json({ error: 'Incorrect PIN' }, { status: 401 }); | ||
| } |
| // You can update the following values to match your application needs. | ||
| // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. | ||
| minSdkVersion flutter.minSdkVersion | ||
| minSdkVersion 24 |
| namespace "com.airqo.app" | ||
| compileSdk 36 | ||
| ndkVersion flutter.ndkVersion | ||
| ndkVersion "27.0.12077973" |
Fixes a CodeRabbit finding: didChangeAppLifecycleState disposed the camera controller on AppLifecycleState.inactive but never cleared the field, so a rebuild before _openCamera() finished on resume could still try to paint CameraPreview against an already-disposed controller. Now nulls the field (and rebuilds) before disposing. Also sends the new x-clean-air-forum-secret header (from CLEAN_AIR_FORUM_API_SECRET) on wall submissions, matching the website's new shared-secret check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wall page used generic black/white styling with no AirQo branding, all active submissions in one long scrolling grid, and a POST endpoint that accepted any imageUrl with no auth — all flagged by the user and CodeRabbit as needing fixes before this goes live at the event. - Wall page now uses the site's real logo and brand blue (#145DFF) instead of generic dark-UI colors, and shows 8 selfies at a time in an auto-advancing slideshow (framer-motion crossfade + page dots) rather than everything at once - Long-press added alongside double-tap for moderation, since a wall display has no cursor to hover/right-click with - POST now requires a shared secret header from the mobile app (x-clean-air-forum-secret) and validates imageUrl actually points at the Cloudinary folder the app uploads to, closing the "anyone who finds this endpoint can push arbitrary images onto the public wall" gap - Both the submission secret and moderation PIN checks now fail closed (with a logged warning) if unset in production, instead of silently leaving the endpoint open — they still skip the check in dev/preview - Added a best-effort per-IP rate limit (5/minute) on submissions to blunt spam before staff can react Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents what this feature still needs from other teams before it's production-grade: real backend endpoints to replace the temporary in-memory store, Cloudinary upload-preset hardening, the new env vars/secrets that need wiring into the deploy workflows (left as docs rather than editing the workflow YAML directly), and a note to confirm the hardcoded event dates before launch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
New azure website changes available for preview here |
Baalmart
left a comment
There was a problem hiding this comment.
Hi @2phonebabykeem and @Mozart299 I am seeing some new changes to the official WEBSITE project, are these accidental or intentional? cc: @OchiengPaul442
OchiengPaul442
left a comment
There was a problem hiding this comment.
@2phonebabykeem Please keep the mobile app and website changes in separate PRs to maintain clear separation of concerns and avoid mixing unrelated changes.
|
@OchiengPaul442 @Baalmart Understood, will fix this |
|
Splitting this into two separate PRs to keep mobile and website changes cleanly separated, per team convention:
Both include everything from this PR plus the CodeRabbit/Copilot review fixes. Closing this one in favor of those two. |
Summary
/selfieswall display page for the conference venue screen (auto-polling grid, double-tap + staff PIN to moderate) backed by a temporary in-memory API — stood up so the feature is demoable end-to-end before the real AirQo backend has equivalent endpoints (noted inline as TEMPORARY, with the request/response shapes designed to carry over unchanged).Test plan
flutter analyzeclean on all touched mobile files/selfieswall page against a live submission end-to-end (mobile → Cloudinary → website API → wall page)CleanAirForumBrand) before the forum🤖 Generated with Claude Code
Summary by CodeRabbit