Compare commits
1 Commits
d59945eb4c
...
87c9f59826
| Author | SHA1 | Date | |
|---|---|---|---|
| 87c9f59826 |
22
public/update/4.11.177.md
Normal file
22
public/update/4.11.177.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
---
|
---
|
||||||
version: 4.11.177
|
version: 4.11.185
|
||||||
title: Profile colors & banners
|
title: Discord collectibles
|
||||||
date: 2026-08-23
|
date: 2026-08-24
|
||||||
summary: Username colors now follow MSC4522 with separate dark and light theme pickers.
|
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.
|
|
||||||
|
|
||||||

|
 
|
||||||
|
|
||||||
## Profile banners (MSC4133)
|
## Profile presentation
|
||||||
|
|
||||||
Profile banners now live on your Matrix profile via `m.banner_url` (MSC4133), not embedded in avatar metadata.
|
- 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.
|
||||||
- Upload or remove a banner independently from your avatar.
|
- Customize the sidebar nameplate opacity in Settings → General → Collectibles.
|
||||||
- Other clients that support MSC4133 can read your banner directly.
|
|
||||||
|
|||||||
BIN
public/update/images/discordcosmetics1.png
Normal file
BIN
public/update/images/discordcosmetics1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/update/images/discordcosmetics2.png
Normal file
BIN
public/update/images/discordcosmetics2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
@@ -338,9 +338,6 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
|
|||||||
);
|
);
|
||||||
const previewDisplayName = profile.displayName;
|
const previewDisplayName = profile.displayName;
|
||||||
const [kind, setKind] = useState<CollectibleKind>('profile_effect');
|
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 [catalogLoading, setCatalogLoading] = useState(false);
|
||||||
const [catalogError, setCatalogError] = useState<string>();
|
const [catalogError, setCatalogError] = useState<string>();
|
||||||
const [items, setItems] = useState<DiscordCollectibleItem[]>([]);
|
const [items, setItems] = useState<DiscordCollectibleItem[]>([]);
|
||||||
@@ -352,12 +349,6 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
|
|||||||
|
|
||||||
const desktopAvailable = isElectronCollectiblesAvailable();
|
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) => {
|
const loadCatalog = useCallback(async (force = false) => {
|
||||||
if (!desktopAvailable) return;
|
if (!desktopAvailable) return;
|
||||||
setCatalogLoading(true);
|
setCatalogLoading(true);
|
||||||
@@ -376,14 +367,8 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
|
|||||||
}, [desktopAvailable]);
|
}, [desktopAvailable]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshTokenState();
|
loadCatalog();
|
||||||
}, [refreshTokenState]);
|
}, [loadCatalog]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasToken) {
|
|
||||||
loadCatalog();
|
|
||||||
}
|
|
||||||
}, [hasToken, loadCatalog]);
|
|
||||||
|
|
||||||
const current = collectibles[KIND_FIELD[kind]];
|
const current = collectibles[KIND_FIELD[kind]];
|
||||||
|
|
||||||
@@ -416,27 +401,6 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
|
|||||||
});
|
});
|
||||||
}, [filteredGroups]);
|
}, [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) => {
|
const handleApply = async (item: DiscordCollectibleItem) => {
|
||||||
setApplyingId(item.id);
|
setApplyingId(item.id);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
@@ -481,48 +445,24 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
|
|||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="300">
|
<Box direction="Column" gap="300">
|
||||||
<Text size="T200" style={{ opacity: 0.8 }}>
|
<Text size="T200" style={{ opacity: 0.8 }}>
|
||||||
Browse Discord shop collectibles live, then download and upload only what you pick to Matrix.
|
Browse a community-published Discord collectibles catalog, then download and upload only what
|
||||||
Your Discord token stays on this device and is only used to list the shop catalog.
|
you pick to Matrix.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{!hasToken ? (
|
<Box gap="200" alignItems="Center" wrap="Wrap">
|
||||||
<Box direction="Column" gap="200">
|
<Button
|
||||||
<Text size="T300">Discord token (required to list the shop)</Text>
|
size="300"
|
||||||
<Input
|
variant="Secondary"
|
||||||
size="300"
|
fill="Soft"
|
||||||
variant="Secondary"
|
radii="300"
|
||||||
style={{ width: '100%' }}
|
onClick={() => loadCatalog(true)}
|
||||||
type="password"
|
disabled={catalogLoading}
|
||||||
placeholder="User token or Bot xxxxx"
|
>
|
||||||
value={tokenInput}
|
<Text size="B300">{catalogLoading ? 'Refreshing…' : 'Refresh catalog'}</Text>
|
||||||
onChange={(e) => setTokenInput(e.target.value)}
|
</Button>
|
||||||
/>
|
</Box>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasToken && (
|
<>
|
||||||
<>
|
|
||||||
<Box gap="200" wrap="Wrap">
|
<Box gap="200" wrap="Wrap">
|
||||||
{KIND_TABS.map((tab) => (
|
{KIND_TABS.map((tab) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -559,7 +499,7 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
|
|||||||
{catalogLoading && (
|
{catalogLoading && (
|
||||||
<Box gap="200" alignItems="Center">
|
<Box gap="200" alignItems="Center">
|
||||||
<Spinner size="100" variant="Secondary" />
|
<Spinner size="100" variant="Secondary" />
|
||||||
<Text size="T200">Loading catalog from Discord…</Text>
|
<Text size="T200">Loading collectibles catalog…</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -606,8 +546,7 @@ export function CollectiblesSection({ collectibles, onApply }: CollectiblesSecti
|
|||||||
{!catalogLoading && filteredGroups.length === 0 && (
|
{!catalogLoading && filteredGroups.length === 0 && (
|
||||||
<Text size="T200" style={{ opacity: 0.7 }}>No items match your search.</Text>
|
<Text size="T200" style={{ opacity: 0.7 }}>No items match your search.</Text>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/ext.d.ts
vendored
3
src/ext.d.ts
vendored
@@ -114,9 +114,6 @@ interface ElectronAPI {
|
|||||||
}) => void) => void;
|
}) => void) => void;
|
||||||
};
|
};
|
||||||
discordCollectibles?: {
|
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<{
|
fetchCatalog: (force?: boolean) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: { items: unknown[]; fetchedAt: string };
|
data?: { items: unknown[]; fetchedAt: string };
|
||||||
|
|||||||
Reference in New Issue
Block a user