mirror of
https://github.com/project-dorado/dorado-hd.git
synced 2026-10-11 21:10:01 +00:00
feat(social): mark cloud inbox messages read server-side via /v1/social/me/inbox/{id}/read
This commit is contained in:
@@ -9,6 +9,7 @@ data class CloudInboxMessage(
|
||||
val recipientTag: String,
|
||||
val subject: String,
|
||||
val body: String,
|
||||
val isRead: Boolean,
|
||||
val createdAt: Long,
|
||||
)
|
||||
|
||||
@@ -89,10 +90,14 @@ class CloudSocialClient(
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote mark-read. The current dorado-cloud inbox is read-only (there is
|
||||
* no mark-read route), so this always reports false; the Room store owns
|
||||
* read state. Kept on the client so a future server route can be wired
|
||||
* without changing callers.
|
||||
* Marks a message read server-side (`POST /v1/social/me/inbox/{id}/read`).
|
||||
* Room remains the source of truth for the UI; this keeps the server's read
|
||||
* state in step and reports false when unavailable. Best-effort by callers.
|
||||
*/
|
||||
suspend fun markRead(id: String): Boolean = false
|
||||
suspend fun markRead(id: String): Boolean {
|
||||
if (id.isBlank()) return false
|
||||
val auth = token() ?: return false
|
||||
val client = clientOrNull() ?: return false
|
||||
return client.markInboxRead(id, auth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,11 +341,16 @@ class DoradoCloudClient(private val http: CloudHttp) {
|
||||
recipientTag = CloudJson.stringOr(obj, "recipientTag", ""),
|
||||
subject = CloudJson.stringOr(obj, "subject", ""),
|
||||
body = CloudJson.stringOr(obj, "body", ""),
|
||||
isRead = CloudJson.bool(obj, "isRead"),
|
||||
createdAt = parseTimestamp(CloudJson.stringOr(obj, "createdAt", "")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Marks one of the signed-in user's inbox messages read; false when not found. */
|
||||
suspend fun markInboxRead(id: String, token: String? = null): Boolean =
|
||||
action("v1/social/me/inbox/${enc(id)}/read", "POST", token)
|
||||
|
||||
suspend fun follow(handle: String, token: String? = null): Boolean = action("v1/social/profiles/${enc(handle)}/follow", "POST", token)
|
||||
|
||||
suspend fun unfollow(handle: String, token: String? = null): Boolean = action("v1/social/profiles/${enc(handle)}/follow", "DELETE", token)
|
||||
|
||||
@@ -66,7 +66,7 @@ class InboxRepository(
|
||||
recipientTag = message.recipientTag.ifBlank { inbox.zuneTag },
|
||||
subject = message.subject,
|
||||
body = message.body,
|
||||
isRead = existing[message.id]?.isRead ?: false,
|
||||
isRead = (existing[message.id]?.isRead ?: false) || message.isRead,
|
||||
createdAt = message.createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ class CloudSocialClientTest {
|
||||
assertEquals("mira", message.senderTag)
|
||||
assertEquals("hello", message.subject)
|
||||
assertEquals("hi there", message.body)
|
||||
assertFalse(message.isRead)
|
||||
assertTrue(message.createdAt > 0)
|
||||
}
|
||||
|
||||
@@ -83,12 +84,16 @@ class CloudSocialClientTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mark read reports the remote cannot accept it`() = runBlocking {
|
||||
fun `mark read posts to the modern route`() = runBlocking {
|
||||
val http = FakeCloudHttp()
|
||||
http.enqueue(204, "")
|
||||
val client = CloudSocialClient({ settings(true) }, { http })
|
||||
|
||||
assertFalse(client.markRead("11111111-1111-1111-1111-111111111111"))
|
||||
assertNull(http.lastPath)
|
||||
assertTrue(client.markRead("11111111-1111-1111-1111-111111111111"))
|
||||
|
||||
assertEquals("v1/social/me/inbox/11111111-1111-1111-1111-111111111111/read", http.lastPath)
|
||||
assertEquals("POST", http.lastMethod)
|
||||
assertEquals("Bearer tok", http.lastAuth)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -70,6 +70,18 @@ class InboxRepositoryTest {
|
||||
assertEquals(1, repo.unreadCount().first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `server read state merges into the cache`() = runBlocking {
|
||||
val http = FakeCloudHttp()
|
||||
enqueueInbox(http, INBOX_READ)
|
||||
val repo = repository(http, enabled = true)
|
||||
|
||||
repo.sync()
|
||||
|
||||
assertTrue(repo.messages().first().single().isRead)
|
||||
assertEquals(0, repo.unreadCount().first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync preserves local read state across a refresh`() = runBlocking {
|
||||
val http = FakeCloudHttp()
|
||||
@@ -121,6 +133,9 @@ class InboxRepositoryTest {
|
||||
|
||||
private companion object {
|
||||
val INBOX_ONE =
|
||||
"""[{"id":"11111111-1111-1111-1111-111111111111","senderTag":"mira","recipientTag":"jane","subject":"hello","body":"hi there","createdAt":"2026-01-02T03:04:05+00:00"}]"""
|
||||
"""[{"id":"11111111-1111-1111-1111-111111111111","senderTag":"mira","recipientTag":"jane","subject":"hello","body":"hi there","isRead":false,"createdAt":"2026-01-02T03:04:05+00:00"}]"""
|
||||
|
||||
val INBOX_READ =
|
||||
"""[{"id":"11111111-1111-1111-1111-111111111111","senderTag":"mira","recipientTag":"jane","subject":"hello","body":"hi there","isRead":true,"createdAt":"2026-01-02T03:04:05+00:00"}]"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,9 +265,9 @@ explicitly documented (long-term / N-A) — no gap unplanned.
|
||||
with cloud enhancement when signed in (`CloudSocialClient` feed/card),
|
||||
local-first `inbox_messages` Room table (v8) + `InboxRepository` with
|
||||
mark-read and offline labels. The cloud reads the same store through the
|
||||
modern bearer route `GET /v1/social/me/inbox` (JSON), so no legacy gateway
|
||||
host proxying is needed; the inbox is still read-only server-side, so read
|
||||
state stays local-only.
|
||||
modern bearer route `GET /v1/social/me/inbox` (JSON) and marks read through
|
||||
`POST /v1/social/me/inbox/{id}/read`, so no legacy gateway host proxying is
|
||||
needed. Room remains the source of truth; server read state merges in.
|
||||
- ✅ **D4** Audiobooks: `AudiobookGrouping` (genre/directory heuristics) →
|
||||
Room v7 `audiobooks`/`audiobook_parts` + `AudiobookRepository` → music
|
||||
crossbar `audiobooks` pivot (book list, parts, resume/bookmark, 0.75–2.0×
|
||||
|
||||
@@ -170,7 +170,6 @@ launch (63/63 crawl failures); fixed by resuming controller calls on
|
||||
service connection — the on-device gate is the reason this shipped correctly.
|
||||
|
||||
**Documented N/A / deferred:** live-radio ±5-min rewind (needs a time-shifting
|
||||
server); cloud inbox mark-read (server serves it read-only; read state is
|
||||
local); the four remaining emulator titles (two corpus gaps, two dead
|
||||
server); the four remaining emulator titles (two corpus gaps, two dead
|
||||
services); desktop i18n remaining locales (deferred registry, 20 shipped);
|
||||
organization repository pinning (profile-level manual step).
|
||||
|
||||
Reference in New Issue
Block a user