diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfbea4..dabc565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ - Edit profile screen with support for updating display name, bio, images, pronouns, and website - Display pronouns and website (with link to browser) on profile screens +- English localization foundation and expanded localized UI coverage. ### Changed @@ -208,3 +209,10 @@ TODO #### 2026-05-01 - Separate views for local and protocol-level saved/bookmarked posts. +- Local notification UI and unread-count badges. +- Push notification registration and delivery flow. + +#### 2026-05-03 + +- Firebase push-notification configuration for iOS and Android. +- Notification reason handling and deep links for notification taps. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9e45a7d..83cfae2 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -81,7 +81,7 @@ Use `just` for common workflows: | `just check` | Format, lint, and test in sequence | For release versioning, signing, packaging, and distribution, see the -[release documentation](docs/release.md). +[release documentation](docs/dev/release.md). ## Website diff --git a/docs/README.md b/docs/README.md index 74ae45c..2471a10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1 +1,16 @@ # Lazurite Project Documentation + +- `docs/dev/` contains maintainer-facing explanations for implemented systems. +- `docs/specs/` is reserved for active or proposed designs that are not yet part + of the developer guide. +- `docs/tasks/` tracks unfinished milestone work. Completed milestones should + move to `CHANGELOG.md` and, when useful, `docs/dev/`. +- `docs/designs/` contains UI wireframes and design references. +- `docs/TODO.md` is the parking lot for smaller ideas and follow-ups. + +Start with: + +- [App foundation](./dev/foundation.md) +- [Patterns](./dev/patterns.md) +- [Routing](./dev/routing.md) +- [Release guide](./dev/release.md) diff --git a/docs/TODO.md b/docs/TODO.md index 7b6527c..b5d6d25 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,6 +1,6 @@ --- title: To-Do/Parking Lot -updated: 2026-05-16 +updated: 2026-05-23 --- ## Tests @@ -47,6 +47,43 @@ updated: 2026-05-16 ## Enhancements +### Relational Discovery + +- Post threads: show quote posts for the focused post. + - Discover candidate posts from `app.bsky.feed.post` record embeds that point at + the focused post. + - Hydrate through AppView before rendering and apply existing moderation filtering. +- Post/link cards: show more posts about the same link or domain. + - Use post facets/external embeds for URL/domain relationships. + - Rank recent/high-signal posts first and cap repeated authors. +- Profiles: show starter packs featuring the account. + - Use `app.bsky.graph.starterpackitem:subject` relationships. + - Hydrate starter packs before rendering and filter unsafe/unavailable records. +- Profiles: show public lists containing the account. + - Use `app.bsky.graph.listitem:subject` relationships. + - Be careful with abusive list names/descriptions; hydrate/filter before rendering. +- Profiles: show similar accounts. + - Define as accounts often followed by the same people, or accounts followed by + people who follow this account. + - Add ranking dampening so celebrity accounts do not dominate results. +- Post threads: show "Also reposted" posts. + - Define as posts reposted by accounts who reposted the focused post. + - Keep the relationship lookup plus AppView hydration bounded to one page at a time. +- Thread roots: show other active branches in the same conversation. + - Use `app.bsky.feed.post` reply root/parent relationships. + - Rank by recent activity and collapse noisy branches by default. +- Lists/starter packs: show similar lists and similar starter packs by overlapping + members. + - Hydrate records before display and avoid surfacing unavailable/deleted records. +- Profiles: show posts mentioning the account. + - Use mention facets pointing at the account DID. + - Hydrate through AppView, apply existing moderation filtering, then rank for + usefulness. + - MVP ranking guardrails: prefer non-replies, recent/high-engagement posts, cap posts + per author, and deprioritize posts with many mentions or repeated text. + +--- + - Adding `/rss` for public BlueSky profiles shows their profile as an RSS feed. It would be cool to display this and allow exporting the feed or a link to it. - In dev tools, show Firehose, Jetstream, and diff --git a/docs/dev/compose-notifications-actions.md b/docs/dev/compose-notifications-actions.md index 9542a19..a6867bb 100644 --- a/docs/dev/compose-notifications-actions.md +++ b/docs/dev/compose-notifications-actions.md @@ -17,6 +17,8 @@ in the `app.bsky.feed.post` collection. Text length is counted with Dart grapheme clusters, not code units. The submit action is disabled for empty text and over-limit posts. Rich text facets are detected before submission and rendered as a live preview while the user types. +Post editing reuses compose in a restricted edit mode; see +[post-editing.md](./post-editing.md). Images upload through `com.atproto.repo.uploadBlob` and are embedded as `app.bsky.embed.images`. A post may include up to four images. Video upload @@ -31,15 +33,14 @@ returns. ## Notifications -`NotificationBloc` in `lib/features/notifications/bloc` owns polling state. -Polling notifications use `app.bsky.notification.listNotifications`, -`getUnreadCount`, and `updateSeen`. Notifications are grouped by day and render -author, reason, reason icon, read state, and an optional post preview. +`NotificationBloc` in `lib/features/notifications/bloc` owns alerts-screen state. +`NotificationDomainService` owns polling, push-triggered reconcile, delivery +dedupe, and local notification display. See [notifications.md](./notifications.md) +for the full processing model. Foreground unread polling runs on an interval while the app is active. Opening the notifications screen marks current notifications as seen. Tapping a -notification routes to the relevant post or profile. Later push notification -work builds on this navigation and seen-state model. +notification routes to the relevant post or profile. ## Post And Profile Actions diff --git a/docs/dev/internationalization.md b/docs/dev/internationalization.md new file mode 100644 index 0000000..7bda9e1 --- /dev/null +++ b/docs/dev/internationalization.md @@ -0,0 +1,61 @@ +--- +title: Internationalization +updated: 2026-05-24 +--- + +Lazurite uses Flutter `gen_l10n` with ARB files and `intl`. English is the only +shipping locale today, but user-facing strings should still go through the +localization layer so future translations do not require another app-wide pass. + +## Files + +- Source strings: `lib/core/l10n/intl_en.arb` +- Generated API: `lib/core/l10n/app_localizations.dart` and companions +- Helper extension: `lib/core/l10n/l10n.dart` +- Generator config: `l10n.yaml` + +Generated localization files are checked in because app code imports them +through `package:lazurite/core/l10n/app_localizations.dart`. + +## Adding or changing copy + +Use semantic keys, not keys shaped around the exact English text. Prefer prefixes +that describe where the string is used: + +- `button*` for button labels +- `label*` for titles, tabs, short labels, and tooltips +- `message*` for helper or explanatory copy +- `dialog*` for dialog titles and body text +- `error*` and `validation*` for failures +- `format*` for parameterized strings + +Every ARB entry needs metadata because `required-resource-attributes` is enabled. +Parameterized strings need typed placeholders and examples. + +Do not localize protocol constants, route paths, database keys, enum storage +values, asset paths, handles, DIDs, AT URIs, URLs, raw record JSON, or log lines. +Server error details and externally localized moderation labels can remain as +provided by the service. + +## Runtime behavior + +`MaterialApp.router` uses Flutter's default system locale resolution. There is no +persisted in-app language picker yet. Add one only when the app ships more than +one real locale. + +Use `context.l10n` from widgets that have a `BuildContext`. App bootstrap and +other non-widget code may import `AppLocalizations` directly. + +## Tests + +Widget tests for localized screens should pump a `MaterialApp` or +`MaterialApp.router` with: + +```dart +localizationsDelegates: AppLocalizations.localizationsDelegates, +supportedLocales: AppLocalizations.supportedLocales, +``` + +Small widget tests may rely on the English fallback in `context.l10n`, but full +screen tests should include delegates. Keep asserting visible English copy until +another locale is added. diff --git a/docs/dev/notifications.md b/docs/dev/notifications.md new file mode 100644 index 0000000..ecd3561 --- /dev/null +++ b/docs/dev/notifications.md @@ -0,0 +1,62 @@ +--- +title: Notifications +updated: 2026-05-24 +--- + +Lazurite uses the Bluesky notification APIs for the in-app alerts feed, unread +badges, push registration, and canonical notification fetches. Firebase supplies +the platform push token transport. Local notifications render OS-level alerts +after Lazurite validates and filters the notification content. + +## Main pieces + +- `NotificationRepository` wraps `app.bsky.notification.*` calls. +- `NotificationDomainService` owns dedupe, moderation filtering, counters, and + the common reconcile path. +- `PushRegistrationService` registers and unregisters the active account's token. +- `FlutterLocalNotificationAdapter` maps approved notifications to Android + channels, iOS categories, and deep-link payloads. +- `notification_background_worker.dart` contains the Firebase and Workmanager + entrypoints. Keep background handlers top-level and annotated where Flutter + requires it. +- `notification_deliveries` in Drift records delivered notification URIs per + account so polling, push, and background reconcile can share dedupe state. + +## Processing model + +Treat a push payload as a trigger, not display content. The payload parser accepts +`senderDid`, `targetDid`, `recordUri`, and `reason`, then fetches the canonical +notification through the authenticated API. The domain service applies moderation +and preference filters before displaying anything. + +All paths should converge on the same domain service methods: + +- foreground unread polling +- notification screen reconcile +- background reconcile +- Firebase background push handling +- foreground Firebase messages + +This keeps routing, dedupe, and local-notification rendering consistent across app +states. + +## Platform notes + +Android notification delivery needs runtime permission on Android 13 and newer. +Channels are grouped by reason family. Background reconcile uses Workmanager and +therefore follows platform minimum intervals. + +iOS delivery depends on APNs through Firebase. Enable Push Notifications and the +required background modes in Xcode. iOS background execution is opportunistic, so +push handling must stay timeout-bounded and safe to drop. + +## Guidelines + +- Never show local notification content directly from a push payload. +- Keep token registration account-scoped and unregister during logout or account + removal where possible. +- Do not leave background catch blocks empty. Log enough context to diagnose + delivery failures without logging token material. +- Add Drift migrations for any delivery-state schema changes. +- Test dedupe, token lifecycle, payload parsing, and deep-link routing when + changing notification behavior. diff --git a/docs/dev/post-editing.md b/docs/dev/post-editing.md new file mode 100644 index 0000000..1efb206 --- /dev/null +++ b/docs/dev/post-editing.md @@ -0,0 +1,44 @@ +--- +title: Post Editing +updated: 2026-05-24 +--- + +Lazurite implements post editing by replacing the authenticated user's +`app.bsky.feed.post` record at the same rkey. This preserves the AT URI while +letting the user change supported fields. + +## Scope + +The edit entry point is currently thread-only. Compose enters edit mode with the +original post URI, CID, record, and text. Edit mode changes labels to edit copy, +shows the algorithm-impact notice, and disables create-only controls such as +drafts, scheduling, and media changes. + +Editable fields: + +- post text +- regenerated rich-text facets + +Preserved fields: + +- reply refs +- embed +- languages +- labels and tags +- unknown record fields +- `createdAt`, when present and valid + +## Write behavior + +The repository deletes and recreates the record on the original rkey with a +`swapRecord` guard against the current CID. If the guard fails, the app treats the +edit as a conflict and asks the user to reopen the post before retrying. + +If recreate fails after delete, Lazurite attempts to restore the original record +on the same rkey. Keep this recovery path conservative and logged. + +## User-facing caveat + +Editing can affect indexing and distribution. `indexedAt` may change, feeds and +search may lag, and counters can briefly look stale while Bluesky services +reconcile the replacement. Keep this explanation visible in compose edit mode. diff --git a/docs/dev/release.md b/docs/dev/release.md new file mode 100644 index 0000000..beff59d --- /dev/null +++ b/docs/dev/release.md @@ -0,0 +1,100 @@ +--- +title: Release Guide +updated: 2026-05-24 +--- + +Use Android Studio and Xcode for the canonical signed-build flows. They surface +signing, provisioning, bundle ID, capability, and upload problems earlier than a +bare Flutter command. Command-line builds should replicate the same signing +configuration checked in or supplied by local/CI secrets. + +## Release checklist + +1. Confirm toolchain versions: `flutter --version`, `dart --version`, Android + Studio, and Xcode. +2. Set `version:` in `pubspec.yaml` or pass matching build flags. + - build name: public numeric version, for example `1.0.0` + - build number: next store upload integer +3. Update `AppVersion.prereleaseLabel` in + `lib/core/app/app_version.dart` when the UI should show an alpha/beta label. +4. Run gates: + + ```bash + flutter pub get + flutter analyze + gtimeout 1200s flutter test --reporter=failures-only + ``` + +5. Build from the tagged commit and upload the artifacts produced by that commit. + +## Versioning + +Flutter maps `version: 1.0.0+6` as follows: + +- `1.0.0` -> Android `versionName` and iOS `CFBundleShortVersionString` +- `6` -> Android `versionCode` and iOS `CFBundleVersion` + +Keep the build name numeric and App Store safe. Do not use values like +`1.0.0-alpha.6` for iOS `MARKETING_VERSION`. The build number must increase for +every store upload. + +After changing versions, run `flutter pub get`, then build once from Flutter or +an IDE so ignored generated files reflect the new version. + +## Android + +Use Android Studio for release signing setup and Play Console uploads when +possible. Confirm the release variant uses the upload key, not debug signing. + +Command-line equivalent: + +```bash +flutter build appbundle --release \ + --build-name "$FLUTTER_BUILD_NAME" \ + --build-number "$FLUTTER_BUILD_NUMBER" +``` + +Upload `build/app/outputs/bundle/release/app-release.aab` to Play internal +testing first. For non-Play channels, build and sign APKs instead. + +## iOS + +Use Xcode to validate bundle ID, signing team, provisioning profile, entitlements, +and capabilities. Archive from Xcode for the safest App Store Connect flow, or +replicate the same signing setup from the command line. + +Command-line equivalent: + +```bash +flutter build ipa --release \ + --build-name "$FLUTTER_BUILD_NAME" \ + --build-number "$FLUTTER_BUILD_NUMBER" +``` + +Upload `build/ios/ipa/*.ipa` with Xcode Organizer or Transporter, then validate +through TestFlight before review. + +## Direct distribution + +GitHub Releases, AltStore, and Obtainium builds must still come from the tagged +commit and use proper release signing. Attach checksums for downloadable assets: + +```bash +shasum -a 256 build/app/outputs/flutter-apk/*.apk build/ios/ipa/*.ipa > checksums.txt +``` + +AltStore PAL uses Apple's alternative distribution package flow. AltStore Classic +uses a signed IPA and source JSON. Obtainium needs a stable release source with +signed APK download URLs. + +## Firebase push configuration + +Use `.env.example` as the public variable list and keep real values in local or +CI secrets. Firebase files are checked in to VCS, and must remain at: + +- Android: `android/app/google-services.json` +- iOS: `ios/Runner/GoogleService-Info.plist`, included in the Runner target + +In Xcode, enable Push Notifications and Background Modes as required by the +notification implementation. Upload the APNs key or certificate in Firebase +Console before expecting production iOS push delivery. diff --git a/docs/dev/social-features-and-moderation.md b/docs/dev/social-features-and-moderation.md index b5543aa..0db104b 100644 --- a/docs/dev/social-features-and-moderation.md +++ b/docs/dev/social-features-and-moderation.md @@ -91,3 +91,15 @@ record. Starter pack detail screens render creator, description, member sample, feed recommendations, and join counts. Actor profile surfaces can show starter packs created by that actor, and search can route directly to a pack detail screen. + +## Profile Context + +Profile context surfaces public relationship data that helps a user understand an +account before acting on it. Keep the presentation neutral: show lists, blocks, +and related social context without scoring the account or implying intent. + +Constellation-backed lookups should stay behind small repository methods. Hydrate +returned DIDs and records through the normal Bluesky profile/list APIs before +rendering, then apply the same moderation filtering used elsewhere. Keep the +Constellation base URL configurable internally, but avoid exposing it as a casual +end-user setting. diff --git a/docs/release.md b/docs/release.md deleted file mode 100644 index ad42829..0000000 --- a/docs/release.md +++ /dev/null @@ -1,275 +0,0 @@ ---- -title: Release and Distribution Guide -updated: 2026-05-17 ---- - -## Shared Release Baseline - -1. Pin and verify toolchain. - - `flutter --version` - - `dart --version` -2. Set version in `pubspec.yaml` and/or pass build flags. - - `--build-name` => human version (for example `1.4.0`) - - `--build-number` => monotonically increasing integer -3. Run release gates. - - `flutter pub get` - - `flutter analyze` - - `gtimeout 1200s flutter test --reporter=failures-only` -4. Tag immutable source. - - `git tag vX.Y.Z` -5. Build store artifacts from that exact tag/commit. - -## Versioning - -Use `pubspec.yaml` as the tracked source of truth for local builds: - -```yaml -version: 1.0.0+6 -``` - -The part before `+` is Flutter's build name. Keep it numeric and App -Store-safe because iOS maps it to `CFBundleShortVersionString`. Do not put -prerelease text such as `-alpha.6` in the build name or iOS -`MARKETING_VERSION`. - -The part after `+` is Flutter's build number. It maps to Android -`versionCode` and iOS `CFBundleVersion`, so it must be a monotonically -increasing integer for every uploaded build. It is not automatically "commits -since tag." Use the store or CI build sequence as the source of truth. For a -`v1.0.0-alpha.6` tag, build number `6` is valid only if `6` is the next upload -number for that app ID/package. If building from commits after the tag, assign -the next unused build number instead of reusing the tag's ordinal. - -Keep platform files aligned: - -- iOS: update the Runner target `MARKETING_VERSION` in - `ios/Runner.xcodeproj/project.pbxproj` to the numeric public version, for - example `1.0.0`. `Info.plist` should continue to read - `$(FLUTTER_BUILD_NAME)` and `$(FLUTTER_BUILD_NUMBER)`. -- Android: keep `android/app/build.gradle.kts` reading `versionName` and - `versionCode` from Flutter (`flutter.versionName` and `flutter.versionCode`). - -For prerelease UI labels, update `AppVersion.prereleaseLabel` in -`lib/core/app/app_version.dart`. With `version: 1.0.0+6` and label `alpha`, the -app renders `Lazurite v1.0.0 alpha 6`. - -After changing versions, run `flutter pub get`, then build from Flutter or the -IDE once so ignored local generated files such as -`ios/Flutter/Generated.xcconfig` and `android/local.properties` reflect the -current build name and number. - -## Environment Variables - -Use the root `.env.example` as the canonical variable list. Keep real values in untracked secrets (`.env.local`, CI secrets manager, etc.). - -## Google Play (Android) - -### Build - -1. Configure real release signing (upload key), not debug signing. -2. Build AAB (preferred by Google Play): - - ```bash - flutter build appbundle --release \ - --build-name "$FLUTTER_BUILD_NAME" \ - --build-number "$FLUTTER_BUILD_NUMBER" - ``` - -3. Artifact: `build/app/outputs/bundle/release/app-release.aab` - -### Deploy - -1. Enroll in Play App Signing. -2. Upload `app-release.aab` to Internal testing first. -3. Promote to closed/open/production after validation. - -### Notes - -- If you need the same signing key across multiple stores, provide your own app signing key when configuring Play App Signing. -- For non-Play Android channels, ship signed APKs (Play consumes AAB; side channels consume APK). - -## Apple App Store (iOS) - -### Build - -1. Use an explicit App ID + matching bundle ID. -2. Build signed IPA: - - ```bash - flutter build ipa --release \ - --build-name "$FLUTTER_BUILD_NAME" \ - --build-number "$FLUTTER_BUILD_NUMBER" - ``` - -3. Artifact: `build/ios/ipa/*.ipa` - -### Deploy - -1. Upload using Xcode or Transporter to App Store Connect. -2. Wait for processing. -3. Ship through TestFlight (internal/external) first. -4. Submit selected build for App Review. - -### Notes - -- App Store Connect associates build using bundle ID + version + build string. -- As of 2026, Apple requires Xcode 14+ for uploads. - -## AltStore.io - -AltStore distribution has two distinct paths. - -### AltStore PAL (EU marketplace path) - -#### Build/Package - -1. Build iOS release (`flutter build ipa --release`). -2. Submit via App Store Connect with Notarization (or App Store approval, which also results in notarization). -3. Download the Alternative Distribution Package (ADP). -4. Host ADP exactly as-delivered; preserve directory hierarchy and do not modify `manifest.json`. - -#### Deploy - -1. Accept Apple Alternative EU Terms Addendum. -2. Register Developer ID with AltStore PAL API. -3. Add returned marketplace token in App Store Connect Integrations. -4. Publish a Source JSON with required app/version metadata. - -### AltStore Classic (sideloaded IPA path) - -#### Build - -1. Build/sign IPA (`flutter build ipa --release`). -2. Host IPA at stable HTTPS URL. - -#### Deploy - -1. Publish/update Source JSON. -2. Keep newest entry first in `versions` array. -3. Ensure each release updates `version` (`CFBundleShortVersionString`) and/or `buildVersion` (`CFBundleVersion`). -4. Include accurate `downloadURL`, `size`, and optional `minOSVersion` / `maxOSVersion`. - -### Notes - -- AltStore determines latest release by `versions` ordering, not dates. -- AltStore checks declared app permissions/entitlements against downloaded app package. - -## Obtainium (Android direct update channel) - -### Build - -1. Produce signed APK artifacts for direct install. -2. Prefer a stable, machine-discoverable release URL source (typically GitHub Releases). - -### Deploy - -1. Publish release where source exposes: - - version identifier - - at least one APK download URL -2. If multiple APK variants exist, keep filenames explicit (`arm64-v8a`, `universal`, etc.) so users can filter reliably. - -### Notes - -- Obtainium supports GitHub, GitLab, F-Droid repos, direct APK links, and HTML fallback. - -## GitHub Releases - -### Build - -1. Build release artifacts from tagged commit (`vX.Y.Z`). -2. Generate checksums for all distributables. - -Example: - -```bash -shasum -a 256 build/app/outputs/flutter-apk/*.apk build/ios/ipa/*.ipa > checksums.txt -``` - -### Deploy - -1. Create release from tag. -2. Attach binaries (`.aab`, `.apk`, `.ipa`, `checksums.txt`, optional symbols/maps). -3. Use generated release notes, then curate manually. - -CLI example: - -```bash -gh release create "v${FLUTTER_BUILD_NAME}" \ - --generate-notes \ - build/app/outputs/bundle/release/app-release.aab \ - build/app/outputs/flutter-apk/*.apk \ - build/ios/ipa/*.ipa \ - checksums.txt -``` - -### Hardening (Recommended) - -- Add artifact attestations in GitHub Actions for build provenance. -- Keep each release asset < 2 GiB. - -## Firebase Push Notifications (iOS + Android) - -### 1) Firebase Project and App Registration - -1. Install CLI tooling. - - `firebase login` - - `dart pub global activate flutterfire_cli` -2. Run: - - ```bash - flutterfire configure - ``` - -3. Commit generated `lib/firebase_options.dart`. - -### 2) Platform Config Files - -1. Android: place `google-services.json` at `android/app/google-services.json`. -2. iOS: place `GoogleService-Info.plist` at `ios/Runner/GoogleService-Info.plist` and include it in Runner target. - -### 3) Android Gradle Wiring - -1. Add Google services Gradle plugin in project/plugin management. -2. Apply `com.google.gms.google-services` in app module. - -### 4) Apple Push Prerequisites - -1. In Xcode, enable `Push Notifications` capability. -2. In Xcode Background Modes, enable: - - `Background fetch` - - `Remote notifications` -3. Upload APNs auth key (`.p8`, Key ID, Team ID) in Firebase Console > Project Settings > Cloud Messaging. - -### 5) App Initialization and Runtime - -1. Initialize with generated options: - - ```dart - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - ``` - -2. Request user notification permission (iOS) before expecting token delivery. -3. Register and sync FCM token with backend; rotate on refresh. - -## Primary References - -- Flutter Android release: -- Flutter iOS release: -- Apple bundle short version (`CFBundleShortVersionString`): -- Apple build version (`CFBundleVersion`): -- Android app versioning: -- Android signing + Play App Signing: -- Play App Signing help: -- App Store Connect uploads: -- Apple explicit App ID / bundle ID requirements: and -- AltStore PAL distribution: -- AltStore source format: -- AltStore updates/version ordering: -- Obtainium tracking/source behavior: and -- GitHub releases: -- GitHub release notes automation: -- GitHub build provenance (artifact attestations): -- GitHub release asset limits: -- Firebase Flutter setup: -- Firebase FCM Flutter setup: -- Firebase Android config (`google-services.json` + plugin): diff --git a/docs/specs/internationalization.md b/docs/specs/internationalization.md deleted file mode 100644 index 6d20fd4..0000000 --- a/docs/specs/internationalization.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: Internationalization -updated: 2026-05-07 ---- - -## Summary - -Lazurite uses Flutter `gen_l10n` with ARB files and `intl` for user-facing UI -strings. The v1 implementation is English-only, follows the system locale, and -establishes the app-local localization architecture needed for future real -translations. - -## Current State - -V1 localizes the foundation and first user-facing surface: - -- App bootstrap localization delegates and supported locales -- App shell navigation, drawer, common buttons, dialogs, and error states -- Auth/login copy, saved account actions, and legal links -- Settings sections, common settings rows, provider dialogs, and troubleshooting -- Search tabs, common search placeholders, post filters, and starter-pack API - unavailable messaging - -Text intentionally not localized in v1: - -- User-generated post/profile/list/feed content -- Handles, DIDs, AT URIs, URLs, record JSON, and log lines -- Server/API error details and externally localized moderation label values -- Remaining feature surfaces listed in `docs/tasks/internationalization.md` - -## Architecture - -Localization source lives in `lib/core/l10n/intl_en.arb`. `l10n.yaml` configures -Flutter generation: - -- `arb-dir: lib/core/l10n` -- `template-arb-file: intl_en.arb` -- `output-localization-file: app_localizations.dart` -- `nullable-getter: false` -- `use-escaping: true` -- `required-resource-attributes: true` - -Generated localization Dart files are checked in because Lazurite imports -`package:lazurite/core/l10n/app_localizations.dart` directly. Widgets should use -`context.l10n` from `package:lazurite/core/l10n/l10n.dart` when they already -have a `BuildContext`; app bootstrap can import `AppLocalizations` directly. - -`MaterialApp.router` owns locale resolution through Flutter's default system -locale behavior. V1 does not add a persisted language setting or runtime picker. - -## Key Naming Rules - -- Prefer semantic keys over copy-shaped keys: `labelSettings`, not - `settingsText`. -- Use prefixes consistently: - - `button*` for button labels - - `label*` for short labels, titles, tabs, and tooltips - - `message*` for explanatory body text and helper text - - `dialog*` for dialog-specific title/body copy - - `error*` and `validation*` for failure copy - - `format*` for parameterized messages -- Every ARB resource must include metadata because - `required-resource-attributes` is enabled. -- Parameterized strings must use ARB placeholders with `type` and an example. - -## Formatting Policy - -Use `intl` for locale-sensitive dates and numbers. Context-free helpers in -`shared/utils/format_utils.dart` should accept an optional locale or use -`Intl.getCurrentLocale()` when no `BuildContext` is available. - -Do not localize protocol constants, database keys, enum storage values, route -paths, asset paths, or provider keys. - -## Testing Policy - -Widget tests covering full localized screens should pump a `MaterialApp` or -`MaterialApp.router` with: - -- `localizationsDelegates: AppLocalizations.localizationsDelegates` -- `supportedLocales: AppLocalizations.supportedLocales` - -The `context.l10n` helper falls back to English for very small widget tests that -intentionally use a bare `MaterialApp`. Tests for English copy should keep -asserting visible English strings for now. When additional locales are added, -add locale-specific widget tests for key flows and locale-sensitive formatting -helpers. - -## Known Limitations - -- English is the only supported locale in v1. -- There is no in-app language picker. -- Some feature-specific strings remain hard-coded and are tracked as follow-up - milestones. -- Plural, gender, and select messages are only introduced where v1 needs them; - future translation work should revisit social-copy grammar in feed/profile - surfaces. diff --git a/docs/specs/notification.md b/docs/specs/notification.md deleted file mode 100644 index faf8ce4..0000000 --- a/docs/specs/notification.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: Notification Architecture -updated: 2026-04-29 ---- - -## Summary - -Lazurite currently supports in-app notifications (alerts feed + unread badge), but -it does not deliver OS-level notifications when the app is backgrounded or killed. -This spec defines a defensive, staged path from polling-only behavior to robust -push + local notification delivery. - -## Current State (Lazurite) - -What exists today: - -- `app.bsky.notification.listNotifications` for alerts feed -- `app.bsky.notification.getUnreadCount` polled every 30s in foreground -- `app.bsky.notification.updateSeen` on read/open flows -- `workmanager` already integrated for scheduled posts - -What is missing: - -- No device push token lifecycle -- No `registerPush` / `unregisterPush` integration -- No local notification renderer/channels/categories -- No durable dedupe state for "already notified" events -- No background notification sync worker - -## Research Findings - -### Bluesky notification APIs - -Core endpoints (auth required): - -- `app.bsky.notification.listNotifications` - - Params include `cursor`, `limit` (1-100), optional `reasons`, optional `seenAt` - - Response includes `notifications[]`, optional `cursor`, optional `seenAt` -- `app.bsky.notification.getUnreadCount` - - Returns unread `count` -- `app.bsky.notification.updateSeen` - - Marks notifications as seen at a timestamp -- `app.bsky.notification.registerPush` - - Required body: `serviceDid`, `token`, `platform`, `appId` - - Optional body: `ageRestricted` -- `app.bsky.notification.unregisterPush` - - Required body: `serviceDid`, `token`, `platform`, `appId` -- `app.bsky.notification.putPreferencesV2` - - Server-side notification preference controls (follow/like/reply/etc.) - -Important nuance: - -- Official Bluesky app passes an `atproto-proxy` header for push registration: - `did:web:api.bsky.app#bsky_notif` -- Official Bluesky app uses `serviceDid: did:web:api.bsky.app` (or staging DID) - -### Reference implementation patterns - -Strong patterns worth reusing: - -- Android FCM entrypoint (`FirebaseMessagingService`) parses push payload keys: - `senderDid`, `targetDid`, `recordUri`, `reason` -- Push payload is treated as a trigger, not trusted display content: - - Resolve full record via authenticated API - - Apply moderation/filtering before display -- Defensive processing contract: - - Timeout-bound notification processing (10s) - - Explicit processed/dropped ack state - - Dedup by stable notification identifiers -- Permission UX: - - Runtime request + rationale + settings fallback -- Delivery UX: - - Channel per reason family (likes/replies/follows/etc.) - - Deep links for post/profile targets - -Architecture-specific behavior to avoid coupling to: - -- Routing token registration through a custom backend endpoint can be valid, but - it is optional and not required for a direct Bluesky `registerPush` strategy. - -### Platform/Flutter constraints - -- Android 13+: `POST_NOTIFICATIONS` runtime permission required -- Android periodic background work minimum interval is 15 minutes -- Android exact-alarm behavior tightened on Android 14 (not suitable as default) -- iOS background execution is system-managed and non-deterministic - (`earliestBeginDate` is not a guarantee) -- iOS background push updates are low priority and can be throttled -- Flutter background handlers must be top-level entry points - (`@pragma('vm:entry-point')` where required) - -## Assumptions and Open Questions - -Assumptions (to validate during implementation): - -- Lazurite can register directly against Bluesky notification service DID - (`did:web:api.bsky.app`) using `atproto-proxy: ...#bsky_notif`. -- Notification payload and reason mapping from Bluesky are stable enough to map - into local channel/category policy. - -Open questions: - -- Multi-account policy: should each account register separate token records? -- Opt-out semantics: unregister on logout vs keep per-account registration? -- Should we support Android foreground service fallback for tighter latency, or - accept periodic/background best-effort only? -- Do we expose per-reason push toggles locally first, or defer to server-side - `putPreferencesV2` only? - -## Architecture Options - -### Option A: Polling only (foreground + background) - -Pros: - -- No FCM/APNs setup required -- Simpler backend story - -Cons: - -- Delayed delivery (>=15 min in background) -- iOS execution unpredictability -- Higher API/battery overhead - -### Option B: Push only - -Pros: - -- Fastest delivery -- Lower polling load - -Cons: - -- Requires strict token lifecycle and permission handling -- Delivery depends on push transport + payload correctness - -### Option C: Hybrid (recommended) - -- Push is primary trigger for near-real-time delivery -- Polling remains as fallback and for reconciliation -- Foreground unread badge polling can remain lightweight - -## Recommended Design - -### 1. NotificationDomainService - -Create a domain orchestrator that all entry points call: - -- `onForegroundTick()` (badge + optional reconcile) -- `onBackgroundTick()` (reconcile window) -- `onPushPayload(Map)` -- `markSeen(DateTime at)` - -Responsibilities: - -- Parse/validate payload defensively -- Fetch canonical notification details from Bluesky -- Filter by moderation + user prefs -- Dedupe and persist delivery state -- Trigger OS local notification display - -### 2. Push token lifecycle - -Add `PushRegistrationService`: - -- Acquire platform token (FCM/APNs bridge) -- Register with Bluesky `registerPush` -- Re-register on token refresh, login, account switch, app upgrade -- Unregister on logout/account removal (`unregisterPush`) - -Inputs: - -- `serviceDid` (prod/staging aware) -- `platform` (`ios`/`android`) -- `appId` (bundle/package identifier) -- `token` - -### 3. Local notifications - -Use a local notification adapter abstraction with platform implementations. - -- Android: - - Channel groups by reason family (`mentions`, `replies`, `follows`, `likes`, `misc`) - - Tap routes to `/post?uri=...` or `/profile/view?actor=...` -- iOS: - - Category identifiers for future actions - - Deep link userInfo payload for route restoration - -### 4. Dedupe + delivery state (Drift) - -Add a new table (with migration): `notification_deliveries` - -Suggested fields: - -- `id` (PK) -- `accountDid` (text) -- `notificationUri` (text, indexed) -- `notificationCid` (text nullable) -- `reason` (text) -- `indexedAt` (datetime) -- `source` (`push|poll`) -- `deliveredAt` (datetime) -- `openedAt` (datetime nullable) -- `dismissedAt` (datetime nullable) -- unique constraint on (`accountDid`, `notificationUri`) - -Use this table to avoid duplicate OS notifications across push and poll paths. - -### 5. Background execution - -Reuse existing `workmanager` foundation. - -- Android: - - Periodic reconcile task at 15m+ cadence - - Network-connected constraint -- iOS: - - Background fetch / BGTaskScheduler best-effort reconcile - - Keep tasks short and idempotent - -### 6. Permission UX - -- Ask only after contextual primer (alerts/home), not on first launch -- On deny, show "Open Settings" path -- Keep in-app alerts functional even when OS permission is denied - -## Rollout Plan - -### Phase N0 - Foundation hardening - -- Extract notification orchestration service -- Add delivery-state persistence + migrations -- Keep behavior polling-only - -### Phase N1 - Local notifications from polling - -- Emit OS local notifications for new unseen items discovered via reconcile -- Validate routing, dedupe, and permissions - -### Phase N2 - Push registration and handling - -- Add token registration/unregistration -- Add background push payload handler -> fetch canonical record -> display - -### Phase N3 - Preference integration - -- Add server preference sync (`getPreferences` / `putPreferencesV2`) -- Add local controls mapped to server fields - -### Phase N4 - Reliability/observability - -- Add structured notification logs + debug screen counters -- Add failure metrics (token register failures, dropped payloads, dedupe suppressions) - -## Testing Strategy - -Required coverage per phase: - -- Unit tests: - - Payload parsing/validation - - Dedupe decisions - - token lifecycle state machine -- Bloc/cubit tests: - - Permission gating - - unread count reconcile behavior -- Integration tests: - - deep link open from notification payload - - background worker reconciliation path -- Manual smoke matrix: - - Android 13+ deny/allow paths - - iOS allow/deny/settings round trip - - multi-account register/unregister correctness - -## Risks and Mitigations - -- Risk: Duplicate alerts from push + poll race - - Mitigation: persisted dedupe with unique key on notification URI -- Risk: Background handlers killed or delayed - - Mitigation: hybrid model + reconcile worker + idempotent processing -- Risk: API/proxy mismatches for `registerPush` - - Mitigation: stage against test account, log raw request/response status -- Risk: Permission denial degrades trust - - Mitigation: contextual request timing, clear fallback behavior - -## Deferred (Not in initial rollout) - -- Rich actions (reply/like from notification shade) -- Notification grouping by thread/conversation -- Server-driven quiet hours / digest mode -- Desktop/web parity diff --git a/docs/specs/phase-5.md b/docs/specs/phase-5.md deleted file mode 100644 index 540276b..0000000 --- a/docs/specs/phase-5.md +++ /dev/null @@ -1,234 +0,0 @@ ---- -title: Phase 5 Spec -updated: 2026-03-31 ---- - -## Feature Parity - -Three new endpoint integrations to round out UI coverage, plus two Constellation-powered features. - ---- - -### 1. Starter Pack Search (Search Screen) - -**Endpoint:** `GET /xrpc/app.bsky.graph.searchStarterPacks` -**Auth:** Not required - -**Request:** - -| Param | Type | Required | Default | Notes | -|----------|--------|----------|---------|-------------------------------| -| `q` | string | yes | - | Lucene-style query | -| `limit` | int | no | 25 | 1–100 | -| `cursor` | string | no | - | Pagination cursor | - -**Response:** - -```json -{ - "cursor": "string?", - "starterPacks": "StarterPackViewBasic[]" -} -``` - -`StarterPackViewBasic` includes: `uri`, `cid`, `record`, `creator` (ProfileViewBasic), -`listItemCount?`, `joinedWeekCount?`, `joinedAllTimeCount?`, `labels?`, `indexedAt`. - -**SDK:** `bluesky.graph.searchStarterPacks(q:, limit:, cursor:)` -→ `XRPCResponse` - -**UI:** Add a third "Starter Packs" tab to the search screen alongside Posts and People. -Tapping a result navigates to the existing starter pack detail screen. Infinite scroll -pagination via cursor. Reuse the existing `StarterPackViewBasic` tile pattern from -the profile starter packs tab. - ---- - -### 2. Suggested Follows (Profile Screen) - -**Endpoint:** `GET /xrpc/app.bsky.graph.getSuggestedFollowsByActor` -**Auth:** Not required - -**Request:** - -| Param | Type | Required | Notes | -|---------|--------|----------|-------------------| -| `actor` | string | yes | DID or handle | - -**Response:** - -```json -{ - "suggestions": "ProfileView[]", - "isFallback": "bool (default false)", - "recIdStr": "string?" -} -``` - -No pagination - returns all suggestions in one response. - -**SDK:** `bluesky.graph.getSuggestedFollowsByActor(actor:)` -→ `XRPCResponse` - -**UI:** New "Suggested Follows" entry in the profile screen's overflow (more options) -bottom sheet. Opens a draggable scrollable sheet listing `ProfileView` tiles with -follow/unfollow buttons. Each tile navigates to the user's profile on tap. Show empty -state if `suggestions` is empty. Hide the menu entry when viewing own profile. - ---- - -### 3. Video Upload Limits (Settings Screen) - -**Endpoint:** `GET /xrpc/app.bsky.video.getUploadLimits` -**Auth:** Required - -**Request:** None - -**Response:** - -```json -{ - "canUpload": "bool", - "remainingDailyVideos": "int?", - "remainingDailyBytes": "int?", - "message": "string?", - "error": "string?" -} -``` - -**SDK:** `bluesky.video.getUploadLimits()` -→ `XRPCResponse` - -**UI:** New tile in the settings screen's Account section showing daily video upload -quota. Display remaining video count and remaining bytes (formatted as MB/GB). -Show `canUpload` status and any server `message`. Fetch on screen load; show -loading indicator while fetching. If the endpoint returns an error or `canUpload` -is false, show the reason. - ---- - -### 4. Profile Context (Constellation) - -Social context for any account powered by [Constellation](https://constellation.microcosm.blue/) - a public AT Protocol backlink index. No auth required; only a `User-Agent` header. - -**Design philosophy** (carried from lazurite-desktop diagnostics): - -- Inform, don't alarm. Present data neutrally. -- No composite risk scores. Show the data; let the user interpret it. -- Context over counts. Prefer showing _what kind_ of lists over _how many_. -- Respect the viewed account. Default to aggregate summaries; expand to specifics on request. - -#### Constellation Client - -A thin HTTP client targeting a configurable Constellation instance (default: `https://constellation.microcosm.blue`). User-configurable via Settings to support self-hosted instances. Timeout: 10 seconds. - -All endpoints use XRPC format at `{base}/xrpc/{endpoint}` with query parameters. - -#### 4a. Blocked By (incoming blocks) - -**Endpoint:** `GET /xrpc/blue.microcosm.links.getBacklinksCount` -**Purpose:** Count of accounts that have blocked this user. - -| Param | Type | Required | Notes | -|----------|--------|----------|------------------------------------| -| `subject`| string | yes | Target DID | -| `source` | string | yes | `app.bsky.graph.block:subject` | - -**Response:** `{ "total": int }` - -**Detail list endpoint:** `GET /xrpc/blue.microcosm.links.getDistinct` - -| Param | Type | Required | Default | Notes | -|----------|--------|----------|---------|------------------------------------| -| `subject`| string | yes | - | Target DID | -| `source` | string | yes | - | `app.bsky.graph.block:subject` | -| `limit` | int | no | 16 | Max 100 | -| `cursor` | string | no | - | Pagination cursor | - -**Response:** `{ "total": int, "dids": string[], "cursor": string? }` - -Returned DIDs are hydrated via `bluesky.actor.getProfiles(actors:)` (batch, max 25 per call) to show profile cards. - -#### 4b. Users Blocked (outgoing blocks) - -**Endpoint:** `GET /xrpc/com.atproto.repo.listRecords` (AT Protocol, not Constellation) - -| Param | Type | Required | Default | Notes | -|-------------|--------|----------|---------|-------------------------------| -| `repo` | string | yes | - | Actor DID | -| `collection`| string | yes | - | `app.bsky.graph.block` | -| `limit` | int | no | 50 | Max 100 | -| `cursor` | string | no | - | Pagination cursor | - -**Response:** `{ "records": Record[], "cursor": string? }` - -Each record's `value.subject` is the blocked DID. Hydrate via `getProfiles`. - -**Note:** Outgoing blocks are only readable from the actor's own repo. For other users' profiles, this tab shows only the count from the actor's public repo listing (if accessible) or is hidden entirely if the repo restricts reads. - -#### 4c. Lists On - -**Endpoint:** `GET /xrpc/blue.microcosm.links.getBacklinks` - -| Param | Type | Required | Default | Notes | -|----------|--------|----------|---------|-----------------------------------------| -| `subject`| string | yes | - | Target DID | -| `source` | string | yes | - | `app.bsky.graph.listitem:subject` | -| `limit` | int | no | 16 | Max 100 | -| `cursor` | string | no | - | Pagination cursor | - -**Response:** - -```json -{ - "total": "int", - "linking_records": [{ "did": "string", "collection": "string", "rkey": "string" }], - "cursor": "string?" -} -``` - -Each backlink record represents a list item. The owning list AT-URI is derived as `at://{record.did}/app.bsky.graph.list/{rkey-of-list}`. Since backlinks only give us the listitem record, we need to resolve the parent list. Two approaches: - -1. **getManyToMany** (preferred): `GET /xrpc/blue.microcosm.links.getManyToMany` with `source=app.bsky.graph.listitem:subject` and `pathToOther=list` returns items grouped by their parent list URI. Each item has `otherSubject` (the list AT-URI). -2. **Fallback**: fetch each listitem record via `com.atproto.repo.getRecord` to read its `list` field, then hydrate lists via `bluesky.graph.getList`. - -Hydrate list metadata via `bluesky.graph.getList(list:)` to show name, purpose, owner, member count. - -#### Profile Context UI - -**Entry point:** New "Context" item in the profile screen's overflow menu (PopupMenuButton / three-dot menu). Navigates to a dedicated full screen - not a tab on the profile, since this data is sought intentionally, not browsed casually. - -**Route:** `/profile-context?did={DID}` - -**Screen layout:** - -- `AppBar` with title "Profile Context" and the user's handle as subtitle -- Three-tab `TabBar`: **Blocked By** | **Blocking** | **Lists** -- Each tab is a paginated list with pull-to-refresh - -**Blocked By tab:** - -- Header row: total count (from `getBacklinksCount`) displayed prominently -- "Show accounts" expand button - on tap, fetches DIDs via `getDistinct` and hydrates profiles -- Profile tiles: avatar, display name, handle. Tap → navigate to profile -- Pagination via cursor (infinite scroll) -- Contextualizing note: _"Blocks are a normal part of social media. This data is public on the AT Protocol."_ - -**Blocking tab:** - -- Same layout as Blocked By but sourced from `listRecords` -- Only available when viewing own profile or if the repo is publicly readable -- When unavailable: show explanatory text - -**Lists tab:** - -- List cards: name, owner handle, purpose badge (curate/modlist/reference), member count, description snippet -- Grouped by purpose (curation first, then moderation, then reference) -- Tap → navigate to list detail screen (`/list?uri=`) -- Pagination via cursor - -**States:** - -- Loading: skeleton shimmer matching card dimensions -- Empty: per-tab contextual empty state (e.g., "Not on any lists" / "No blocks found") -- Error: inline retry button per tab, not full-screen error diff --git a/docs/specs/post-editing.md b/docs/specs/post-editing.md deleted file mode 100644 index a7474dd..0000000 --- a/docs/specs/post-editing.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: Post Editing Spec (v1) -updated: 2026-04-14 ---- - -## Summary - -Add AT Protocol post editing to Lazurite by replacing post records via -`com.atproto.repo.deleteRecord` + `com.atproto.repo.createRecord` (same `rkey`, -same URI), with a v1 scope of: - -- Entry point: thread screen only -- Editable fields: post text + regenerated facets -- Preserved fields: reply/embed/langs/labels/tags/unknown fields -- Concurrency control: `swapRecord` with the current post CID - -## Protocol Mechanics - -### Record Replacement - -Use delete + recreate on the existing `app.bsky.feed.post` rkey: - -- `repo`: authenticated account DID -- `collection`: `app.bsky.feed.post` -- `rkey`: extracted from post AT-URI -- Delete guard: `swapRecord` with latest/current CID from the post view -- Recreate: `createRecord` with the same `rkey` to preserve AT-URI - -The edit payload is built from the original record, replacing only: - -- `text` -- `facets` (recomputed from updated text; removed when empty) - -`createdAt` is preserved from the original record when present. If missing or -invalid, fallback to current UTC timestamp as a defensive safeguard. - -### Conflict Handling - -When delete/recreate detects stale state (`InvalidSwap`) or changed ownership, -Lazurite treats the edit as a conflict and shows a non-merge message instructing -the user to reopen and retry. - -If recreate fails after delete, Lazurite attempts defensive recovery by -restoring the original record on the same `rkey`. - -## UX and Flow - -### Thread Entry - -For author-owned posts in the thread action sheet: - -- Add `Edit Post` action. -- Navigate to compose with edit context: - - `editPostUri` - - `editPostCid` - - `editRecord` - - `initialText` - -On successful edit completion, refresh the thread by reloading the current -post URI. - -### Compose Edit Mode - -Compose supports explicit edit mode via route context. - -Edit-mode behavior: - -- Title/action labels switch to edit wording (`Edit Post`, `Save Changes`) -- Inline algorithm-impact notice is shown with an info dialog -- Unsupported create-flow controls are disabled/hidden: - - Save Draft - - Schedule - - Add/remove image - - Add/remove video -- Submission performs `putRecord` update rather than `createRecord` - -## Algorithmic Implications (User Notice) - -Editing can change how the post is indexed and distributed: - -- Post metadata like `indexedAt` may change after edits -- Feed ranking and search visibility may shift after re-indexing -- Read-after-write propagation can be delayed across services and surfaces -- Because edits are saved as delete+recreate on the same URI, counters and - visibility can briefly lag while services reconcile state - -Lazurite informs users inline in compose edit mode and provides an info action -for additional context. - -## Limitations - -- Edit action is exposed only in thread view -- Only text/facets are user-editable -- No merge flow for edit conflicts - -## Beyond - -- Add edit entry points in timeline/search/saved post cards -- Consider edit-history affordances and richer conflict resolution UX - - The question here is where do we store history? What happens between logins? At what - point does Lazurite need its own lexicons for features like this? diff --git a/docs/tasks/internationalization.md b/docs/tasks/internationalization.md index d490cf4..7a49d5e 100644 --- a/docs/tasks/internationalization.md +++ b/docs/tasks/internationalization.md @@ -1,29 +1,8 @@ -# Internationalization Milestones +# Internationalization Tasks -## M0 - Foundation and English ARB - -- [x] Add `flutter_localizations`, `intl`, `flutter.generate`, and `l10n.yaml` -- [x] Add canonical `lib/core/l10n/intl_en.arb` -- [x] Generate and track `AppLocalizations` Dart files -- [x] Wire `MaterialApp.router` delegates and supported locales -- [x] Add `context.l10n` helper for Lazurite widgets - -## M1 - Core, Shared, Auth, Settings, Search - -- [x] Localize app shell navigation, drawer labels, and common menu copy -- [x] Localize shared confirmation/error/moderation overlay copy -- [x] Localize login, saved account actions, and legal links -- [x] Localize settings sections, provider dialogs, and troubleshooting actions -- [x] Localize primary search tabs, placeholders, filters, and unavailable states -- [x] Add focused widget/localization tests for migrated surfaces - -## M2 - Remaining Feature Surfaces - -- [x] Localize feed cards, post menus, post actions, saved posts, and trending -- [x] Localize compose flow, media alt text editors, draft/schedule states, and validation -- [x] Localize profile screens, profile actions, reports, follows, lists, and starter packs -- [x] Localize messages, notifications, alerts, and account switching sheets -- [x] Localize moderation settings/detail screens and logs/devtools user-facing labels +Completed localization foundation and feature-surface migration work is recorded in +[CHANGELOG.md, v1.0.0 (Alpha 6)](../../CHANGELOG.md#v100-alpha-6). Notes live in +[docs/dev/internationalization.md](../dev/internationalization.md). ## M3 - User-Facing Language Selection diff --git a/docs/tasks/notification.md b/docs/tasks/notification.md index ac4d66e..ae2e292 100644 --- a/docs/tasks/notification.md +++ b/docs/tasks/notification.md @@ -1,45 +1,13 @@ --- -title: Notification Milestones -updated: 2026-05-17 +title: Notification Follow-up Tasks +updated: 2026-05-24 --- -## M1 - Foundation Hardening (Polling Baseline) +Completed polling, local notification, push registration, payload processing, +background reconcile, and Firebase/APNs setup milestones are recorded in +[CHANGELOG.md, v1.0.0 (Alpha 1)](../../CHANGELOG.md#v100-alpha-1). -- [x] Introduce `NotificationDomainService` orchestration layer -- [x] Add Drift `notification_deliveries` table with migration -- [x] Route existing polling paths through orchestration layer -- [x] Add unit tests for dedupe and state persistence - -## M2 - Local Notifications from Reconcile - -- [x] Add local notification adapter abstraction -- [x] Android channels by reason family -- [x] iOS category + payload deep-link mapping -- [x] Show local notifications for newly discovered unseen items -- [x] Add widget/integration tests for tap -> route behavior - -## M3 - Push Registration Lifecycle - -- [x] Add token acquisition and refresh listeners -- [x] Implement `registerPush` and `unregisterPush` -- [x] Wire account login/switch/logout paths -- [x] Add retries/backoff for registration failures -- [x] Add unit tests for lifecycle transitions - -## M4 - Push Payload Processing - -- [x] Add background payload entrypoint (`@pragma('vm:entry-point')`) -- [x] Parse defensively: `senderDid`, `targetDid`, `recordUri`, `reason` -- [x] Fetch canonical notification payload before display -- [x] Apply moderation + preference filtering before display -- [x] Add timeout-bound processing and drop accounting - -## M5 - Background Reconciliation - -- [x] Add periodic background reconcile task (Android 15m+) -- [x] Add iOS background fetch/BGTaskScheduler integration -- [x] Ensure tasks are idempotent and dedupe-safe -- [x] Add test harness for worker entrypoints +Notes live in [docs/dev/notifications.md](../dev/notifications.md). ## M6 - Preferences and UX @@ -53,11 +21,3 @@ updated: 2026-05-17 - [ ] Add structured logs + debug counters for notification flows - [ ] Add smoke checklist for Android/iOS permission and delivery scenarios - [ ] Validate multi-account behavior and token cleanup - -## M8 - Firebase/APNs Production Push Setup - -- [x] Create/configure Firebase project apps for iOS + Android -- [x] Add `GoogleService-Info.plist` to iOS target and `google-services.json` to `android/app` -- [x] Configure Apple Push Notifications capability/provisioning in Apple Developer -- [x] Upload APNs auth key/certificate to Firebase Cloud Messaging settings -- [x] Validate end-to-end remote push delivery (foreground, background, terminated) on iOS + Android