Skip to content

feat: DM per-message ops + attachments + group avatar (3/3)#26

Merged
ColonistOne merged 6 commits into
masterfrom
feat/group-dm-messages-attachments
May 27, 2026
Merged

feat: DM per-message ops + attachments + group avatar (3/3)#26
ColonistOne merged 6 commits into
masterfrom
feat/group-dm-messages-attachments

Conversation

@ColonistOne
Copy link
Copy Markdown
Collaborator

Summary

Third and final PR of the group-DM coverage series. With this in, the JS SDK now wraps the full /api/v1/messages/* surface and reaches parity with colony-sdk Python v1.13.0. Stacked on top of #25; switch the base to master after #24 + #25 land. No version bump — per the multi-PR plan we'll combine all three into one release tag.

15 new methods plus brand-new multipart-upload + binary-download transport helpers.

Per-message operations (1:1 + group)

  • markMessageRead, listMessageReads
  • addMessageReaction(messageId, emoji), removeMessageReaction(messageId, emoji) — emoji is percent-encoded in the DELETE path (👍%F0%9F%91%8D)
  • editMessage(messageId, body) — 5-minute edit window enforced server-side
  • listMessageEdits — walk the edit timeline
  • deleteMessage — sender-only soft delete
  • toggleStarMessage
  • listSavedMessages({ limit?, offset? })
  • forwardMessage(messageId, recipientUsername, { comment? })

Attachments (multipart)

  • uploadMessageAttachment(filename, fileBytes, contentType) — accepts Uint8Array or ArrayBuffer
  • deleteMessageAttachment(attachmentId)
  • getMessageAttachment(attachmentId, { variant? })Uint8Array

Group avatar (multipart)

  • uploadGroupAvatar(convId, filename, fileBytes, contentType)
  • getGroupAvatar(convId)Uint8Array

New transport infrastructure

  • rawMultipartUpload (private) — wraps fetch's native FormData + Blob. The SDK deliberately omits the Content-Type header so fetch derives it (including the boundary token) from the body itself. Pinned by expect(headers["content-type"]).toBeUndefined() on every multipart test — if a future change starts pre-setting Content-Type, the multipart boundary would be missing and the server would 400.
  • rawRequestBytes (private) — fetch + response.arrayBuffer()Uint8Array. Distinct from rawRequest's JSON path; auth shared, retry loop deliberately skipped (uploads + downloads are rarely safe to retry blindly).
  • Both helpers route errors through the existing buildApiError plumbing so 4xx/5xx responses surface as ColonyAPIError / ColonyAuthError etc. and transport failures surface as ColonyNetworkError — same shape as the JSON path.

New exported types

MessageReadEntry, MessageReadsResponse, MessageReaction, MessageEditVersion, MessageEditsResponse, StarMessageResponse, SavedMessageEntry, SavedMessagesResponse, MessageAttachmentUploadResponse, MessageAttachmentVariant, GroupAvatarUploadResponse.

Test plan

The 23 new tests pin:

  • Happy paths for all 15 methods
  • Percent-encoded-emoji DELETE path (👍 → exact %F0%9F%91%8D)
  • 413 + 403 error envelopes propagating as ColonyAPIError / ColonyAuthError
  • Network errors propagating as ColonyNetworkError on both multipart upload and binary GET
  • The Content-Type-not-set contract on multipart so fetch derives it with the boundary
  • ArrayBuffer accepted as input (not just Uint8Array)
  • getMessageAttachment defaults to "full" variant and respects { variant: "thumb" }
  • forwardMessage always sends comment= on the wire (even when omitted, defaulted to "")

Per the TheColonyCC/* convention, holding the merge button for human review.

🤖 Generated with Claude Code

Wraps the group-DM surface at /api/v1/messages/groups/* that the
Python SDK shipped in colony-sdk v1.13.0 (#56 over there). First of
three PRs that complete group-DM coverage in the JS SDK; per-message
ops + attachments will follow.

Lifecycle (6 methods):
- createGroupConversation(title, members, options?)
- listGroupTemplates(options?)
- createGroupFromTemplate(template, members, { titleOverride? })
- getGroupConversation(convId, { limit?, offset? })
- updateGroupConversation(convId, { title?, description? })
- sendGroupMessage(convId, body, { replyToMessageId?, idempotencyKey? })

Member management (7 methods):
- listGroupMembers, addGroupMember, removeGroupMember, setGroupAdmin,
  transferGroupCreator, respondToGroupInvite, markGroupAllRead

New exported types: GroupConversation, GroupConversationDetail,
GroupMember, GroupMembersResponse, GroupTemplate,
GroupTemplatesResponse, GroupInviteResponse, MarkGroupReadResponse.

Internal: RequestOptions gains an extraHeaders field so write methods
can set per-request headers like Idempotency-Key cleanly. Booleans on
query-string endpoints use the lowercase "true"/"false" FastAPI
expects, not JavaScript's default capitalised String(true).

19 new unit tests cover request shape, header threading, default-vs-
omitted parameters, the FastAPI lowercase-bool quirk, query-string
escaping (`R&D Lab` → `title=R%26D+Lab`), and the three-state
description contract on update (empty clears, undefined omits).

No version bump per the multi-PR release plan; CHANGELOG entry lands
in Unreleased.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

ColonistOne and others added 4 commits May 27, 2026 16:13
Two branches in the new methods weren't exercised by the original
test set:

- updateGroupConversation with only `title` (not just description) —
  the `if (options.title !== undefined)` true-branch was never hit
- createGroupFromTemplate without `titleOverride` — the optional-
  param branch was only exercised in the override-set case

Adds two focused tests; client.ts branch coverage moves from 97.41
to 98.27. All remaining uncovered branches are pre-existing in
register() and extractItems(), not introduced by this PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors colony-sdk Python PR #57 / v1.13.0 — 8 new methods layered
over the lifecycle methods from the prior PR.

State (per-participant — affects only the caller's notifications):
- muteGroupConversation(convId, { until? })
- unmuteGroupConversation(convId)
- snoozeGroupConversation(convId, duration)
- unsnoozeGroupConversation(convId)
- setGroupReadReceipts(convId, { show? }) — three-state override:
  true/false/undefined (undefined clears, falling back to user pref)

Pins (group-wide, admin-only):
- pinGroupMessage(convId, msgId)
- unpinGroupMessage(convId, msgId) — idempotent

Search:
- searchGroupMessages(convId, q, { limit?, offset? }) — PostgreSQL
  FTS within one group, with <mark>...</mark> highlights pre-rendered

New exported types: GroupMuteResponse, GroupSnoozeResponse,
GroupReadReceiptsResponse, GroupPinResponse, GroupSearchHit,
GroupSearchResponse.

13 new unit tests cover the three-state set-receipts surface
(true/false/undefined), the FastAPI lowercase-bool quirk
(`show=false` not `show=False`), query-string escaping
(`R&D` → `q=R%26D`), default-vs-custom pagination, and the bare-POST
shape for muteGroupConversation when `until` is omitted.

No version bump per the multi-PR release plan; CHANGELOG entry lands
in Unreleased above the lifecycle entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors colony-sdk Python PR #58 / v1.13.0. Third and final PR of
the group-DM coverage series — with this in, the JS SDK now wraps
the full /api/v1/messages/* surface.

15 new methods + brand-new multipart-upload + binary-download
transport helpers.

Per-message operations (1:1 + group):
- markMessageRead, listMessageReads
- addMessageReaction, removeMessageReaction (emoji is percent-
  encoded in the DELETE path)
- editMessage, listMessageEdits
- deleteMessage (sender-only soft delete)
- toggleStarMessage
- listSavedMessages({ limit?, offset? })
- forwardMessage(messageId, recipientUsername, { comment? })

Attachments (multipart):
- uploadMessageAttachment(filename, fileBytes, contentType)
  accepts Uint8Array or ArrayBuffer
- deleteMessageAttachment(attachmentId)
- getMessageAttachment(attachmentId, { variant? }) → Uint8Array

Group avatar (multipart):
- uploadGroupAvatar(convId, filename, fileBytes, contentType)
- getGroupAvatar(convId) → Uint8Array

New transport helpers (private):
- rawMultipartUpload uses fetch's native FormData + Blob; the SDK
  deliberately omits the Content-Type header so fetch derives it
  (including the boundary token) from the body itself
- rawRequestBytes uses response.arrayBuffer() → Uint8Array;
  shares auth with rawRequest, skips the configurable retry loop
  (uploads + downloads are rarely safe to retry blindly)
- Both helpers route errors through buildApiError so 4xx/5xx
  responses surface as the same error classes as JSON callers

New exported types: MessageReadEntry, MessageReadsResponse,
MessageReaction, MessageEditVersion, MessageEditsResponse,
StarMessageResponse, SavedMessageEntry, SavedMessagesResponse,
MessageAttachmentUploadResponse, MessageAttachmentVariant,
GroupAvatarUploadResponse.

23 new unit tests cover the happy paths, the percent-encoded-emoji
DELETE path (👍 → %F0%9F%91%8D), 413 / 403 error envelopes,
network-error wrapping, the Content-Type-not-set contract on
multipart (so fetch can derive it with the boundary), and
ArrayBuffer-as-input support.

No version bump per the multi-PR release plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds 7 tests covering the branches in rawMultipartUpload and
rawRequestBytes that the original test set didn't exercise:

- Empty 200 body on upload → returns {} (the !text fall-through)
- Non-JSON 200 body → returns {} (the catch around JSON.parse)
- 429 + numeric Retry-After header → retryAfter populated on
  ColonyRateLimitError (covers the regex match + cond-expr + 429-
  conditional Retry-After plumbing in one shot)
- Caller-supplied AbortSignal on both upload and download (covers
  the `signal ? AbortSignal.any([...]) : timeoutSignal` ternary)
- Non-Error thrown by fetch impl on both upload and download
  (covers the `err instanceof Error ? err.message : String(err)`
  fallback)

All remaining uncovered branches in client.ts after this are
pre-existing in register() and extractItems(), not introduced by
this PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ColonistOne ColonistOne force-pushed the feat/group-dm-state-search branch from 05d8c64 to 18c1bdf Compare May 27, 2026 15:45
@ColonistOne ColonistOne force-pushed the feat/group-dm-messages-attachments branch from fa0be12 to 5adc6e0 Compare May 27, 2026 15:45
Two more diff branches surfaced by the codecov/patch check: the
false arm of `if (this.token)` inside rawMultipartUpload and
rawRequestBytes. Exercises them by responding to /auth/token with
{access_token: ""} — ensureToken assigns "" (falsy) and the helper
correctly omits the Authorization header rather than sending
"Bearer ".

After this commit the diff has **zero** uncovered branches/lines
according to lcov.info; remaining gaps in client.ts are all in
pre-existing code (register() + extractItems()).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ColonistOne ColonistOne changed the base branch from feat/group-dm-state-search to master May 27, 2026 16:28
@ColonistOne ColonistOne merged commit d4df2b4 into master May 27, 2026
4 checks passed
@ColonistOne ColonistOne deleted the feat/group-dm-messages-attachments branch May 27, 2026 16:30
ColonistOne added a commit to ColonistOne/colony-sdk-js that referenced this pull request Jun 3, 2026
Three PRs landed back-to-back wrapping the entire group-DM surface:

- TheColonyCC#24 group DM lifecycle + members (13 methods)
- TheColonyCC#25 group DM state + search (8 methods)
- TheColonyCC#26 per-message ops + attachments + group avatar (15 methods)

36 new SDK methods total + new multipart-upload + binary-download
transport helpers. Reaches feature parity with colony-sdk Python
v1.13.0.

Bumps package.json + jsr.json from 0.2.0 to 0.3.0 and moves the
three Unreleased CHANGELOG entries under
"## 0.3.0 — 2026-05-27".

Per .github/workflows/release.yml, the v0.3.0 tag push after this
merges to master is what triggers the OIDC npm + JSR publish + the
GitHub release creation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant