feat: GIF search (Klipy), stickers, emoji picker, and bug fixes
- Add GIF search powered by Klipy API with correct response mapping (file.sm/hd tiers, not flat files structure) - Add sticker system: packs, upload with auto-downscale, send in messages - Add tabbed InputPopover with emoji, GIF, and sticker pickers - Fix GIF API key migration race condition (column-add loop vs rename) - Fix masked API key corruption on settings save (server + client guards) - Fix sticker packs 403 (reversed isMember parameter order) - Fix emoji picker not filling popover width (perLine 8→9, CSS 100%) - Add error logging for Klipy API failures
This commit is contained in:
@@ -46,6 +46,9 @@ import type {
|
||||
SpaceLayoutItem,
|
||||
SpaceFolder,
|
||||
InvitePreview,
|
||||
GifResult,
|
||||
StickerPack,
|
||||
Sticker,
|
||||
} from '@backspace/shared';
|
||||
|
||||
export class RateLimitError extends Error {
|
||||
@@ -187,6 +190,22 @@ export class BackspaceApiClient {
|
||||
myJoinRequests: (status?: string) => Promise<{ requests: JoinRequest[] }>;
|
||||
};
|
||||
|
||||
readonly gif: {
|
||||
trending: (limit?: number, pos?: string) => Promise<{ results: GifResult[]; next: string }>;
|
||||
search: (q: string, limit?: number, pos?: string) => Promise<{ results: GifResult[]; next: string }>;
|
||||
enabled: () => Promise<{ enabled: boolean }>;
|
||||
};
|
||||
|
||||
readonly stickers: {
|
||||
getPacks: (spaceId: string) => Promise<{ packs: StickerPack[] }>;
|
||||
createPack: (spaceId: string, data: { name: string; description?: string }) => Promise<StickerPack>;
|
||||
updatePack: (spaceId: string, packId: string, data: { name?: string; description?: string }) => Promise<StickerPack>;
|
||||
deletePack: (spaceId: string, packId: string) => Promise<{ success: boolean }>;
|
||||
uploadSticker: (spaceId: string, packId: string, file: File, name: string, tags?: string) => Promise<Sticker>;
|
||||
deleteSticker: (stickerId: string) => Promise<{ success: boolean }>;
|
||||
myStickers: () => Promise<{ packs: StickerPack[] }>;
|
||||
};
|
||||
|
||||
readonly admin: {
|
||||
storageStats: () => Promise<StorageStats>;
|
||||
storageOrphans: () => Promise<{ orphans: OrphanedFile[] }>;
|
||||
@@ -504,6 +523,59 @@ export class BackspaceApiClient {
|
||||
},
|
||||
};
|
||||
|
||||
this.gif = {
|
||||
trending: (limit = 30, pos?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('limit', String(limit));
|
||||
if (pos) params.set('pos', pos);
|
||||
return request<{ results: GifResult[]; next: string }>('GET', `/gif/trending?${params}`);
|
||||
},
|
||||
search: (q: string, limit = 30, pos?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('q', q);
|
||||
params.set('limit', String(limit));
|
||||
if (pos) params.set('pos', pos);
|
||||
return request<{ results: GifResult[]; next: string }>('GET', `/gif/search?${params}`);
|
||||
},
|
||||
enabled: () => request<{ enabled: boolean }>('GET', '/gif/enabled'),
|
||||
};
|
||||
|
||||
this.stickers = {
|
||||
getPacks: (spaceId: string) =>
|
||||
request<{ packs: StickerPack[] }>('GET', `/spaces/${spaceId}/sticker-packs`),
|
||||
createPack: (spaceId: string, data: { name: string; description?: string }) =>
|
||||
request<StickerPack>('POST', `/spaces/${spaceId}/sticker-packs`, data),
|
||||
updatePack: (spaceId: string, packId: string, data: { name?: string; description?: string }) =>
|
||||
request<StickerPack>('PATCH', `/spaces/${spaceId}/sticker-packs/${packId}`, data),
|
||||
deletePack: (spaceId: string, packId: string) =>
|
||||
request<{ success: boolean }>('DELETE', `/spaces/${spaceId}/sticker-packs/${packId}`),
|
||||
uploadSticker: async (spaceId: string, packId: string, file: File, name: string, tags = '') => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('name', name);
|
||||
formData.append('tags', tags);
|
||||
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const response = await fetch(`${baseUrl}/spaces/${spaceId}/sticker-packs/${packId}/stickers`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Upload failed' }));
|
||||
throw new Error((error as { error: string }).error || `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<Sticker>;
|
||||
},
|
||||
deleteSticker: (stickerId: string) =>
|
||||
request<{ success: boolean }>('DELETE', `/stickers/${stickerId}`),
|
||||
myStickers: () =>
|
||||
request<{ packs: StickerPack[] }>('GET', '/users/@me/stickers'),
|
||||
};
|
||||
|
||||
this.admin = {
|
||||
storageStats: () => request<StorageStats>('GET', '/admin/storage/stats'),
|
||||
storageOrphans: () => request<{ orphans: OrphanedFile[] }>('GET', '/admin/storage/orphans'),
|
||||
|
||||
Reference in New Issue
Block a user