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
+40
View File
@@ -38,6 +38,7 @@ interface ChatState {
clearAllMessages: () => void;
loadMoreMessages: (channelId: string) => Promise<boolean>;
sendMessage: (channelId: string, content: string, attachmentIds?: string[]) => Promise<void>;
sendStickerMessage: (channelId: string, stickerId: string) => Promise<void>;
editMessage: (messageId: string, content: string, channelId: string) => Promise<void>;
deleteMessage: (messageId: string, channelId: string) => Promise<void>;
addMessage: (channelId: string, message: MessageWithUser) => void;
@@ -289,6 +290,45 @@ export const useChatStore = create<ChatState>((set, get) => ({
}
},
sendStickerMessage: async (channelId: string, stickerId: string) => {
const isDm = isDmChannel(channelId);
const currentUser = useAuthStore.getState().user;
const origin = getChannelOrigin(channelId);
const client = getApiForOrigin(origin);
// Optimistic message
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
if (currentUser) {
const optimisticMessage: MessageWithUser = {
id: tempId,
channelId: isDm ? '' : channelId,
userId: currentUser.id,
content: null,
replyToId: null,
editedAt: null,
createdAt: Date.now(),
user: currentUser,
attachments: [],
reactions: [],
stickerId,
};
if (isDm) {
(optimisticMessage as any).dmChannelId = channelId;
}
get().addMessage(channelId, optimisticMessage);
}
try {
if (isDm) {
await client.dm.sendMessage(channelId, { stickerId });
} else {
await client.channels.sendMessage(channelId, { content: '', stickerId });
}
} catch {
get().removeMessage(tempId, channelId);
}
},
editMessage: async (messageId: string, content: string, channelId: string) => {
const isDm = isDmChannel(channelId);
const origin = getChannelOrigin(channelId);