Use public Discord collectibles catalog.
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s

Remove local token setup in favor of the cached published catalog and document the collectible profile experience in the latest update notes.
This commit is contained in:
2026-08-24 19:29:48 +10:00
parent d59945eb4c
commit 87c9f59826
6 changed files with 55 additions and 97 deletions

22
public/update/4.11.177.md Normal file
View File

@@ -0,0 +1,22 @@
---
version: 4.11.177
title: Profile colors & banners
date: 2026-08-23
summary: Username colors now follow MSC4522 with separate dark and light theme pickers.
---
## Profile colors (MSC4522)
Username colors are now stored on your Matrix profile instead of inside avatar images. In turn **will need to be updated in your Account settings**, this is now cross compatible with Element + Cinny
- Set **separate colors for dark and light themes** in Settings → Account → Profile.
- Per-room username overrides are read from member profile data when available.
![Paarrot](./images/usernamecolors.png)
## Profile banners (MSC4133)
Profile banners now live on your Matrix profile via `m.banner_url` (MSC4133), not embedded in avatar metadata.
- Upload or remove a banner independently from your avatar.
- Other clients that support MSC4133 can read your banner directly.

View File

@@ -1,22 +1,22 @@
---
version: 4.11.177
title: Profile colors & banners
date: 2026-08-23
summary: Username colors now follow MSC4522 with separate dark and light theme pickers.
version: 4.11.185
title: Discord collectibles
date: 2026-08-24
summary: Add Discord-style profile effects, nameplates, and avatar decorations to Paarrot profiles.
---
## Profile colors (MSC4522)
## Discord collectibles
Username colors are now stored on your Matrix profile instead of inside avatar images. In turn **will need to be updated in your Account settings**, this is now cross compatible with Element + Cinny
- Browse and apply profile effects, animated nameplates, and avatar decorations in Settings → Account → Profile.
- Collectible assets are uploaded to your Matrix profile, so other Paarrot users can see them without Discord.
- The catalog now comes from a public git, and no Discord token is required in Paarrot for the cosmetics.
- Profile-effect previews play their intro and loop animations, with a mini profile preview for each effect.
- Set **separate colors for dark and light themes** in Settings → Account → Profile.
- Per-room username overrides are read from member profile data when available.
![Paarrot](./images/usernamecolors.png)
![Discord Cosmetics 1](./images/discordcosmetics1.png) ![Discord Cosmetics 2](./images/discordcosmetics2.png)
## Profile banners (MSC4133)
## Profile presentation
Profile banners now live on your Matrix profile via `m.banner_url` (MSC4133), not embedded in avatar metadata.
- Upload or remove a banner independently from your avatar.
- Other clients that support MSC4133 can read your banner directly.
- Avatar decorations render in messages, profile cards, and the bottom sidebar avatar.
- Nameplates to the right of messages, beneath timestamps and can be displayed behind the bottom sidebar avatar with opacity settings.
- Customize the sidebar nameplate opacity in Settings → General → Collectibles.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -338,9 +338,6 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
);
const previewDisplayName = profile.displayName;
const [kind, setKind] = useState<CollectibleKind>('profile_effect');
const [hasToken, setHasToken] = useState(false);
const [tokenInput, setTokenInput] = useState('');
const [tokenSaving, setTokenSaving] = useState(false);
const [catalogLoading, setCatalogLoading] = useState(false);
const [catalogError, setCatalogError] = useState<string>();
const [items, setItems] = useState<DiscordCollectibleItem[]>([]);
@@ -352,12 +349,6 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
const desktopAvailable = isElectronCollectiblesAvailable();
const refreshTokenState = useCallback(async () => {
if (!desktopAvailable) return;
const result = await window.electron!.discordCollectibles!.hasToken();
setHasToken(Boolean(result?.data));
}, [desktopAvailable]);
const loadCatalog = useCallback(async (force = false) => {
if (!desktopAvailable) return;
setCatalogLoading(true);
@@ -376,14 +367,8 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
}, [desktopAvailable]);
useEffect(() => {
refreshTokenState();
}, [refreshTokenState]);
useEffect(() => {
if (hasToken) {
loadCatalog();
}
}, [hasToken, loadCatalog]);
loadCatalog();
}, [loadCatalog]);
const current = collectibles[KIND_FIELD[kind]];
@@ -416,27 +401,6 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
});
}, [filteredGroups]);
const handleSaveToken = async () => {
if (!desktopAvailable) return;
setTokenSaving(true);
setCatalogError(undefined);
try {
await window.electron!.discordCollectibles!.setToken(tokenInput);
setTokenInput('');
await refreshTokenState();
} catch (e) {
setCatalogError(e instanceof Error ? e.message : 'Failed to save token.');
}
setTokenSaving(false);
};
const handleClearToken = async () => {
if (!desktopAvailable) return;
await window.electron!.discordCollectibles!.clearToken();
setItems([]);
await refreshTokenState();
};
const handleApply = async (item: DiscordCollectibleItem) => {
setApplyingId(item.id);
setError(undefined);
@@ -481,48 +445,24 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
return (
<Box direction="Column" gap="300">
<Text size="T200" style={{ opacity: 0.8 }}>
Browse Discord shop collectibles live, then download and upload only what you pick to Matrix.
Your Discord token stays on this device and is only used to list the shop catalog.
Browse a community-published Discord collectibles catalog, then download and upload only what
you pick to Matrix.
</Text>
{!hasToken ? (
<Box direction="Column" gap="200">
<Text size="T300">Discord token (required to list the shop)</Text>
<Input
size="300"
variant="Secondary"
style={{ width: '100%' }}
type="password"
placeholder="User token or Bot xxxxx"
value={tokenInput}
onChange={(e) => setTokenInput(e.target.value)}
/>
<Button
size="300"
variant="Primary"
fill="Solid"
radii="300"
onClick={handleSaveToken}
disabled={tokenSaving || !tokenInput.trim()}
>
<Text size="B300">{tokenSaving ? 'Saving…' : 'Save token'}</Text>
</Button>
{catalogError && <Text size="T200" style={{ color: color.Critical.Main }}>{catalogError}</Text>}
</Box>
) : (
<Box gap="200" alignItems="Center" wrap="Wrap">
<Text size="T200" style={{ opacity: 0.8 }}>Discord token saved on this device.</Text>
<Button size="300" variant="Secondary" fill="Soft" radii="300" onClick={() => loadCatalog(true)} disabled={catalogLoading}>
<Text size="B300">{catalogLoading ? 'Refreshing…' : 'Refresh catalog'}</Text>
</Button>
<Button size="300" variant="Critical" fill="Soft" radii="300" onClick={handleClearToken}>
<Text size="B300">Remove token</Text>
</Button>
</Box>
)}
<Box gap="200" alignItems="Center" wrap="Wrap">
<Button
size="300"
variant="Secondary"
fill="Soft"
radii="300"
onClick={() => loadCatalog(true)}
disabled={catalogLoading}
>
<Text size="B300">{catalogLoading ? 'Refreshing…' : 'Refresh catalog'}</Text>
</Button>
</Box>
{hasToken && (
<>
<>
<Box gap="200" wrap="Wrap">
{KIND_TABS.map((tab) => (
<Button
@@ -559,7 +499,7 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
{catalogLoading && (
<Box gap="200" alignItems="Center">
<Spinner size="100" variant="Secondary" />
<Text size="T200">Loading catalog from Discord</Text>
<Text size="T200">Loading collectibles catalog</Text>
</Box>
)}
@@ -606,8 +546,7 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
{!catalogLoading && filteredGroups.length === 0 && (
<Text size="T200" style={{ opacity: 0.7 }}>No items match your search.</Text>
)}
</>
)}
</>
</Box>
);
}

3
src/ext.d.ts vendored
View File

@@ -114,9 +114,6 @@ interface ElectronAPI {
}) => void) => void;
};
discordCollectibles?: {
hasToken: () => Promise<{ success: boolean; data?: boolean; error?: string }>;
setToken: (token: string) => Promise<{ success: boolean; error?: string }>;
clearToken: () => Promise<{ success: boolean; error?: string }>;
fetchCatalog: (force?: boolean) => Promise<{
success: boolean;
data?: { items: unknown[]; fetchedAt: string };