diff --git a/web/src/ui/modal/Modal.tsx b/web/src/ui/modal/Modal.tsx
new file mode 100644
index 0000000..43b6078
--- /dev/null
+++ b/web/src/ui/modal/Modal.tsx
@@ -0,0 +1,47 @@
+// gomuks - A Matrix client written in Go.
+// Copyright (C) 2024 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see
.
+import React, { JSX, createContext, useCallback, useState } from "react"
+
+export interface ModalState {
+ content: JSX.Element
+ dimmed?: boolean
+ wrapperClass?: string
+ onClose?: () => void
+}
+
+type openModal = (state: ModalState) => void
+
+export const ModalContext = createContext
(() =>
+ console.error("Tried to open modal without being inside context"))
+
+export const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
+ const [state, setState] = useState(null)
+ const onClose = useCallback(() => {
+ setState(null)
+ state?.onClose?.()
+ }, [state])
+ return <>
+
+ {children}
+
+ {state &&
+ {state.content}
+
}
+ >
+}
diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx
index eb5da5d..57270f4 100644
--- a/web/src/ui/timeline/TimelineEvent.tsx
+++ b/web/src/ui/timeline/TimelineEvent.tsx
@@ -19,7 +19,7 @@ import { useRoomState } from "@/api/statestore"
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
import { isEventID } from "@/util/validation.ts"
import { ClientContext } from "../ClientContext.ts"
-import { LightboxContext } from "../Lightbox.tsx"
+import { LightboxContext } from "../modal/Lightbox.tsx"
import { useRoomContext } from "../roomcontext.ts"
import EventMenu from "./EventMenu.tsx"
import { ReplyIDBody } from "./ReplyBody.tsx"
diff --git a/web/src/ui/timeline/content/MemberBody.tsx b/web/src/ui/timeline/content/MemberBody.tsx
index 7840724..af9bdeb 100644
--- a/web/src/ui/timeline/content/MemberBody.tsx
+++ b/web/src/ui/timeline/content/MemberBody.tsx
@@ -16,7 +16,7 @@
import React, { use } from "react"
import { getAvatarURL } from "@/api/media.ts"
import { MemberEventContent, UserID } from "@/api/types"
-import { LightboxContext } from "../../Lightbox.tsx"
+import { LightboxContext } from "../../modal/Lightbox.tsx"
import EventContentProps from "./props.ts"
function useChangeDescription(
diff --git a/web/src/ui/timeline/content/useMediaContent.tsx b/web/src/ui/timeline/content/useMediaContent.tsx
index b33e309..90cc573 100644
--- a/web/src/ui/timeline/content/useMediaContent.tsx
+++ b/web/src/ui/timeline/content/useMediaContent.tsx
@@ -17,7 +17,7 @@ import { CSSProperties, use } from "react"
import { getEncryptedMediaURL, getMediaURL } from "@/api/media.ts"
import type { EventType, MediaMessageEventContent } from "@/api/types"
import { ImageContainerSize, calculateMediaSize } from "@/util/mediasize.ts"
-import { LightboxContext } from "../../Lightbox.tsx"
+import { LightboxContext } from "../../modal/Lightbox.tsx"
import DownloadIcon from "@/icons/download.svg?react"
export const useMediaContent = (