···3030 <meta-data
3131 android:name="flutterEmbedding"
3232 android:value="2" />
3333+ <!-- WorkManager background service for scheduled posts -->
3434+ <service
3535+ android:name="be.tramckrijte.workmanager.BackgroundWorker"
3636+ android:exported="false"
3737+ android:permission="android.permission.BIND_JOB_SERVICE" />
3338 </application>
3439 <!-- Required to query activities that can process text, see:
3540 https://developer.android.com/training/package-visibility and
+88-81
docs/specs/phase-3.md
···2233## Post Composition
4455-Posts are created via `com.atproto.repo.createRecord` with collection
66-`app.bsky.feed.post`. The compose screen is a full-screen modal opened from a
77-floating action button on the home screen.
55+Posts are created via `com.atproto.repo.createRecord` with collection `app.bsky.feed.post`.
66+The compose screen is a full-screen modal opened from a floating action button on the home screen.
8798### Post Record Structure
109···19182019### Media Uploads
21202222-Upload images via `com.atproto.repo.uploadBlob`. Returns a `blob` ref used in
2323-the embed object. No GIF support — images only.
2121+Upload images via `com.atproto.repo.uploadBlob`. Returns a `blob` ref used in the embed object.
24222523| Constraint | Value |
2624| -------------- | ---------------------------------- |
···2927| Accepted types | JPEG, PNG, WebP |
3028| Alt text | Required UI field, optional in API |
31293232-Embed type for images: `app.bsky.embed.images`. Each image entry has `image`
3333-(blob ref), `alt` (string), and optional `aspectRatio` (`width` / `height`).
3030+Embed type for images: `app.bsky.embed.images`.
3131+Each image entry has `image` (blob ref), `alt` (string), and optional `aspectRatio` (`width` / `height`).
3232+3333+### Video Uploads
3434+3535+Upload video via `app.bsky.video.uploadVideo` on the `video.bsky.app` service.
3636+Video processing is asynchronous — the endpoint returns a `JobStatus` with a `jobId`.
3737+Poll `app.bsky.video.getJobStatus` until `state` is `JOB_STATE_COMPLETED` (returns the final `blob` ref) or `JOB_STATE_FAILED`.
3838+3939+Before uploading, call `app.bsky.video.getUploadLimits` to check the user's remaining daily quota (`canUpload`, `remainingDailyVideos`, `remainingDailyBytes`).
4040+If the user cannot upload, show the server-provided `message` or a fallback explaining the daily limit.
4141+4242+| Constraint | Value |
4343+| -------------- | -------------------------------------------- |
4444+| Max videos | 1 per post (mutually exclusive with images) |
4545+| Max file size | 100 MB |
4646+| Accepted types | MP4 |
4747+| Alt text | Optional UI field |
4848+| Captions | Optional; `EmbedVideoCaption` with lang code |
4949+5050+Embed type for video: `app.bsky.embed.video`. Fields:
5151+5252+| Field | Type | Description |
5353+| -------------- | ------------------- | -------------------------------------- |
5454+| `video` | blob | Processed video blob from job status |
5555+| `alt` | string | Accessibility description |
5656+| `aspectRatio` | object | `width` / `height` integers |
5757+| `captions` | array | Caption files with BCP-47 `lang` codes |
5858+| `presentation` | string | `"default"` or `"gif"` playback hint |
5959+6060+The compose screen must show a progress indicator during video upload and processing.
6161+Disable the submit button until processing completes. If the job fails, display the error message from `JobStatus.error` and allow the user to retry or remove the video.
6262+6363+A post embeds either images **or** a video, never both. When a video is attached, the image picker should be disabled (and vice versa).
6464+Switching media type should prompt the user to confirm replacing the existing attachment(s).
34653566### Facet Detection
36673737-Use **bluesky_text** to detect mentions, links, and hashtags in the post text
3838-and produce the `facets` array automatically before submission. The compose
3939-screen should render a live preview of detected facets with colour-coded
4040-highlights as the user types.
6868+Use **bluesky_text** to detect mentions, links, and hashtags in the post text and produce the `facets` array automatically before submission.
6969+The compose screen should render a live preview of detected facets with color-coded highlights as the user types.
41704271### Grapheme Counter
43724444-Display a live character counter showing remaining graphemes (300 max). Use
4545-Dart's `Characters` class for accurate grapheme cluster counting. Disable the
4646-submit button when the count exceeds 300 or the text is empty.
7373+Display a live character counter showing remaining graphemes (300 max).
7474+Use Dart's `Characters` class for accurate grapheme cluster counting.
7575+Disable the submit button when the count exceeds 300 or the text is empty.
47764877### Drafts
49785050-Persist unsent posts locally in a Drift `drafts` table. On network failure or
5151-explicit save, always store the draft. Drafts are account-scoped.
7979+Persist unsent posts locally in a Drift `drafts` table.
8080+On network failure or explicit save, always store the draft. Drafts are account-scoped.
52815382| Column | Type | Notes |
5483| ------------- | -------- | ---------------------------------------- |
···66956796### Scheduled Posts
68976969-Schedule posts for future publication using a local scheduler. Store the
7070-scheduled time alongside the draft. Use a `WorkManager` (Android) /
7171-`BGTaskScheduler` (iOS) background task to submit the post at the scheduled
7272-time. If the device is offline at the scheduled time, queue the post and retry
7373-when connectivity resumes.
9898+Schedule posts for future publication using a local scheduler.
9999+Store the scheduled time alongside the draft. Use a `WorkManager` (Android) / `BGTaskScheduler` (iOS) background task to submit the post at the scheduled time.
100100+If the device is offline at the scheduled time, queue the post and retry when connectivity resumes.
741017575-Add a `scheduled_at` (nullable datetime) column to the `drafts` table. When
7676-non-null, the draft is treated as scheduled rather than a regular draft.
102102+Add a `scheduled_at` (nullable datetime) column to the `drafts` table.
103103+When non-null, the draft is treated as scheduled rather than a regular draft.
771047878-Build a `ComposeBloc` with events: `TextChanged`, `MediaAttached`,
7979-`MediaRemoved`, `AltTextUpdated`, `DraftSaved`, `DraftLoaded`,
8080-`PostScheduled`, `PostSubmitted`.
105105+Build a `ComposeBloc` with events: `TextChanged`, `MediaAttached`, `MediaRemoved`, `AltTextUpdated`, `VideoAttached`, `VideoRemoved`, `DraftSaved`, `DraftLoaded`, `PostScheduled`, `PostSubmitted`.
8110682107## Notifications
83108···108133109134### Rendering
110135111111-Group notifications by day. Each notification row shows the author avatar, the
112112-reason icon, a summary line, and an optional post preview snippet. Tapping a
113113-notification navigates to the relevant post or profile.
136136+Group notifications by day. Each notification row shows the author avatar, the reason icon, a summary line, and an optional post preview snippet.
137137+Tapping a notification navigates to the relevant post or profile.
114138115115-Display an unread count badge on the Notifications nav bar item. Poll
116116-`getUnreadCount` on a 30-second interval when the app is foregrounded. Call
117117-`updateSeen` when the notifications screen is opened.
139139+Display an unread count badge on the Notifications nav bar item.
140140+Poll `getUnreadCount` on a 30-second interval when the app is foregrounded.
141141+Call `updateSeen` when the notifications screen is opened.
118142119119-Build a `NotificationBloc` with events: `NotificationsRequested`,
120120-`NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead`.
143143+Build a `NotificationBloc` with events: `NotificationsRequested`, `NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead`.
121144122145## Post & Profile Actions
123146124124-All post and profile interactions use the AT Protocol record model. Actions
125125-that create a relationship (like, repost, follow, block) write a record via
126126-`com.atproto.repo.createRecord` and undo by deleting via
127127-`com.atproto.repo.deleteRecord`. Muting is a server-side procedure call with
128128-no persistent record.
147147+All post and profile interactions use the AT Protocol record model.
148148+Actions that create a relationship (like, repost, follow, block) write a record via `com.atproto.repo.createRecord` and undo by deleting via `com.atproto.repo.deleteRecord`.
149149+Muting is a server-side procedure call with no persistent record.
129150130151### API
131152···139160140161### Like
141162142142-Collection: `app.bsky.feed.like`. Record contains a `subject` (RepoStrongRef
143143-with the post's AT-URI and CID) and `createdAt`.
163163+Collection: `app.bsky.feed.like`.
164164+Record contains a `subject` (RepoStrongRef with the post's AT-URI and CID) and `createdAt`.
144165145145-To unlike, extract the record key (rkey) from the `viewer.like` AT-URI and
146146-call `deleteRecord` with collection `app.bsky.feed.like` and that rkey.
166166+To unlike, extract the record key (rkey) from the `viewer.like` AT-URI and call `deleteRecord` with collection `app.bsky.feed.like` and that rkey.
147167148148-The `PostView.viewer.like` field is non-null when the current user has liked
149149-the post. Use this to drive the filled/outlined heart icon state.
168168+The `PostView.viewer.like` field is non-null when the current user has liked the post.
169169+Use this to drive the filled/outlined heart icon state.
150170151171### Repost
152172153153-Collection: `app.bsky.feed.repost`. Record structure is identical to like —
154154-`subject` (RepoStrongRef) + `createdAt`.
173173+Collection: `app.bsky.feed.repost`. Record structure is identical to like — `subject` (RepoStrongRef) + `createdAt`.
155174156175To un-repost, extract the rkey from `viewer.repost` and delete the record.
157176158158-The `PostView.viewer.repost` field is non-null when the current user has
159159-reposted. Use this for the repost icon state.
177177+The `PostView.viewer.repost` field is non-null when the current user has reposted.
178178+Use this for the repost icon state.
160179161180### Follow
162181163163-Collection: `app.bsky.graph.follow`. Record contains `subject` (the target
164164-user's DID as a string) and `createdAt`.
182182+Collection: `app.bsky.graph.follow`. Record contains `subject` (the target user's DID as a string) and `createdAt`.
165183166184To unfollow, extract the rkey from `viewer.following` and delete the record.
167185···177195- `app.bsky.graph.muteActor` — input: `{ actor: DID }`
178196- `app.bsky.graph.unmuteActor` — input: `{ actor: DID }`
179197180180-Both return empty responses. The `viewer.muted` boolean on profiles reflects
181181-the current mute state. Muted accounts' posts are still fetched but should be
182182-visually de-emphasised or filtered in the UI based on user preference.
198198+Both return empty responses. The `viewer.muted` boolean on profiles reflects the current mute state.
199199+Muted accounts' posts are still fetched but should be visually de-emphasised or filtered in the UI based on user preference.
183200184201### Block
185202186186-Collection: `app.bsky.graph.block`. Record contains `subject` (the target
187187-user's DID) and `createdAt`.
203203+Collection: `app.bsky.graph.block`. Record contains `subject` (the target user's DID) and `createdAt`.
188204189205To unblock, extract the rkey from `viewer.blocking` and delete the record.
190206···216232| `reasonRude` | Harassment or rude behaviour |
217233| `reasonOther` | Other (requires text explanation) |
218234219219-The report dialog should present the reason picker and an optional free-text
220220-description field. Submitting returns a report ID for confirmation.
235235+The report dialog should present the reason picker and an optional free-text description field.
236236+Submitting returns a report ID for confirmation.
221237222238### Optimistic Updates
223239224224-All toggle actions (like, repost, follow, mute, block) should use optimistic
225225-UI updates:
240240+All toggle actions (like, repost, follow, mute, block) should use optimistic UI updates:
2262412272421. Immediately update the local state (icon, count, button label).
2282432. Fire the API call in the background.
2292443. On success, reconcile with the server response (update the viewer URI).
2302454. On failure, roll back the local state and show a snackbar error.
231246232232-Build a `PostActionCubit` that manages per-post action state (like, repost,
233233-save). It accepts the initial `ViewerState` from the post and exposes
247247+Build a `PostActionCubit` that manages per-post action state (like, repost, save).
248248+It accepts the initial `ViewerState` from the post and exposes
234249toggleable methods.
235250236236-Build a `ProfileActionCubit` that manages per-profile action state (follow,
237237-mute, block). It accepts the initial `ViewerState` from the profile and
238238-exposes toggleable methods.
251251+Build a `ProfileActionCubit` that manages per-profile action state (follow, mute, block).
252252+It accepts the initial `ViewerState` from the profile and exposes toggleable methods.
239253240254### Post Action Bar
241255···248262| Like | heart | Toggle like | — |
249263| Share | share | Share sheet | — |
250264251251-The bookmark (save) icon is placed in the post overflow menu alongside
252252-"Report" and "Copy link".
265265+The bookmark (save) icon is placed in the post overflow menu alongside "Report" and "Copy link".
253266254254-Like and repost counts are displayed next to their respective icons. Counts
255255-update optimistically. The repost long-press opens a bottom sheet with
256256-"Repost" and "Quote Post" options.
267267+Like and repost counts are displayed next to their respective icons. Counts update optimistically.
268268+The repost long-press opens a bottom sheet with "Repost" and "Quote Post" options.
257269258270### Profile Action Buttons
259271···266278| Blocked by them | — | No button |
267279| You blocked them | "Unblock" | Delete block |
268280269269-The profile overflow menu (three-dot icon) contains: Mute / Unmute, Block /
270270-Unblock, Report, Copy DID, Share profile.
281281+The profile overflow menu (three-dot icon) contains: Mute / Unmute, Block / Unblock, Report, Copy DID, Share profile.
271282272283Mute and block actions should show a confirmation dialog before proceeding.
273284274285## Saved Posts
275286276276-Allow users to bookmark posts locally for later reading. Saved posts are stored
277277-only in Drift — nothing is written to the network. This is intentionally
278278-private and local-only.
287287+Allow users to bookmark posts locally for later reading. Saved posts are stored only in Drift — nothing is written to the network.
288288+This is intentionally private and local-only.
279289280290### Drift Table
281291···291301292302### UI
293303294294-Add a "Save" action (bookmark icon) to the post action bar. Tapping toggles
295295-the saved state. Saved posts are viewable from a "Saved" section in the
296296-profile screen or settings.
304304+Add a "Save" action (bookmark icon) to the post action bar. Tapping toggles the saved state.
305305+Saved posts are viewable from a "Saved" section in the profile screen or settings.
297306298298-Build a `SavedPostsCubit` that reads/writes the `saved_posts` table and
299299-exposes a stream of saved post URIs for quick lookup (to show filled vs
300300-outlined bookmark icons in the feed).
307307+Build a `SavedPostsCubit` that reads/writes the `saved_posts` table and exposes a stream of saved post URIs for quick lookup (to show filled vs outlined bookmark icons in the feed).
+15-11
docs/tasks/phase-3.md
···2233## M8 — Post Composition
4455-- [ ] Full-screen compose modal with text input, live grapheme counter (300 max), and submit button
66-- [ ] `ComposeBloc` — events: `TextChanged`, `MediaAttached`, `MediaRemoved`, `AltTextUpdated`, `DraftSaved`, `DraftLoaded`, `PostScheduled`, `PostSubmitted`
77-- [ ] Image attachment via `uploadBlob` — up to 4 images, alt text input per image
88-- [ ] Live facet detection and preview via `bluesky_text` (mentions, links, hashtags)
99-- [ ] Post creation via `com.atproto.repo.createRecord` with `app.bsky.feed.post` collection
1010-- [ ] Reply support — pass `parent` + `root` refs when composing from a post thread
1111-- [ ] Drift migration: add `drafts` table (id, account_did, text, reply_uri, embed_json, media_paths, created_at, updated_at, scheduled_at)
1212-- [ ] Draft save on network failure, explicit save, and back-navigation
1313-- [ ] Drafts list UI accessible from compose toolbar
1414-- [ ] Scheduled posts — date/time picker, background task via WorkManager / BGTaskScheduler
1515-- [ ] Floating action button on home screen to open compose modal
55+- [x] Full-screen compose modal with text input, live grapheme counter (300 max), and submit button
66+- [x] `ComposeBloc` — events: `TextChanged`, `MediaAttached`, `MediaRemoved`, `AltTextUpdated`, `VideoAttached`, `VideoRemoved`, `DraftSaved`, `DraftLoaded`, `PostScheduled`, `PostSubmitted`
77+- [x] Image attachment via `uploadBlob` — up to 4 images, alt text input per image, file size (1 MB max) and MIME type (JPEG, PNG, WebP) validation
88+- [x] Video attachment via `app.bsky.video.uploadVideo` — 1 per post (mutually exclusive with images), 100 MB max, MP4 only
99+- [x] Video upload quota check via `getUploadLimits` before upload; show limit message if `canUpload` is false
1010+- [x] Video processing job polling via `getJobStatus` with progress indicator; handle `JOB_STATE_FAILED` with error display and retry
1111+- [x] Video embed via `app.bsky.embed.video` with alt text, aspectRatio, and optional captions
1212+- [x] Live facet detection and preview via `bluesky_text` (mentions, links, hashtags)
1313+- [x] Post creation via `com.atproto.repo.createRecord` with `app.bsky.feed.post` collection
1414+- [x] Reply support — pass `parent` + `root` refs when composing from a post thread
1515+- [x] Drift migration: add `drafts` table (id, account_did, text, reply_uri, embed_json, media_paths, created_at, updated_at, scheduled_at)
1616+- [x] Draft save on network failure, explicit save, and back-navigation
1717+- [x] Drafts list UI accessible from compose toolbar
1818+- [x] Scheduled posts — date/time picker, background task via WorkManager / BGTaskScheduler
1919+- [x] Floating action button on home screen to open compose modal
16201721## M9 — Notifications
1822