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);
+12
View File
@@ -6,10 +6,12 @@ interface SettingsState {
streamingLimits: InstanceStreamingLimits | null;
instanceSettings: InstanceAdminSettings | null;
isAdmin: boolean;
gifEnabled: boolean;
fetchStreamingLimits: () => Promise<void>;
updateStreamingLimits: (limits: Partial<InstanceStreamingLimits>) => Promise<void>;
fetchInstanceSettings: () => Promise<void>;
updateInstanceSettings: (data: Partial<InstanceAdminSettings>) => Promise<void>;
fetchGifEnabled: () => Promise<void>;
setIsAdmin: (isAdmin: boolean) => void;
}
@@ -32,6 +34,7 @@ export const useSettingsStore = create<SettingsState>((set) => ({
streamingLimits: null,
instanceSettings: null,
isAdmin: false,
gifEnabled: false,
fetchStreamingLimits: async () => {
try {
@@ -70,5 +73,14 @@ export const useSettingsStore = create<SettingsState>((set) => ({
}
},
fetchGifEnabled: async () => {
try {
const { enabled } = await api.gif.enabled();
set({ gifEnabled: enabled });
} catch {
set({ gifEnabled: false });
}
},
setIsAdmin: (isAdmin: boolean) => set({ isAdmin }),
}));