detached 7e3029f

refactor(aim): remove emoji buddy icon presets

The emoji-on-color presets were not period accurate. Everyone now shows
the AIM running man in the buddy list, IM header and Get Info window.
Drops users.buddyIcon from the schema and updateProfile.
commit
7e3029fd5345f65c7bf9762246595deb239f118e
author
Kit · · 2026-09-27 14:06 UTC
parent
e2dff43
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
10 files · +9 -125
M src/convex/aim.ts +2 -9
@@ -39,7 +39,6 @@ export function toPublicUser(user: Doc<'users'>) {
3939 avatarUrl: user.avatarUrl,
4040 lastSeen: user.lastSeen,
4141 awayMessage: status === 'away' ? user.awayMessage : undefined,
42- buddyIcon: user.buddyIcon,
4342 createdAt: user.createdAt
4443 };
4544 }
@@ -176,8 +175,7 @@ export const getRoomMessages = authQuery({
176175 id: sender._id,
177176 nickname: sender.nickname,
178177 status: computeEffectiveStatus(sender.status, sender.lastSeen),
179- avatarUrl: sender.avatarUrl,
180- buddyIcon: sender.buddyIcon
178+ avatarUrl: sender.avatarUrl
181179 }
182180 : null
183181 };
@@ -214,7 +212,6 @@ export const getMe = authQuery({
214212 status: u.status,
215213 awayMessage: u.awayMessage,
216214 profile: u.profile,
217- buddyIcon: u.buddyIcon,
218215 createdAt: u.createdAt
219216 };
220217 }
@@ -240,15 +237,11 @@ export const setAwayMessage = authMutation({
240237 });
241238
242239 export const updateProfile = authMutation({
243- args: { profile: v.optional(v.string()), buddyIcon: v.optional(v.string()) },
240+ args: { profile: v.optional(v.string()) },
244241 handler: async (ctx, args) => {
245242 const patch: Partial<Doc<'users'>> = {};
246243 if (args.profile !== undefined)
247244 patch.profile = args.profile.trim().slice(0, MAX_PROFILE_LENGTH);
248- if (args.buddyIcon !== undefined) {
249- if (!/^[a-z0-9-]{1,32}$/.test(args.buddyIcon)) throw new Error('Invalid buddy icon');
250- patch.buddyIcon = args.buddyIcon;
251- }
252245 await ctx.db.patch(ctx.user._id, patch);
253246 return { success: true };
254247 }
@@ -39,7 +39,6 @@ export function toPublicUser(user: Doc<'users'>) {
39 avatarUrl: user.avatarUrl,39 avatarUrl: user.avatarUrl,
40 lastSeen: user.lastSeen,40 lastSeen: user.lastSeen,
41 awayMessage: status === 'away' ? user.awayMessage : undefined,41 awayMessage: status === 'away' ? user.awayMessage : undefined,
42- buddyIcon: user.buddyIcon,
43 createdAt: user.createdAt42 createdAt: user.createdAt
44 };43 };
45 }44 }
@@ -176,8 +175,7 @@ export const getRoomMessages = authQuery({
176 id: sender._id,175 id: sender._id,
177 nickname: sender.nickname,176 nickname: sender.nickname,
178 status: computeEffectiveStatus(sender.status, sender.lastSeen),177 status: computeEffectiveStatus(sender.status, sender.lastSeen),
179- avatarUrl: sender.avatarUrl,178+ avatarUrl: sender.avatarUrl
180- buddyIcon: sender.buddyIcon
181 }179 }
182 : null180 : null
183 };181 };
@@ -214,7 +212,6 @@ export const getMe = authQuery({
214 status: u.status,212 status: u.status,
215 awayMessage: u.awayMessage,213 awayMessage: u.awayMessage,
216 profile: u.profile,214 profile: u.profile,
217- buddyIcon: u.buddyIcon,
218 createdAt: u.createdAt215 createdAt: u.createdAt
219 };216 };
220 }217 }
@@ -240,15 +237,11 @@ export const setAwayMessage = authMutation({
240 });237 });
241 238
242 export const updateProfile = authMutation({239 export const updateProfile = authMutation({
243- args: { profile: v.optional(v.string()), buddyIcon: v.optional(v.string()) },240+ args: { profile: v.optional(v.string()) },
244 handler: async (ctx, args) => {241 handler: async (ctx, args) => {
245 const patch: Partial<Doc<'users'>> = {};242 const patch: Partial<Doc<'users'>> = {};
246 if (args.profile !== undefined)243 if (args.profile !== undefined)
247 patch.profile = args.profile.trim().slice(0, MAX_PROFILE_LENGTH);244 patch.profile = args.profile.trim().slice(0, MAX_PROFILE_LENGTH);
248- if (args.buddyIcon !== undefined) {
249- if (!/^[a-z0-9-]{1,32}$/.test(args.buddyIcon)) throw new Error('Invalid buddy icon');
250- patch.buddyIcon = args.buddyIcon;
251- }
252 await ctx.db.patch(ctx.user._id, patch);245 await ctx.db.patch(ctx.user._id, patch);
253 return { success: true };246 return { success: true };
254 }247 }
M src/convex/schema.ts +1 -2
@@ -12,8 +12,7 @@ export default defineSchema({
1212 lastReadMentionTimestamp: v.optional(v.number()), // Timestamp of last read mention
1313 // AIM profile
1414 awayMessage: v.optional(v.string()), // Shown while status === 'away'
15- profile: v.optional(v.string()), // Free text "Get Info" profile
16- buddyIcon: v.optional(v.string()) // Preset icon id (see $lib/aim/buddy-icons)
15+ profile: v.optional(v.string()) // Free text "Get Info" profile
1716 })
1817 .index('by_nickname', ['nickname'])
1918 .index('by_status', ['status']),
@@ -12,8 +12,7 @@ export default defineSchema({
12 lastReadMentionTimestamp: v.optional(v.number()), // Timestamp of last read mention12 lastReadMentionTimestamp: v.optional(v.number()), // Timestamp of last read mention
13 // AIM profile13 // AIM profile
14 awayMessage: v.optional(v.string()), // Shown while status === 'away'14 awayMessage: v.optional(v.string()), // Shown while status === 'away'
15- profile: v.optional(v.string()), // Free text "Get Info" profile15+ profile: v.optional(v.string()) // Free text "Get Info" profile
16- buddyIcon: v.optional(v.string()) // Preset icon id (see $lib/aim/buddy-icons)
17 })16 })
18 .index('by_nickname', ['nickname'])17 .index('by_nickname', ['nickname'])
19 .index('by_status', ['status']),18 .index('by_status', ['status']),
D src/lib/aim/buddy-icons.ts +0 -35
@@ -1,35 +0,0 @@
1-/**
2- * Preset buddy icons (48x48 in the original AIM). Rendered as emoji on a
3- * coloured tile so no image assets are needed.
4- */
5-export interface BuddyIconPreset {
6- id: string;
7- emoji: string;
8- label: string;
9- bg: string;
10-}
11-
12-export const BUDDY_ICONS: BuddyIconPreset[] = [
13- { id: 'runner', emoji: '🏃', label: 'Running man', bg: '#f6c700' },
14- { id: 'smiley', emoji: '🙂', label: 'Smiley', bg: '#ffe680' },
15- { id: 'cool', emoji: '😎', label: 'Cool', bg: '#8fd3ff' },
16- { id: 'cat', emoji: '🐱', label: 'Chat', bg: '#ffd1dc' },
17- { id: 'dog', emoji: '🐶', label: 'Chien', bg: '#d9c3a5' },
18- { id: 'alien', emoji: '👽', label: 'Alien', bg: '#b6ffb0' },
19- { id: 'skull', emoji: '💀', label: 'Crâne', bg: '#dcdcdc' },
20- { id: 'fire', emoji: '🔥', label: 'Feu', bg: '#ffb27a' },
21- { id: 'star', emoji: '⭐', label: 'Étoile', bg: '#3a4fbf' },
22- { id: 'heart', emoji: '💜', label: 'Cœur', bg: '#f7c6ff' },
23- { id: 'music', emoji: '🎵', label: 'Musique', bg: '#c4f0ff' },
24- { id: 'game', emoji: '🎮', label: 'Jeux', bg: '#c9ffd6' },
25- { id: 'pizza', emoji: '🍕', label: 'Pizza', bg: '#ffe0b3' },
26- { id: 'rocket', emoji: '🚀', label: 'Fusée', bg: '#1f2a5a' },
27- { id: 'ghost', emoji: '👻', label: 'Fantôme', bg: '#eaeaff' },
28- { id: 'robot', emoji: '🤖', label: 'Robot', bg: '#cfd8dc' }
29-];
30-
31-export const DEFAULT_BUDDY_ICON = BUDDY_ICONS[0];
32-
33-export function getBuddyIcon(id: string | null | undefined): BuddyIconPreset {
34- return BUDDY_ICONS.find((i) => i.id === id) ?? DEFAULT_BUDDY_ICON;
35-}
@@ -1,35 +0,0 @@
1-/**
2- * Preset buddy icons (48x48 in the original AIM). Rendered as emoji on a
3- * coloured tile so no image assets are needed.
4- */
5-export interface BuddyIconPreset {
6- id: string;
7- emoji: string;
8- label: string;
9- bg: string;
10-}
11-
12-export const BUDDY_ICONS: BuddyIconPreset[] = [
13- { id: 'runner', emoji: '🏃', label: 'Running man', bg: '#f6c700' },
14- { id: 'smiley', emoji: '🙂', label: 'Smiley', bg: '#ffe680' },
15- { id: 'cool', emoji: '😎', label: 'Cool', bg: '#8fd3ff' },
16- { id: 'cat', emoji: '🐱', label: 'Chat', bg: '#ffd1dc' },
17- { id: 'dog', emoji: '🐶', label: 'Chien', bg: '#d9c3a5' },
18- { id: 'alien', emoji: '👽', label: 'Alien', bg: '#b6ffb0' },
19- { id: 'skull', emoji: '💀', label: 'Crâne', bg: '#dcdcdc' },
20- { id: 'fire', emoji: '🔥', label: 'Feu', bg: '#ffb27a' },
21- { id: 'star', emoji: '⭐', label: 'Étoile', bg: '#3a4fbf' },
22- { id: 'heart', emoji: '💜', label: 'Cœur', bg: '#f7c6ff' },
23- { id: 'music', emoji: '🎵', label: 'Musique', bg: '#c4f0ff' },
24- { id: 'game', emoji: '🎮', label: 'Jeux', bg: '#c9ffd6' },
25- { id: 'pizza', emoji: '🍕', label: 'Pizza', bg: '#ffe0b3' },
26- { id: 'rocket', emoji: '🚀', label: 'Fusée', bg: '#1f2a5a' },
27- { id: 'ghost', emoji: '👻', label: 'Fantôme', bg: '#eaeaff' },
28- { id: 'robot', emoji: '🤖', label: 'Robot', bg: '#cfd8dc' }
29-];
30-
31-export const DEFAULT_BUDDY_ICON = BUDDY_ICONS[0];
32-
33-export function getBuddyIcon(id: string | null | undefined): BuddyIconPreset {
34- return BUDDY_ICONS.find((i) => i.id === id) ?? DEFAULT_BUDDY_ICON;
35-}
M src/lib/aim/client.ts +1 -1
@@ -72,7 +72,7 @@ export function useAimClient() {
7272 const { client, hash } = requireAuth();
7373 return client.mutation(api.aim.setAwayMessage, { tokenHash: hash, awayMessage });
7474 },
75- async updateProfile(patch: { profile?: string; buddyIcon?: string }) {
75+ async updateProfile(patch: { profile?: string }) {
7676 const { client, hash } = requireAuth();
7777 return client.mutation(api.aim.updateProfile, { tokenHash: hash, ...patch });
7878 },
@@ -72,7 +72,7 @@ export function useAimClient() {
72 const { client, hash } = requireAuth();72 const { client, hash } = requireAuth();
73 return client.mutation(api.aim.setAwayMessage, { tokenHash: hash, awayMessage });73 return client.mutation(api.aim.setAwayMessage, { tokenHash: hash, awayMessage });
74 },74 },
75- async updateProfile(patch: { profile?: string; buddyIcon?: string }) {75+ async updateProfile(patch: { profile?: string }) {
76 const { client, hash } = requireAuth();76 const { client, hash } = requireAuth();
77 return client.mutation(api.aim.updateProfile, { tokenHash: hash, ...patch });77 return client.mutation(api.aim.updateProfile, { tokenHash: hash, ...patch });
78 },78 },
M src/lib/components/chat-room.svelte +1 -2
@@ -79,8 +79,7 @@
7979 status: u.status as SafeUser['status'],
8080 avatarUrl: u.avatarUrl ?? null,
8181 lastSeen: u.lastSeen ?? null,
82- awayMessage: u.awayMessage,
83- buddyIcon: u.buddyIcon
82+ awayMessage: u.awayMessage
8483 }))
8584 .sort((a, b) => {
8685 const statusOrder: Record<string, number> = {
@@ -79,8 +79,7 @@
79 status: u.status as SafeUser['status'],79 status: u.status as SafeUser['status'],
80 avatarUrl: u.avatarUrl ?? null,80 avatarUrl: u.avatarUrl ?? null,
81 lastSeen: u.lastSeen ?? null,81 lastSeen: u.lastSeen ?? null,
82- awayMessage: u.awayMessage,82+ awayMessage: u.awayMessage
83- buddyIcon: u.buddyIcon
84 }))83 }))
85 .sort((a, b) => {84 .sort((a, b) => {
86 const statusOrder: Record<string, number> = {85 const statusOrder: Record<string, number> = {
M src/lib/components/desktop/buddy-list.svelte +1 -10
@@ -6,7 +6,6 @@
66 import type { Id } from '../../../convex/_generated/dataModel';
77 import type { Buddy, AimStatus } from '$lib/types/aim';
88 import { DEFAULT_BUDDY_GROUPS, STATUS_LABELS_FR } from '$lib/types/aim';
9- import { getBuddyIcon } from '$lib/aim/buddy-icons';
109 import { playSound } from '$lib/aim/sounds';
1110 import { authQuery, useAimClient, convexAvailable } from '$lib/aim/client';
1211 import { chatState } from '$lib/states/chat.svelte';
@@ -327,9 +326,7 @@
327326 <div class="buddy-content">
328327 <header class="identity">
329328 {#if me}
330- <div class="icon" style:background={getBuddyIcon(meQuery?.data?.buddyIcon).bg}>
331- {getBuddyIcon(meQuery?.data?.buddyIcon).emoji}
332- </div>
329+ <img class="icon" src="/aim/running-man-32.png" alt="" width="32" height="32" />
333330 <div class="identity-text">
334331 <strong
335332 ><img src="/aim/running-man-16.png" alt="" width="16" height="16" />
@@ -523,13 +520,7 @@
523520 margin: 3px 0 0;
524521 }
525522 .icon {
526- display: grid;
527- place-items: center;
528- width: 32px;
529- height: 32px;
530523 flex: none;
531- border: 1px solid #9c8700;
532- font-size: 22px;
533524 }
534525 .identity-text {
535526 min-width: 0;
@@ -6,7 +6,6 @@
6 import type { Id } from '../../../convex/_generated/dataModel';6 import type { Id } from '../../../convex/_generated/dataModel';
7 import type { Buddy, AimStatus } from '$lib/types/aim';7 import type { Buddy, AimStatus } from '$lib/types/aim';
8 import { DEFAULT_BUDDY_GROUPS, STATUS_LABELS_FR } from '$lib/types/aim';8 import { DEFAULT_BUDDY_GROUPS, STATUS_LABELS_FR } from '$lib/types/aim';
9- import { getBuddyIcon } from '$lib/aim/buddy-icons';
10 import { playSound } from '$lib/aim/sounds';9 import { playSound } from '$lib/aim/sounds';
11 import { authQuery, useAimClient, convexAvailable } from '$lib/aim/client';10 import { authQuery, useAimClient, convexAvailable } from '$lib/aim/client';
12 import { chatState } from '$lib/states/chat.svelte';11 import { chatState } from '$lib/states/chat.svelte';
@@ -327,9 +326,7 @@
327 <div class="buddy-content">326 <div class="buddy-content">
328 <header class="identity">327 <header class="identity">
329 {#if me}328 {#if me}
330- <div class="icon" style:background={getBuddyIcon(meQuery?.data?.buddyIcon).bg}>329+ <img class="icon" src="/aim/running-man-32.png" alt="" width="32" height="32" />
331- {getBuddyIcon(meQuery?.data?.buddyIcon).emoji}
332- </div>
333 <div class="identity-text">330 <div class="identity-text">
334 <strong331 <strong
335 ><img src="/aim/running-man-16.png" alt="" width="16" height="16" />332 ><img src="/aim/running-man-16.png" alt="" width="16" height="16" />
@@ -523,13 +520,7 @@
523 margin: 3px 0 0;520 margin: 3px 0 0;
524 }521 }
525 .icon {522 .icon {
526- display: grid;
527- place-items: center;
528- width: 32px;
529- height: 32px;
530 flex: none;523 flex: none;
531- border: 1px solid #9c8700;
532- font-size: 22px;
533 }524 }
534 .identity-text {525 .identity-text {
535 min-width: 0;526 min-width: 0;
M src/lib/components/desktop/im-window.svelte +1 -16
@@ -6,7 +6,6 @@
66 import type { EnrichedMessage, SafeUser } from '$lib/types/chat';
77 import { authQuery, useAimClient } from '$lib/aim/client';
88 import { playSound } from '$lib/aim/sounds';
9- import { getBuddyIcon } from '$lib/aim/buddy-icons';
109 import { STATUS_LABELS_FR, type AimStatus } from '$lib/types/aim';
1110 import { chatState } from '$lib/states/chat.svelte';
1211 import { desktop } from '$lib/states/desktop.svelte';
@@ -31,7 +30,6 @@
3130 const profileQuery = useQuery(convexApi.aim.getUserProfile, () => ({ userId: otherUserId }));
3231 const currentUser = $derived(chatState.getCurrentUser());
3332 const profile = $derived(profileQuery.data);
34- const buddyIcon = $derived(getBuddyIcon(profile?.buddyIcon));
3533 const status = $derived((profile?.status ?? 'offline') as AimStatus);
3634 const messages = $derived.by<EnrichedMessage[]>(() =>
3735 (messagesQuery?.data ?? []).map((msg) => ({
@@ -262,14 +260,7 @@
262260 {/snippet}
263261 <div class="im-body">
264262 <div class="header">
265- <div
266- class="buddy-icon"
267- style:background={buddyIcon.bg}
268- role="img"
269- aria-label={buddyIcon.label}
270- >
271- {buddyIcon.emoji}
272- </div>
263+ <img class="buddy-icon" src="/aim/running-man-48.png" alt="" width="48" height="48" />
273264 <div class="identity">
274265 <strong>{profile?.nickname ?? otherNickname}</strong><span
275266 ><img src={`/aim/status-${status}.svg`} alt="" width="14" height="14" />
@@ -380,12 +371,6 @@
380371 padding: 2px 4px;
381372 }
382373 .buddy-icon {
383- width: 48px;
384- height: 48px;
385- display: grid;
386- place-items: center;
387- font-size: 29px;
388- border: 1px solid #aca899;
389374 flex: none;
390375 }
391376 .identity {
@@ -6,7 +6,6 @@
6 import type { EnrichedMessage, SafeUser } from '$lib/types/chat';6 import type { EnrichedMessage, SafeUser } from '$lib/types/chat';
7 import { authQuery, useAimClient } from '$lib/aim/client';7 import { authQuery, useAimClient } from '$lib/aim/client';
8 import { playSound } from '$lib/aim/sounds';8 import { playSound } from '$lib/aim/sounds';
9- import { getBuddyIcon } from '$lib/aim/buddy-icons';
10 import { STATUS_LABELS_FR, type AimStatus } from '$lib/types/aim';9 import { STATUS_LABELS_FR, type AimStatus } from '$lib/types/aim';
11 import { chatState } from '$lib/states/chat.svelte';10 import { chatState } from '$lib/states/chat.svelte';
12 import { desktop } from '$lib/states/desktop.svelte';11 import { desktop } from '$lib/states/desktop.svelte';
@@ -31,7 +30,6 @@
31 const profileQuery = useQuery(convexApi.aim.getUserProfile, () => ({ userId: otherUserId }));30 const profileQuery = useQuery(convexApi.aim.getUserProfile, () => ({ userId: otherUserId }));
32 const currentUser = $derived(chatState.getCurrentUser());31 const currentUser = $derived(chatState.getCurrentUser());
33 const profile = $derived(profileQuery.data);32 const profile = $derived(profileQuery.data);
34- const buddyIcon = $derived(getBuddyIcon(profile?.buddyIcon));
35 const status = $derived((profile?.status ?? 'offline') as AimStatus);33 const status = $derived((profile?.status ?? 'offline') as AimStatus);
36 const messages = $derived.by<EnrichedMessage[]>(() =>34 const messages = $derived.by<EnrichedMessage[]>(() =>
37 (messagesQuery?.data ?? []).map((msg) => ({35 (messagesQuery?.data ?? []).map((msg) => ({
@@ -262,14 +260,7 @@
262 {/snippet}260 {/snippet}
263 <div class="im-body">261 <div class="im-body">
264 <div class="header">262 <div class="header">
265- <div263+ <img class="buddy-icon" src="/aim/running-man-48.png" alt="" width="48" height="48" />
266- class="buddy-icon"
267- style:background={buddyIcon.bg}
268- role="img"
269- aria-label={buddyIcon.label}
270- >
271- {buddyIcon.emoji}
272- </div>
273 <div class="identity">264 <div class="identity">
274 <strong>{profile?.nickname ?? otherNickname}</strong><span265 <strong>{profile?.nickname ?? otherNickname}</strong><span
275 ><img src={`/aim/status-${status}.svg`} alt="" width="14" height="14" />266 ><img src={`/aim/status-${status}.svg`} alt="" width="14" height="14" />
@@ -380,12 +371,6 @@
380 padding: 2px 4px;371 padding: 2px 4px;
381 }372 }
382 .buddy-icon {373 .buddy-icon {
383- width: 48px;
384- height: 48px;
385- display: grid;
386- place-items: center;
387- font-size: 29px;
388- border: 1px solid #aca899;
389 flex: none;374 flex: none;
390 }375 }
391 .identity {376 .identity {
M src/lib/components/desktop/profile-window.svelte +2 -48
@@ -6,7 +6,6 @@
66 import { desktop } from '$lib/states/desktop.svelte';
77 import { chatState } from '$lib/states/chat.svelte';
88 import { useAimClient } from '$lib/aim/client';
9- import { BUDDY_ICONS, getBuddyIcon } from '$lib/aim/buddy-icons';
109 import { STATUS_LABELS_FR } from '$lib/types/aim';
1110 import { formatFrenchDateTime } from '$lib/utils/date-format';
1211
@@ -17,7 +16,6 @@
1716 const profile = $derived(profileQuery.data);
1817 const own = $derived(userId === chatState.getCurrentUser()?.id);
1918 let text = $state('');
20- let icon = $state('runner');
2119 let saved = $state(false);
2220 let saving = $state(false);
2321 let error = $state('');
@@ -25,7 +23,6 @@
2523 $effect(() => {
2624 if (profile && initializedFor !== userId) {
2725 text = profile.profile ?? '';
28- icon = profile.buddyIcon ?? 'runner';
2926 initializedFor = userId;
3027 }
3128 });
@@ -35,7 +32,7 @@
3532 saved = false;
3633 error = '';
3734 try {
38- await client.updateProfile({ profile: text, buddyIcon: icon });
35+ await client.updateProfile({ profile: text });
3936 saved = true;
4037 } catch (err) {
4138 error = err instanceof Error ? err.message : 'Impossible d’enregistrer.';
@@ -49,13 +46,7 @@
4946 <div class="profile-content">
5047 {#if profile}
5148 <div class="identity">
52- <div
53- class="buddy-icon"
54- style:background={getBuddyIcon(profile.buddyIcon).bg}
55- aria-label={getBuddyIcon(profile.buddyIcon).label}
56- >
57- {getBuddyIcon(profile.buddyIcon).emoji}
58- </div>
49+ <img class="buddy-icon" src="/aim/running-man-48.png" alt="" width="48" height="48" />
5950 <div>
6051 <strong>{profile.nickname}</strong>
6152 <div class="status">
@@ -82,21 +73,6 @@
8273 oninput={() => (saved = false)}
8374 ></textarea>
8475 <small>{text.length}/1000</small>
85- <div class="icon-grid" aria-label="Choisir une icône">
86- {#each BUDDY_ICONS as preset (preset.id)}
87- <button
88- class:selected={icon === preset.id}
89- aria-label={preset.label}
90- aria-pressed={icon === preset.id}
91- title={preset.label}
92- style:background={preset.bg}
93- onclick={() => {
94- icon = preset.id;
95- saved = false;
96- }}>{preset.emoji}</button
97- >
98- {/each}
99- </div>
10076 {:else}
10177 <div id="profile-text" class="sunken-panel profile-text">
10278 {profile.profile ?? 'Aucun profil pour le moment.'}
@@ -136,13 +112,7 @@
136112 gap: 10px;
137113 }
138114 .buddy-icon {
139- width: 48px;
140- height: 48px;
141115 flex: 0 0 48px;
142- display: grid;
143- place-items: center;
144- font-size: 28px;
145- border: 1px solid #777;
146116 }
147117 .status {
148118 display: flex;
@@ -172,22 +142,6 @@
172142 textarea {
173143 resize: vertical;
174144 }
175- .icon-grid {
176- display: grid;
177- grid-template-columns: repeat(8, 1fr);
178- gap: 3px;
179- }
180- .icon-grid button {
181- padding: 0;
182- min-width: 0;
183- height: 30px;
184- font-size: 18px;
185- cursor: pointer;
186- border: 2px solid transparent;
187- }
188- .icon-grid button.selected {
189- border-color: #245edb;
190- }
191145 .actions {
192146 margin-top: auto;
193147 display: flex;
@@ -6,7 +6,6 @@
6 import { desktop } from '$lib/states/desktop.svelte';6 import { desktop } from '$lib/states/desktop.svelte';
7 import { chatState } from '$lib/states/chat.svelte';7 import { chatState } from '$lib/states/chat.svelte';
8 import { useAimClient } from '$lib/aim/client';8 import { useAimClient } from '$lib/aim/client';
9- import { BUDDY_ICONS, getBuddyIcon } from '$lib/aim/buddy-icons';
10 import { STATUS_LABELS_FR } from '$lib/types/aim';9 import { STATUS_LABELS_FR } from '$lib/types/aim';
11 import { formatFrenchDateTime } from '$lib/utils/date-format';10 import { formatFrenchDateTime } from '$lib/utils/date-format';
12 11
@@ -17,7 +16,6 @@
17 const profile = $derived(profileQuery.data);16 const profile = $derived(profileQuery.data);
18 const own = $derived(userId === chatState.getCurrentUser()?.id);17 const own = $derived(userId === chatState.getCurrentUser()?.id);
19 let text = $state('');18 let text = $state('');
20- let icon = $state('runner');
21 let saved = $state(false);19 let saved = $state(false);
22 let saving = $state(false);20 let saving = $state(false);
23 let error = $state('');21 let error = $state('');
@@ -25,7 +23,6 @@
25 $effect(() => {23 $effect(() => {
26 if (profile && initializedFor !== userId) {24 if (profile && initializedFor !== userId) {
27 text = profile.profile ?? '';25 text = profile.profile ?? '';
28- icon = profile.buddyIcon ?? 'runner';
29 initializedFor = userId;26 initializedFor = userId;
30 }27 }
31 });28 });
@@ -35,7 +32,7 @@
35 saved = false;32 saved = false;
36 error = '';33 error = '';
37 try {34 try {
38- await client.updateProfile({ profile: text, buddyIcon: icon });35+ await client.updateProfile({ profile: text });
39 saved = true;36 saved = true;
40 } catch (err) {37 } catch (err) {
41 error = err instanceof Error ? err.message : 'Impossible d’enregistrer.';38 error = err instanceof Error ? err.message : 'Impossible d’enregistrer.';
@@ -49,13 +46,7 @@
49 <div class="profile-content">46 <div class="profile-content">
50 {#if profile}47 {#if profile}
51 <div class="identity">48 <div class="identity">
52- <div49+ <img class="buddy-icon" src="/aim/running-man-48.png" alt="" width="48" height="48" />
53- class="buddy-icon"
54- style:background={getBuddyIcon(profile.buddyIcon).bg}
55- aria-label={getBuddyIcon(profile.buddyIcon).label}
56- >
57- {getBuddyIcon(profile.buddyIcon).emoji}
58- </div>
59 <div>50 <div>
60 <strong>{profile.nickname}</strong>51 <strong>{profile.nickname}</strong>
61 <div class="status">52 <div class="status">
@@ -82,21 +73,6 @@
82 oninput={() => (saved = false)}73 oninput={() => (saved = false)}
83 ></textarea>74 ></textarea>
84 <small>{text.length}/1000</small>75 <small>{text.length}/1000</small>
85- <div class="icon-grid" aria-label="Choisir une icône">
86- {#each BUDDY_ICONS as preset (preset.id)}
87- <button
88- class:selected={icon === preset.id}
89- aria-label={preset.label}
90- aria-pressed={icon === preset.id}
91- title={preset.label}
92- style:background={preset.bg}
93- onclick={() => {
94- icon = preset.id;
95- saved = false;
96- }}>{preset.emoji}</button
97- >
98- {/each}
99- </div>
100 {:else}76 {:else}
101 <div id="profile-text" class="sunken-panel profile-text">77 <div id="profile-text" class="sunken-panel profile-text">
102 {profile.profile ?? 'Aucun profil pour le moment.'}78 {profile.profile ?? 'Aucun profil pour le moment.'}
@@ -136,13 +112,7 @@
136 gap: 10px;112 gap: 10px;
137 }113 }
138 .buddy-icon {114 .buddy-icon {
139- width: 48px;
140- height: 48px;
141 flex: 0 0 48px;115 flex: 0 0 48px;
142- display: grid;
143- place-items: center;
144- font-size: 28px;
145- border: 1px solid #777;
146 }116 }
147 .status {117 .status {
148 display: flex;118 display: flex;
@@ -172,22 +142,6 @@
172 textarea {142 textarea {
173 resize: vertical;143 resize: vertical;
174 }144 }
175- .icon-grid {
176- display: grid;
177- grid-template-columns: repeat(8, 1fr);
178- gap: 3px;
179- }
180- .icon-grid button {
181- padding: 0;
182- min-width: 0;
183- height: 30px;
184- font-size: 18px;
185- cursor: pointer;
186- border: 2px solid transparent;
187- }
188- .icon-grid button.selected {
189- border-color: #245edb;
190- }
191 .actions {145 .actions {
192 margin-top: auto;146 margin-top: auto;
193 display: flex;147 display: flex;
M src/lib/types/aim.ts +0 -1
@@ -10,7 +10,6 @@ export interface AimUser {
1010 avatarUrl?: string | null;
1111 lastSeen?: number | null;
1212 awayMessage?: string;
13- buddyIcon?: string;
1413 createdAt?: number;
1514 }
1615
@@ -10,7 +10,6 @@ export interface AimUser {
10 avatarUrl?: string | null;10 avatarUrl?: string | null;
11 lastSeen?: number | null;11 lastSeen?: number | null;
12 awayMessage?: string;12 awayMessage?: string;
13- buddyIcon?: string;
14 createdAt?: number;13 createdAt?: number;
15 }14 }
16 15
M src/lib/types/chat.ts +0 -1
@@ -9,7 +9,6 @@ export interface User {
99 lastSeen?: number | null; // Optional last active timestamp
1010 lastReadMentionTimestamp?: number | null; // Timestamp of last read mention
1111 awayMessage?: string; // AIM away message (only exposed while away)
12- buddyIcon?: string; // Preset buddy icon id
1312 }
1413
1514 export type UserStatus = 'offline' | 'online' | 'away' | string;
@@ -9,7 +9,6 @@ export interface User {
9 lastSeen?: number | null; // Optional last active timestamp9 lastSeen?: number | null; // Optional last active timestamp
10 lastReadMentionTimestamp?: number | null; // Timestamp of last read mention10 lastReadMentionTimestamp?: number | null; // Timestamp of last read mention
11 awayMessage?: string; // AIM away message (only exposed while away)11 awayMessage?: string; // AIM away message (only exposed while away)
12- buddyIcon?: string; // Preset buddy icon id
13 }12 }
14 13
15 export type UserStatus = 'offline' | 'online' | 'away' | string;14 export type UserStatus = 'offline' | 'online' | 'away' | string;

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close