{bookmarkedNotJoined.map((room) => {
const flatIndex = jidToIndex.get(room.jid) ?? -1
- const draft = drafts.get(room.jid)
return (
handleRoomClick(room.jid, false)}
onDoubleClick={() => handleRoomDoubleClick(room.jid, false, room.nickname)}
onJoin={() => joinRoom(room.jid, room.nickname)}
@@ -279,7 +281,6 @@ interface RoomItemProps {
isActive: boolean
isSelected?: boolean
isKeyboardNav?: boolean
- draft?: string
onClick: () => void
onDoubleClick: () => void
onJoin: () => void
@@ -299,7 +300,6 @@ const RoomItem = memo(function RoomItem({
isActive,
isSelected,
isKeyboardNav,
- draft,
onClick,
onDoubleClick,
onJoin,
@@ -317,6 +317,9 @@ const RoomItem = memo(function RoomItem({
const menu = useContextMenu()
const currentLang = i18n.language.split('-')[0]
const timeFormat = useSettingsStore((s) => s.timeFormat)
+ // Per-item subscription: only this row re-renders when ITS draft changes,
+ // not the whole list when any room's draft changes.
+ const draft = useRoomStore((s) => s.drafts.get(room.jid))
// Get last message for preview (uses pre-computed lastMessage from metadata for better performance)
const lastMessage = room.lastMessage ?? null
diff --git a/packages/fluux-sdk/src/hooks/useChatActions.ts b/packages/fluux-sdk/src/hooks/useChatActions.ts
new file mode 100644
index 00000000..5719c0b7
--- /dev/null
+++ b/packages/fluux-sdk/src/hooks/useChatActions.ts
@@ -0,0 +1,246 @@
+import { useCallback, useMemo } from 'react'
+import { chatStore, connectionStore } from '../stores'
+import { useXMPPContext } from '../provider'
+import type { Conversation, ChatStateNotification, FileAttachment } from '../core'
+import { createFetchOlderHistory } from './shared'
+
+/**
+ * Action-only counterpart to `useChat()`.
+ *
+ * Returns the same actions as `useChat()` but performs ZERO store subscriptions.
+ * Use this in components that only need to invoke chat actions and do not need
+ * to react to chat state changes.
+ *
+ * Calling `useChat()` subscribes the component to many chat store values
+ * (`conversations`, `activeMessages`, `typingStates`, `drafts`, MAM state, etc.).
+ * `useChatActions()` reads actions directly via `chatStore.getState()`, avoiding
+ * any subscription.
+ *
+ * @returns A stable object of chat action callbacks
+ *
+ * @category Hooks
+ */
+export function useChatActions() {
+ const { client } = useXMPPContext()
+
+ const sendMessage = useCallback(
+ async (
+ to: string,
+ body: string,
+ type: 'chat' | 'groupchat' = 'chat',
+ replyTo?: { id: string; to?: string; fallback?: { author: string; body: string } },
+ attachment?: FileAttachment
+ ): Promise => {
+ return await client.chat.sendMessage(to, body, type, replyTo, undefined, attachment)
+ },
+ [client]
+ )
+
+ const setActiveConversation = useCallback(async (id: string | null) => {
+ if (id) {
+ await chatStore.getState().loadMessagesFromCache(id, { limit: 100 })
+ }
+ chatStore.getState().setActiveConversation(id)
+ }, [])
+
+ const addConversation = useCallback((conv: Conversation) => {
+ chatStore.getState().addConversation(conv)
+ }, [])
+
+ const deleteConversation = useCallback((id: string) => {
+ chatStore.getState().deleteConversation(id)
+ }, [])
+
+ const markAsRead = useCallback((conversationId: string) => {
+ chatStore.getState().markAsRead(conversationId)
+ }, [])
+
+ const archiveConversation = useCallback((id: string) => {
+ chatStore.getState().archiveConversation(id)
+ }, [])
+
+ const unarchiveConversation = useCallback((id: string) => {
+ chatStore.getState().unarchiveConversation(id)
+ }, [])
+
+ const isArchived = useCallback((id: string) => {
+ return chatStore.getState().isArchived(id)
+ }, [])
+
+ const sendChatState = useCallback(
+ async (to: string, state: ChatStateNotification, type: 'chat' | 'groupchat' = 'chat') => {
+ await client.chat.sendChatState(to, state, type)
+ },
+ [client]
+ )
+
+ const sendReaction = useCallback(
+ async (to: string, messageId: string, emojis: string[], type: 'chat' | 'groupchat' = 'chat') => {
+ await client.chat.sendReaction(to, messageId, emojis, type)
+ },
+ [client]
+ )
+
+ const sendCorrection = useCallback(
+ async (conversationId: string, messageId: string, newBody: string, attachment?: FileAttachment) => {
+ await client.chat.sendCorrection(conversationId, messageId, newBody, 'chat', attachment)
+ },
+ [client]
+ )
+
+ const retractMessage = useCallback(
+ async (conversationId: string, messageId: string) => {
+ await client.chat.sendRetraction(conversationId, messageId, 'chat')
+ },
+ [client]
+ )
+
+ const sendEasterEgg = useCallback(
+ async (to: string, type: 'chat' | 'groupchat', animation: string) => {
+ await client.chat.sendEasterEgg(to, type, animation)
+ },
+ [client]
+ )
+
+ const clearAnimation = useCallback(() => {
+ chatStore.getState().clearAnimation()
+ }, [])
+
+ const setDraft = useCallback((conversationId: string, text: string) => {
+ chatStore.getState().setDraft(conversationId, text)
+ }, [])
+
+ const getDraft = useCallback((conversationId: string) => {
+ return chatStore.getState().getDraft(conversationId)
+ }, [])
+
+ const clearDraft = useCallback((conversationId: string) => {
+ chatStore.getState().clearDraft(conversationId)
+ }, [])
+
+ const clearFirstNewMessageId = useCallback((conversationId: string) => {
+ chatStore.getState().clearFirstNewMessageId(conversationId)
+ }, [])
+
+ const updateLastSeenMessageId = useCallback((conversationId: string, messageId: string) => {
+ chatStore.getState().updateLastSeenMessageId(conversationId, messageId)
+ }, [])
+
+ const fetchHistory = useCallback(
+ async (conversationId?: string): Promise => {
+ const connectionStatus = connectionStore.getState().status
+ if (connectionStatus !== 'online') return
+
+ const targetId = conversationId ?? chatStore.getState().activeConversationId
+ if (!targetId) return
+
+ const conversation = chatStore.getState().conversations.get(targetId)
+ if (!conversation || conversation.type !== 'chat') return
+
+ const mamState = chatStore.getState().getMAMQueryState(targetId)
+ if (mamState.isLoading) return
+
+ chatStore.getState().setMAMLoading(targetId, true)
+
+ try {
+ let cachedMessages = chatStore.getState().messages.get(targetId)
+ if (!cachedMessages || cachedMessages.length === 0) {
+ await chatStore.getState().loadMessagesFromCache(targetId, { limit: 100 })
+ cachedMessages = chatStore.getState().messages.get(targetId)
+ }
+
+ const newestCachedMessage = cachedMessages?.[cachedMessages.length - 1]
+
+ const queryOptions: { with: string; start?: string } = { with: conversation.id }
+
+ if (newestCachedMessage?.timestamp) {
+ const startTime = new Date(newestCachedMessage.timestamp.getTime() + 1)
+ queryOptions.start = startTime.toISOString()
+ }
+
+ await client.chat.queryMAM(queryOptions)
+ } catch (error) {
+ console.error('Failed to fetch history:', error)
+ } finally {
+ chatStore.getState().setMAMLoading(targetId, false)
+ }
+ },
+ [client]
+ )
+
+ const fetchOlderHistory = useMemo(
+ () =>
+ createFetchOlderHistory({
+ getActiveId: () => chatStore.getState().activeConversationId,
+ isValidTarget: (id) => {
+ const conversation = chatStore.getState().conversations.get(id)
+ return !!conversation && conversation.type === 'chat'
+ },
+ getMAMState: (id) => chatStore.getState().getMAMQueryState(id),
+ setMAMLoading: (id, loading) => chatStore.getState().setMAMLoading(id, loading),
+ loadFromCache: (id, limit) => chatStore.getState().loadOlderMessagesFromCache(id, limit),
+ getOldestMessageId: (id) => {
+ const messages = chatStore.getState().messages.get(id)
+ if (!messages || messages.length === 0) return undefined
+ return messages[0].stanzaId || messages[0].id
+ },
+ queryMAM: async (id, beforeId) => {
+ const conversation = chatStore.getState().conversations.get(id)
+ if (conversation) {
+ await client.chat.queryMAM({ with: conversation.id, before: beforeId })
+ }
+ },
+ errorLogPrefix: 'Failed to fetch older chat history',
+ }),
+ [client]
+ )
+
+ return useMemo(
+ () => ({
+ sendMessage,
+ setActiveConversation,
+ addConversation,
+ deleteConversation,
+ markAsRead,
+ archiveConversation,
+ unarchiveConversation,
+ isArchived,
+ sendChatState,
+ sendReaction,
+ sendCorrection,
+ retractMessage,
+ sendEasterEgg,
+ clearAnimation,
+ setDraft,
+ getDraft,
+ clearDraft,
+ clearFirstNewMessageId,
+ updateLastSeenMessageId,
+ fetchHistory,
+ fetchOlderHistory,
+ }),
+ [
+ sendMessage,
+ setActiveConversation,
+ addConversation,
+ deleteConversation,
+ markAsRead,
+ archiveConversation,
+ unarchiveConversation,
+ isArchived,
+ sendChatState,
+ sendReaction,
+ sendCorrection,
+ retractMessage,
+ sendEasterEgg,
+ clearAnimation,
+ setDraft,
+ getDraft,
+ clearDraft,
+ clearFirstNewMessageId,
+ updateLastSeenMessageId,
+ fetchHistory,
+ fetchOlderHistory,
+ ]
+ )
+}
diff --git a/packages/fluux-sdk/src/hooks/useRoomActions.ts b/packages/fluux-sdk/src/hooks/useRoomActions.ts
new file mode 100644
index 00000000..1de9e1b9
--- /dev/null
+++ b/packages/fluux-sdk/src/hooks/useRoomActions.ts
@@ -0,0 +1,483 @@
+import { useCallback, useMemo } from 'react'
+import { roomStore } from '../stores'
+import { useXMPPContext } from '../provider'
+import type {
+ MentionReference,
+ ChatStateNotification,
+ FileAttachment,
+ RSMRequest,
+ AdminRoom,
+ RSMResponse,
+ RoomAffiliation,
+ RoomRole,
+ PollData,
+ PollSettings,
+} from '../core/types'
+import { createFetchOlderHistory } from './shared'
+
+/**
+ * Action-only counterpart to `useRoom()`.
+ *
+ * Returns the same actions as `useRoom()` but performs ZERO store subscriptions.
+ * Use this in components that only need to invoke room actions and do not need
+ * to react to room state changes (e.g. modals that fire-and-close).
+ *
+ * Calling `useRoom()` subscribes the component to ~15 room store values
+ * (`activeRoom`, `activeMessages`, `joinedRooms`, `roomsWithUnreadCount`, etc.),
+ * causing it to re-render on every room store update. During background MAM
+ * sync this can produce hundreds of re-renders per second. `useRoomActions()`
+ * avoids this by reading actions directly via `roomStore.getState()`.
+ *
+ * @returns A stable object of room action callbacks
+ *
+ * @example
+ * ```tsx
+ * function InviteModal({ room }) {
+ * const { inviteMultipleToRoom } = useRoomActions()
+ * // No re-render when other rooms update during sync
+ * }
+ * ```
+ *
+ * @category Hooks
+ */
+export function useRoomActions() {
+ const { client } = useXMPPContext()
+
+ const joinRoom = useCallback(
+ async (roomJid: string, nickname: string, options?: { maxHistory?: number; password?: string }) => {
+ await client.muc.joinRoom(roomJid, nickname, options)
+ },
+ [client]
+ )
+
+ const createQuickChat = useCallback(
+ async (nickname: string, topic?: string, invitees?: string[]): Promise => {
+ return await client.muc.createQuickChat(nickname, topic, invitees)
+ },
+ [client]
+ )
+
+ const leaveRoom = useCallback(
+ async (roomJid: string) => {
+ await client.muc.leaveRoom(roomJid)
+ },
+ [client]
+ )
+
+ const getRoom = useCallback(
+ (roomJid: string) => roomStore.getState().rooms.get(roomJid),
+ []
+ )
+
+ const setActiveRoom = useCallback(async (roomJid: string | null) => {
+ if (roomJid) {
+ await roomStore.getState().loadMessagesFromCache(roomJid, { limit: 100 })
+ }
+ roomStore.getState().setActiveRoom(roomJid)
+ }, [])
+
+ const markAsRead = useCallback((roomJid: string) => {
+ roomStore.getState().markAsRead(roomJid)
+ }, [])
+
+ const sendMessage = useCallback(
+ async (
+ roomJid: string,
+ body: string,
+ replyTo?: { id: string; to: string; fallback?: { author: string; body: string } },
+ references?: MentionReference[],
+ attachment?: FileAttachment
+ ): Promise => {
+ return await client.chat.sendMessage(roomJid, body, 'groupchat', replyTo, references, attachment)
+ },
+ [client]
+ )
+
+ const sendReaction = useCallback(
+ async (roomJid: string, messageId: string, emojis: string[]) => {
+ await client.chat.sendReaction(roomJid, messageId, emojis, 'groupchat')
+ },
+ [client]
+ )
+
+ const sendPoll = useCallback(
+ async (roomJid: string, title: string, options: string[], settings?: Partial, description?: string, deadline?: string, customEmojis?: string[]) => {
+ return await client.poll.sendPoll(roomJid, title, options, settings, description, deadline, customEmojis)
+ },
+ [client]
+ )
+
+ const votePoll = useCallback(
+ async (roomJid: string, messageId: string, optionEmoji: string, currentMyReactions: string[], poll: PollData, isClosed?: boolean) => {
+ await client.poll.vote(roomJid, messageId, optionEmoji, currentMyReactions, poll, isClosed)
+ },
+ [client]
+ )
+
+ const closePoll = useCallback(
+ async (roomJid: string, messageId: string) => {
+ return await client.poll.closePoll(roomJid, messageId)
+ },
+ [client]
+ )
+
+ const sendCorrection = useCallback(
+ async (roomJid: string, messageId: string, newBody: string, attachment?: FileAttachment) => {
+ await client.chat.sendCorrection(roomJid, messageId, newBody, 'groupchat', attachment)
+ },
+ [client]
+ )
+
+ const retractMessage = useCallback(
+ async (roomJid: string, messageId: string) => {
+ await client.chat.sendRetraction(roomJid, messageId, 'groupchat')
+ },
+ [client]
+ )
+
+ const moderateMessage = useCallback(
+ async (roomJid: string, stanzaId: string, reason?: string) => {
+ await client.muc.moderateMessage(roomJid, stanzaId, reason)
+ },
+ [client]
+ )
+
+ const setBookmark = useCallback(
+ async (
+ roomJid: string,
+ options: { name: string; nick: string; autojoin?: boolean; password?: string }
+ ) => {
+ await client.muc.setBookmark(roomJid, options)
+ },
+ [client]
+ )
+
+ const removeBookmark = useCallback(
+ async (roomJid: string) => {
+ await client.muc.removeBookmark(roomJid)
+ },
+ [client]
+ )
+
+ const setRoomNotifyAll = useCallback(
+ async (roomJid: string, notifyAll: boolean, persistent: boolean = false) => {
+ await client.muc.setRoomNotifyAll(roomJid, notifyAll, persistent)
+ },
+ [client]
+ )
+
+ const sendChatState = useCallback(
+ async (roomJid: string, state: ChatStateNotification) => {
+ await client.chat.sendChatState(roomJid, state, 'groupchat')
+ },
+ [client]
+ )
+
+ const sendEasterEgg = useCallback(
+ async (roomJid: string, animation: string) => {
+ await client.chat.sendEasterEgg(roomJid, 'groupchat', animation)
+ },
+ [client]
+ )
+
+ const clearAnimation = useCallback(() => {
+ roomStore.getState().clearAnimation()
+ }, [])
+
+ const setDraft = useCallback((roomJid: string, text: string) => {
+ roomStore.getState().setDraft(roomJid, text)
+ }, [])
+
+ const getDraft = useCallback((roomJid: string) => {
+ return roomStore.getState().getDraft(roomJid)
+ }, [])
+
+ const clearDraft = useCallback((roomJid: string) => {
+ roomStore.getState().clearDraft(roomJid)
+ }, [])
+
+ const clearFirstNewMessageId = useCallback((roomJid: string) => {
+ roomStore.getState().clearFirstNewMessageId(roomJid)
+ }, [])
+
+ const updateLastSeenMessageId = useCallback((roomJid: string, messageId: string) => {
+ roomStore.getState().updateLastSeenMessageId(roomJid, messageId)
+ }, [])
+
+ const setRoomAvatar = useCallback(
+ async (roomJid: string, imageData: Uint8Array, mimeType: string) => {
+ const base64 = btoa(String.fromCharCode(...Array.from(imageData)))
+ const dataUrl = `data:${mimeType};base64,${base64}`
+ await client.profile.setRoomAvatar(roomJid, dataUrl, mimeType)
+ },
+ [client]
+ )
+
+ const clearRoomAvatar = useCallback(
+ async (roomJid: string) => {
+ await client.profile.clearRoomAvatar(roomJid)
+ },
+ [client]
+ )
+
+ const restoreRoomAvatarFromCache = useCallback(
+ async (roomJid: string, avatarHash: string) => {
+ return client.profile.restoreRoomAvatarFromCache(roomJid, avatarHash)
+ },
+ [client]
+ )
+
+ const browsePublicRooms = useCallback(
+ async (mucServiceJid?: string, rsm?: RSMRequest): Promise<{ rooms: AdminRoom[]; pagination: RSMResponse }> => {
+ return client.admin.fetchRoomList(mucServiceJid, rsm)
+ },
+ [client]
+ )
+
+ const inviteToRoom = useCallback(
+ async (roomJid: string, inviteeJid: string, reason?: string) => {
+ await client.muc.sendMediatedInvitation(roomJid, inviteeJid, reason)
+ },
+ [client]
+ )
+
+ const inviteMultipleToRoom = useCallback(
+ async (roomJid: string, inviteeJids: string[], reason?: string) => {
+ await client.muc.sendMediatedInvitations(roomJid, inviteeJids, reason)
+ },
+ [client]
+ )
+
+ const submitRoomConfig = useCallback(
+ async (roomJid: string, values: Record) => {
+ await client.muc.submitRoomConfig(roomJid, values)
+ },
+ [client]
+ )
+
+ const setSubject = useCallback(
+ async (roomJid: string, subject: string) => {
+ await client.muc.setSubject(roomJid, subject)
+ },
+ [client]
+ )
+
+ const createRoom = useCallback(
+ async (
+ roomJid: string,
+ nickname: string,
+ config: {
+ name: string
+ description?: string
+ isPublic?: boolean
+ membersOnly?: boolean
+ extraFields?: Record
+ },
+ options?: { invitees?: string[] }
+ ) => {
+ await client.muc.createRoom(roomJid, nickname, config, options)
+ },
+ [client]
+ )
+
+ const destroyRoom = useCallback(
+ async (roomJid: string, reason?: string, alternateRoomJid?: string) => {
+ await client.muc.destroyRoom(roomJid, reason, alternateRoomJid)
+ },
+ [client]
+ )
+
+ const roomExists = useCallback(
+ async (roomJid: string): Promise => {
+ return client.muc.roomExists(roomJid)
+ },
+ [client]
+ )
+
+ const setAffiliation = useCallback(
+ async (roomJid: string, userJid: string, affiliation: RoomAffiliation, reason?: string) => {
+ await client.muc.setAffiliation(roomJid, userJid, affiliation, reason)
+ },
+ [client]
+ )
+
+ const setRole = useCallback(
+ async (roomJid: string, nick: string, role: RoomRole, reason?: string) => {
+ await client.muc.setRole(roomJid, nick, role, reason)
+ },
+ [client]
+ )
+
+ const queryAffiliationList = useCallback(
+ async (roomJid: string, affiliation: RoomAffiliation) => {
+ return client.muc.queryAffiliationList(roomJid, affiliation)
+ },
+ [client]
+ )
+
+ const listHats = useCallback(
+ async (roomJid: string) => {
+ return client.muc.listHats(roomJid)
+ },
+ [client]
+ )
+
+ const createHat = useCallback(
+ async (roomJid: string, title: string, uri: string, hue?: number) => {
+ await client.muc.createHat(roomJid, title, uri, hue)
+ },
+ [client]
+ )
+
+ const destroyHat = useCallback(
+ async (roomJid: string, uri: string) => {
+ await client.muc.destroyHat(roomJid, uri)
+ },
+ [client]
+ )
+
+ const listHatAssignments = useCallback(
+ async (roomJid: string) => {
+ return client.muc.listHatAssignments(roomJid)
+ },
+ [client]
+ )
+
+ const assignHat = useCallback(
+ async (roomJid: string, userJid: string, hatUri: string) => {
+ await client.muc.assignHat(roomJid, userJid, hatUri)
+ },
+ [client]
+ )
+
+ const unassignHat = useCallback(
+ async (roomJid: string, userJid: string, hatUri: string) => {
+ await client.muc.unassignHat(roomJid, userJid, hatUri)
+ },
+ [client]
+ )
+
+ const fetchOlderHistory = useMemo(
+ () =>
+ createFetchOlderHistory({
+ getActiveId: () => roomStore.getState().activeRoomJid,
+ isValidTarget: (id) => {
+ const room = roomStore.getState().rooms.get(id)
+ return !!room && !room.isQuickChat
+ },
+ getMAMState: (id) => roomStore.getState().getRoomMAMQueryState(id),
+ setMAMLoading: (id, loading) => roomStore.getState().setRoomMAMLoading(id, loading),
+ loadFromCache: (id, limit) => roomStore.getState().loadOlderMessagesFromCache(id, limit),
+ getOldestMessageId: (id) => {
+ const room = roomStore.getState().rooms.get(id)
+ const messages = room?.messages
+ if (!messages || messages.length === 0) return undefined
+ return messages[0].stanzaId || messages[0].id
+ },
+ queryMAM: async (id, beforeId) => {
+ await client.chat.queryRoomMAM({ roomJid: id, before: beforeId })
+ },
+ errorLogPrefix: 'Failed to fetch older room history',
+ }),
+ [client]
+ )
+
+ return useMemo(
+ () => ({
+ joinRoom,
+ createQuickChat,
+ leaveRoom,
+ getRoom,
+ setActiveRoom,
+ markAsRead,
+ sendMessage,
+ sendReaction,
+ sendPoll,
+ votePoll,
+ closePoll,
+ sendCorrection,
+ retractMessage,
+ moderateMessage,
+ sendChatState,
+ setBookmark,
+ removeBookmark,
+ setRoomNotifyAll,
+ sendEasterEgg,
+ clearAnimation,
+ setDraft,
+ getDraft,
+ clearDraft,
+ clearFirstNewMessageId,
+ updateLastSeenMessageId,
+ setRoomAvatar,
+ clearRoomAvatar,
+ restoreRoomAvatarFromCache,
+ browsePublicRooms,
+ inviteToRoom,
+ inviteMultipleToRoom,
+ submitRoomConfig,
+ setSubject,
+ createRoom,
+ destroyRoom,
+ roomExists,
+ setAffiliation,
+ setRole,
+ queryAffiliationList,
+ listHats,
+ createHat,
+ destroyHat,
+ listHatAssignments,
+ assignHat,
+ unassignHat,
+ fetchOlderHistory,
+ }),
+ [
+ joinRoom,
+ createQuickChat,
+ leaveRoom,
+ getRoom,
+ setActiveRoom,
+ markAsRead,
+ sendMessage,
+ sendReaction,
+ sendPoll,
+ votePoll,
+ closePoll,
+ sendCorrection,
+ retractMessage,
+ moderateMessage,
+ sendChatState,
+ setBookmark,
+ removeBookmark,
+ setRoomNotifyAll,
+ sendEasterEgg,
+ clearAnimation,
+ setDraft,
+ getDraft,
+ clearDraft,
+ clearFirstNewMessageId,
+ updateLastSeenMessageId,
+ setRoomAvatar,
+ clearRoomAvatar,
+ restoreRoomAvatarFromCache,
+ browsePublicRooms,
+ inviteToRoom,
+ inviteMultipleToRoom,
+ submitRoomConfig,
+ setSubject,
+ createRoom,
+ destroyRoom,
+ roomExists,
+ setAffiliation,
+ setRole,
+ queryAffiliationList,
+ listHats,
+ createHat,
+ destroyHat,
+ listHatAssignments,
+ assignHat,
+ unassignHat,
+ fetchOlderHistory,
+ ]
+ )
+}
diff --git a/packages/fluux-sdk/src/index.ts b/packages/fluux-sdk/src/index.ts
index 6ab28645..7f43275e 100644
--- a/packages/fluux-sdk/src/index.ts
+++ b/packages/fluux-sdk/src/index.ts
@@ -86,6 +86,7 @@ export type { XMPPProviderProps } from './provider'
export { useConnection } from './hooks/useConnection'
export { useChat } from './hooks/useChat'
export { useChatActive } from './hooks/useChatActive'
+export { useChatActions } from './hooks/useChatActions'
export { useRoster } from './hooks/useRoster'
export { useRosterActions } from './hooks/useRosterActions'
export { useContactIdentities, type ContactIdentity } from './hooks/useContactIdentities'
@@ -94,6 +95,7 @@ export { useEvents } from './hooks/useEvents'
export { useActivityLog } from './hooks/useActivityLog'
export { useRoom } from './hooks/useRoom'
export { useRoomActive } from './hooks/useRoomActive'
+export { useRoomActions } from './hooks/useRoomActions'
export { useXMPP } from './hooks/useXMPP'
export { useAdmin } from './hooks/useAdmin'
export { useBlocking } from './hooks/useBlocking'