Skip to content

Commit dbe8792

Browse files
authored
docs: add QDN avatar bridge reference (#41)
1 parent b4507e2 commit dbe8792

2 files changed

Lines changed: 147 additions & 1 deletion

File tree

‎src/Reference.tsx‎

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,69 @@ if (canFetchAuthorAvatar) {
8181
// descriptor before turning avatar.body into a Blob URL for one <img>.
8282
}
8383
}`,
84+
avatarFeatureDetection: `const actions = await window.qdnRequest({
85+
action: 'SHOW_ACTIONS',
86+
});
87+
88+
const canFetchAccountAvatar = actions.includes('FETCH_ACCOUNT_AVATAR');
89+
const canFetchGroupAvatar = actions.includes('FETCH_GROUP_AVATAR');
90+
const canSetAccountAvatar = actions.includes('SET_ACCOUNT_AVATAR');
91+
const canSetGroupAvatar = actions.includes('SET_GROUP_AVATAR');`,
92+
fetchAvatar: `const MAX_AVATAR_BYTES = 500 * 1024;
93+
94+
async function loadVisibleAccountAvatar(address) {
95+
const result = await window.qdnRequest({
96+
action: 'FETCH_ACCOUNT_AVATAR',
97+
address,
98+
maxBytes: MAX_AVATAR_BYTES,
99+
});
100+
101+
if (result.status === 'PENDING') {
102+
window.setTimeout(() => loadVisibleAccountAvatar(address),
103+
(result.retryAfterSeconds ?? 5) * 1000);
104+
return null;
105+
}
106+
107+
if (result.encoding !== 'base64' ||
108+
result.contentLength > MAX_AVATAR_BYTES ||
109+
!String(result.contentType).startsWith('image/')) return null;
110+
111+
const binary = atob(result.body);
112+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
113+
if (bytes.byteLength !== result.contentLength) return null;
114+
115+
// source is 'POINTER' or 'LEGACY'. descriptor is the pointer tuple when set.
116+
const url = URL.createObjectURL(new Blob([bytes], { type: result.contentType }));
117+
return { source: result.source, descriptor: result.descriptor, url };
118+
}
119+
120+
// Revoke the prior URL when its image is replaced or unmounted.
121+
// Use FETCH_GROUP_AVATAR with { groupId } for a visible group avatar.`,
122+
setAvatarPointer: `// First publish a public, single-file image resource and wait for READY.
123+
// Publishing does not set the avatar pointer by itself.
124+
const avatar = {
125+
service: 'THUMBNAIL',
126+
name: selectedPrimaryName,
127+
identifier: 'avatar',
128+
};
129+
130+
await window.qdnRequest({
131+
action: 'SET_ACCOUNT_AVATAR',
132+
avatar,
133+
});
134+
135+
// A group setter also requires its group id:
136+
await window.qdnRequest({
137+
action: 'SET_GROUP_AVATAR',
138+
groupId,
139+
avatar: {
140+
service: 'THUMBNAIL',
141+
name: groupOwnerPrimaryName,
142+
identifier: \`qortium-group-avatar-v1-\${groupId}\`,
143+
},
144+
});
145+
146+
// Clear either pointer with a separate approved request: { avatar: null }.`,
84147
notifications: `const postId = 'm1abc123';
85148
86149
await window.qdnRequest({
@@ -252,6 +315,7 @@ export default function Reference() {
252315
<a href="#lifecycle">Lifecycle</a>
253316
<a href="#metadata">Metadata</a>
254317
<a href="#bridge">Home bridge</a>
318+
<a href="#avatars">Avatars</a>
255319
<a href="#examples">Examples</a>
256320
</nav>
257321

@@ -513,9 +577,70 @@ export default function Reference() {
513577
</aside>
514578
</section>
515579

580+
<section className="reference-section" id="avatars">
581+
<div className="reference-section__heading">
582+
<p className="reference-kicker">06 · Account and group avatars</p>
583+
<h2>Use the pointer-aware bridge, not a named thumbnail URL</h2>
584+
<p>
585+
Call <code>SHOW_ACTIONS</code> before showing avatar controls. Fetch images only for identities currently
586+
visible in the interface; do not turn a batch identity lookup into a batch image download.
587+
</p>
588+
</div>
589+
590+
<CopyableCode label="Avatar capability detection" snippet="avatarFeatureDetection" />
591+
592+
<div className="reference-grid">
593+
<ReferenceCard title="Safe reads">
594+
<p>
595+
<code>FETCH_ACCOUNT_AVATAR</code> accepts an <code>address</code> (or the selected account), and
596+
<code> FETCH_GROUP_AVATAR</code> accepts a positive <code>groupId</code> or <code>txGroupId</code>.
597+
These reads are public-node safe.
598+
</p>
599+
<p>
600+
A ready result carries base64 <code>body</code>, <code>contentType</code>, <code>contentLength</code>,
601+
<code>source</code>, and an optional <code>{'{ service, name, identifier }'}</code> descriptor. Build an
602+
in-memory Blob URL only; never rebuild a raw node/QDN URL from the response.
603+
</p>
604+
</ReferenceCard>
605+
<ReferenceCard title="Pending and fallback">
606+
<p>
607+
A <code>status: 'PENDING'</code> result is retryable after <code>retryAfterSeconds</code>. Keep initials
608+
visible while it is queued. Missing, malformed, or unsupported results fall back to initials without a
609+
retry loop.
610+
</p>
611+
<p>
612+
An explicit pointer wins and resolves to its latest resource revision. Invalid pointer content fails
613+
closed. A <code>source: 'LEGACY'</code> result is compatibility data, not an on-chain pointer; do not use
614+
<code> avatarSrc</code> or <code>avatarUrl</code> as an authoritative image source.
615+
</p>
616+
</ReferenceCard>
617+
<ReferenceCard title="Authoring">
618+
<p>
619+
Publish the public single-file image first, wait for that resource to become <code>READY</code>, then
620+
call the matching setter. The publish and pointer assignment are separate, single-request approvals.
621+
</p>
622+
<p>
623+
Both setters accept <code>{'{ service, name, identifier }'}</code> or <code>avatar: null</code> to clear.
624+
Account assignment targets the selected account; group assignment also includes <code>groupId</code>.
625+
</p>
626+
</ReferenceCard>
627+
</div>
628+
629+
<CopyableCode label="Fetch one visible account avatar" snippet="fetchAvatar" />
630+
<CopyableCode label="Set or clear an avatar pointer" snippet="setAvatarPointer" />
631+
632+
<aside className="reference-callout">
633+
<strong>Bounded, mutable image data.</strong>
634+
<p>
635+
Home validates raster image bytes and caps avatar responses at 500 KiB. A pointer is intentionally mutable,
636+
so cache it briefly and revalidate rather than treating a descriptor as an immutable signature.
637+
</p>
638+
</aside>
639+
</section>
640+
516641
<section className="reference-section" id="examples">
517642
<div className="reference-section__heading">
518-
<p className="reference-kicker">06 · Copyable examples</p>
643+
<p className="reference-kicker">07 · Copyable examples</p>
519644
<h2>Publish, discover, fetch, and delete</h2>
520645
<p>
521646
These examples use the Qortium Home bridge. Substitute real selected-account names and generated short

‎src/reference.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,25 @@ describe('Help developer reference', () => {
4747
expect(html).toContain('Author avatars');
4848
expect(html).toContain('never builds a direct thumbnail URL');
4949
});
50+
51+
it('documents the pointer-aware avatar contract and safe fallback behaviour', () => {
52+
const html = renderToStaticMarkup(<Reference />);
53+
54+
expect(html).toContain('Account and group avatars');
55+
expect(html).toContain("status: &#x27;PENDING&#x27;");
56+
expect(html).toContain('500 KiB');
57+
expect(html).toContain('latest resource revision');
58+
expect(html).toContain('avatar: null');
59+
});
60+
61+
it('provides copyable feature detection, read, and setter examples for avatars', () => {
62+
expect(REFERENCE_SNIPPETS.avatarFeatureDetection).toContain("'FETCH_ACCOUNT_AVATAR'");
63+
expect(REFERENCE_SNIPPETS.avatarFeatureDetection).toContain("'SET_GROUP_AVATAR'");
64+
expect(REFERENCE_SNIPPETS.fetchAvatar).toContain("status === 'PENDING'");
65+
expect(REFERENCE_SNIPPETS.fetchAvatar).toContain('retryAfterSeconds');
66+
expect(REFERENCE_SNIPPETS.fetchAvatar).toContain("source is 'POINTER' or 'LEGACY'");
67+
expect(REFERENCE_SNIPPETS.fetchAvatar).toContain('URL.createObjectURL(new Blob');
68+
expect(REFERENCE_SNIPPETS.setAvatarPointer).toContain("action: 'SET_ACCOUNT_AVATAR'");
69+
expect(REFERENCE_SNIPPETS.setAvatarPointer).toContain('avatar: null');
70+
});
5071
});

0 commit comments

Comments
 (0)