diff --git a/package-lock.json b/package-lock.json
index 5d5a0131..c216c3d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,7 +19,9 @@
"emoji-picker-react": "^4.13.3",
"exifr": "^7.1.3",
"gh-pages": "^6.3.0",
+ "highlight.js": "^11.11.1",
"html-react-parser": "^5.2.7",
+ "immer": "^10.2.0",
"marked": "^16.4.0",
"react": "^18.3.1",
"react-color": "^2.19.3",
@@ -45,7 +47,6 @@
"@vitest/ui": "^3.1.2",
"autoprefixer": "^10.4.20",
"globals": "^15.14.0",
- "highlight.js": "^11.11.1",
"jsdom": "^26.1.0",
"lefthook": "^1.11.10",
"postcss": "^8.5.1",
@@ -4106,7 +4107,6 @@
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
@@ -4225,6 +4225,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
diff --git a/package.json b/package.json
index b14a8026..0e74b3cc 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"gh-pages": "^6.3.0",
"highlight.js": "^11.11.1",
"html-react-parser": "^5.2.7",
+ "immer": "^10.2.0",
"marked": "^16.4.0",
"react": "^18.3.1",
"react-color": "^2.19.3",
diff --git a/src/App.tsx b/src/App.tsx
index 786ce01c..3fa9a111 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,6 +6,7 @@ import type React from "react";
import { useEffect, useState } from "react";
import AppLayout from "./components/layout/AppLayout";
import { ServerNoticesPopup } from "./components/message/ServerNoticesPopup";
+import { ModalStackProvider } from "./components/modals";
import AddServerModal from "./components/ui/AddServerModal";
import ChannelListModal from "./components/ui/ChannelListModal";
import ChannelRenameModal from "./components/ui/ChannelRenameModal";
@@ -29,10 +30,7 @@ const askPermissions = async () => {
};
const initializeEnvSettings = (
- toggleAddServerModal: (
- isOpen?: boolean,
- prefillDetails?: ConnectionDetails | null,
- ) => void,
+ openModal: (modalId: string, props?: unknown) => void,
joinChannel: (serverId: string, channelName: string) => void,
) => {
if (loadSavedServers().length > 0) return;
@@ -47,7 +45,7 @@ const initializeEnvSettings = (
}
if (!__DEFAULT_IRC_SERVER_NAME__) {
}
- toggleAddServerModal(true, {
+ openModal("addServer", {
name: __DEFAULT_IRC_SERVER_NAME__ || "Obsidian IRC",
host,
port,
@@ -68,19 +66,9 @@ const initializeEnvSettings = (
const App: React.FC = () => {
const {
- toggleAddServerModal,
- toggleEditServerModal,
- ui: {
- isAddServerModalOpen,
- isUserProfileModalOpen,
- isChannelListModalOpen,
- isChannelRenameModalOpen,
- isServerNoticesPopupOpen,
- isEditServerModalOpen,
- editServerId,
- linkSecurityWarnings,
- profileViewRequest,
- },
+ openModal,
+ closeModal,
+ ui: { isServerNoticesPopupOpen, linkSecurityWarnings, profileViewRequest },
joinChannel,
connectToSavedServers,
toggleServerNoticesPopup,
@@ -138,47 +126,44 @@ const App: React.FC = () => {
// askPermissions();
useEffect(() => {
- initializeEnvSettings(toggleAddServerModal, joinChannel);
+ initializeEnvSettings(openModal, joinChannel);
// Auto-reconnect to saved servers on app startup
connectToSavedServers();
}, [
- toggleAddServerModal,
+ openModal,
joinChannel, // Auto-reconnect to saved servers on app startup
connectToSavedServers,
]); // Removed connectToSavedServers from dependencies
return (
-
-
- {isAddServerModalOpen &&
}
- {isEditServerModalOpen && editServerId && (
-
toggleEditServerModal(false)}
- />
- )}
- {isUserProfileModalOpen && }
- {isChannelListModalOpen && }
- {isChannelRenameModalOpen && }
-
- {userProfileModalState?.isOpen && (
- setUserProfileModalState(null)}
- serverId={userProfileModalState.serverId}
- username={userProfileModalState.username}
- />
- )}
- {isServerNoticesPopupOpen && (
- toggleServerNoticesPopup(false)}
- onUsernameContextMenu={handleUsernameContextMenu}
- onIrcLinkClick={handleIrcLinkClick}
- joinChannel={joinChannel}
- />
- )}
-
+
+
+
+
+
+
+
+
+
+ {userProfileModalState?.isOpen && (
+
setUserProfileModalState(null)}
+ serverId={userProfileModalState.serverId}
+ username={userProfileModalState.username}
+ />
+ )}
+ {isServerNoticesPopupOpen && (
+ toggleServerNoticesPopup(false)}
+ onUsernameContextMenu={handleUsernameContextMenu}
+ onIrcLinkClick={handleIrcLinkClick}
+ joinChannel={joinChannel}
+ />
+ )}
+
+
);
};
diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx
index 12eac11d..300f9e18 100644
--- a/src/components/layout/AppLayout.tsx
+++ b/src/components/layout/AppLayout.tsx
@@ -2,7 +2,7 @@ import { platform } from "@tauri-apps/plugin-os";
import type React from "react";
import { useEffect } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
-import useStore from "../../store";
+import useStore, { type layoutColumn } from "../../store";
import { GlobalNotifications } from "../ui/GlobalNotifications";
import { ChannelList } from "./ChannelList";
import { ChatArea } from "./ChatArea";
diff --git a/src/components/layout/ChannelList.tsx b/src/components/layout/ChannelList.tsx
index 09ae7b48..0dda3732 100644
--- a/src/components/layout/ChannelList.tsx
+++ b/src/components/layout/ChannelList.tsx
@@ -38,7 +38,7 @@ export const ChannelList: React.FC<{
pinPrivateChat,
unpinPrivateChat,
reorderPrivateChats,
- toggleUserProfileModal,
+ openModal,
setMobileViewActiveColumn,
reorderChannels,
} = useStore();
@@ -1276,7 +1276,7 @@ export const ChannelList: React.FC<{
toggleUserProfileModal(true)}
+ onClick={() => openModal("settings")}
>
@@ -1341,7 +1341,7 @@ export const ChannelList: React.FC<{
diff --git a/src/components/layout/ChatArea.tsx b/src/components/layout/ChatArea.tsx
index 390766fc..adf38c1c 100644
--- a/src/components/layout/ChatArea.tsx
+++ b/src/components/layout/ChatArea.tsx
@@ -169,7 +169,7 @@ export const ChatArea: React.FC<{
const selectPrivateChat = useStore((state) => state.selectPrivateChat);
const connect = useStore((state) => state.connect);
const joinChannel = useStore((state) => state.joinChannel);
- const toggleAddServerModal = useStore((state) => state.toggleAddServerModal);
+ const openModal = useStore((state) => state.openModal);
const redactMessage = useStore((state) => state.redactMessage);
const warnUser = useStore((state) => state.warnUser);
const kickUser = useStore((state) => state.kickUser);
@@ -182,15 +182,10 @@ export const ChatArea: React.FC<{
selectedPrivateChatId: null,
};
const { selectedChannelId, selectedPrivateChatId } = currentSelection;
- const {
- isMemberListVisible,
- isSettingsModalOpen,
- isUserProfileModalOpen,
- isAddServerModalOpen,
- isChannelListModalOpen,
- isChannelRenameModalOpen,
- isServerNoticesPopupOpen,
- } = ui;
+ const { isMemberListVisible, isServerNoticesPopupOpen } = ui;
+
+ // Check if settings modal is open via modal manager
+ const isSettingsModalOpen = ui.modals.settings?.isOpen || false;
const isMobile = useMediaQuery("(max-width: 768px)");
@@ -274,7 +269,7 @@ export const ChatArea: React.FC<{
const parsed = parseIrcUrl(rawUrl, currentUser?.username || "user");
// Open the connect modal with pre-filled server details
- toggleAddServerModal(true, {
+ openModal("addServer", {
name: parsed.host,
host: parsed.host,
port: parsed.port.toString(),
@@ -1409,23 +1404,19 @@ export const ChatArea: React.FC<{
if ("__TAURI__" in window && ["android", "ios"].includes(platform()))
return;
// Don't steal focus if any modal is open
- if (
+ const isAnyModalOpen =
isSettingsModalOpen ||
- isUserProfileModalOpen ||
- isAddServerModalOpen ||
- isChannelListModalOpen ||
- isChannelRenameModalOpen
- )
- return;
+ ui.modals.addServer?.isOpen ||
+ ui.modals.editServer?.isOpen ||
+ ui.modals.channelList?.isOpen ||
+ ui.modals.channelRename?.isOpen;
+ if (isAnyModalOpen) return;
inputRef.current?.focus();
}, [
selectedChannelId,
selectedPrivateChatId,
isSettingsModalOpen,
- isUserProfileModalOpen,
- isAddServerModalOpen,
- isChannelListModalOpen,
- isChannelRenameModalOpen,
+ ui.modals,
]);
return (
diff --git a/src/components/layout/ChatHeader.tsx b/src/components/layout/ChatHeader.tsx
index 01511a1d..c441c326 100644
--- a/src/components/layout/ChatHeader.tsx
+++ b/src/components/layout/ChatHeader.tsx
@@ -67,13 +67,8 @@ export const ChatHeader: React.FC
= ({
onOpenChannelSettings,
onOpenInviteUser,
}) => {
- const {
- toggleChannelListModal,
- toggleChannelRenameModal,
- toggleMemberList,
- pinPrivateChat,
- unpinPrivateChat,
- } = useStore();
+ const { openModal, toggleMemberList, pinPrivateChat, unpinPrivateChat } =
+ useStore();
const [isEditingTopic, setIsEditingTopic] = useState(false);
const [editedTopic, setEditedTopic] = useState("");
const [avatarLoadFailed, setAvatarLoadFailed] = useState(false);
@@ -584,7 +579,7 @@ export const ChatHeader: React.FC = ({
)}