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:
Jannis Braun
2026-03-15 02:04:37 +01:00
parent 7113f47b17
commit 3de6e4a668
26 changed files with 2160 additions and 50 deletions
+27
View File
@@ -70,6 +70,7 @@ export const messages = sqliteTable('messages', {
userId: text('user_id').notNull().references(() => users.id),
replyToId: text('reply_to_id'),
content: text('content'),
stickerId: text('sticker_id'),
editedAt: integer('edited_at'),
createdAt: integer('created_at').notNull(),
}, (table) => ({
@@ -112,6 +113,7 @@ export const dmMessages = sqliteTable('dm_messages', {
userId: text('user_id').notNull().references(() => users.id),
replyToId: text('reply_to_id'),
content: text('content'),
stickerId: text('sticker_id'),
editedAt: integer('edited_at'),
createdAt: integer('created_at').notNull(),
}, (table) => ({
@@ -213,6 +215,30 @@ export const userSpaceLayout = sqliteTable('user_space_layout', {
updatedAt: integer('updated_at').notNull(),
});
export const stickerPacks = sqliteTable('sticker_packs', {
id: text('id').primaryKey(),
spaceId: text('space_id').notNull().references(() => spaces.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
description: text('description'),
createdBy: text('created_by').notNull().references(() => users.id),
createdAt: integer('created_at').notNull(),
});
export const stickers = sqliteTable('stickers', {
id: text('id').primaryKey(),
packId: text('pack_id').notNull().references(() => stickerPacks.id, { onDelete: 'cascade' }),
spaceId: text('space_id').notNull().references(() => spaces.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
tags: text('tags').default(''),
filename: text('filename').notNull(),
mimetype: text('mimetype').notNull(),
size: integer('size').notNull(),
width: integer('width'),
height: integer('height'),
uploadedBy: text('uploaded_by').notNull().references(() => users.id),
createdAt: integer('created_at').notNull(),
});
export const instanceSettings = sqliteTable('instance_settings', {
id: integer('id').primaryKey().default(1),
instanceName: text('instance_name').default('Backspace'),
@@ -226,6 +252,7 @@ export const instanceSettings = sqliteTable('instance_settings', {
maxResolution: integer('max_resolution').notNull().default(1080),
maxFramerate: integer('max_framerate').notNull().default(60),
registrationOpen: integer('registration_open'), // null = use env var default, 0/1 = explicit
gifApiKey: text('gif_api_key'),
updatedAt: integer('updated_at').notNull(),
});