From 3dcc1df45187d0947ef8f68d0f9af13d0bbbbe0f Mon Sep 17 00:00:00 2001
From: Sebastian Mohr <sebastian@mohrenclan.de>
Date: Mon, 6 Jan 2025 14:28:58 +0100
Subject: [PATCH] Added possibility to trigger context menu on mobile devices.

---
 CHANGELOG.md                                  |  2 +-
 .../debug/playground/inputs/contextMenu.tsx   | 24 +++++++++
 .../app/debug/playground/inputs/page.tsx      |  2 +
 .../editor/sidebar/previews/previewSnips.tsx  | 16 +++---
 .../lib/hooks/useMobileSafeContextMenu.ts     | 54 +++++++++++++++++++
 5 files changed, 90 insertions(+), 8 deletions(-)
 create mode 100644 apps/fullstack/app/debug/playground/inputs/contextMenu.tsx
 create mode 100644 apps/fullstack/lib/hooks/useMobileSafeContextMenu.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b32a74b2..234bc7d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,7 @@ the labels did not update correctly and were not removed correctly
 - New snips notification not disappearing from the not seen list once they are placed see [#100](https://gitlab.gwdg.de/irp/snip/-/issues/100)
 - Sidebar not clickable in fullscreen on some mobile devices. Added safe area padding to the sidebar to avoid this issue.
 - Reconnection issue on stale pages to websocket server see [#89](https://gitlab.gwdg.de/irp/snip/-/issues/89)
-
+- Allow contextMenu click on mobile devices by holding the touch event for a longer time
 
 ## [1.9.0]
 
diff --git a/apps/fullstack/app/debug/playground/inputs/contextMenu.tsx b/apps/fullstack/app/debug/playground/inputs/contextMenu.tsx
new file mode 100644
index 00000000..ff24b579
--- /dev/null
+++ b/apps/fullstack/app/debug/playground/inputs/contextMenu.tsx
@@ -0,0 +1,24 @@
+import useMobileSafeContextMenu from "lib/hooks/useMobileSafeContextMenu";
+
+import { Wrapper } from "./utils";
+
+export const ContextMenu = () => {
+    const eleProps = useMobileSafeContextMenu(() => {
+        alert("context menu");
+    });
+    return (
+        <Wrapper>
+            <h3>ContextMenu</h3>
+            <div
+                {...eleProps}
+                style={{
+                    width: "100px",
+                    height: "100px",
+                    backgroundColor: "red",
+                }}
+            >
+                Right click me or long press me on mobile
+            </div>
+        </Wrapper>
+    );
+};
diff --git a/apps/fullstack/app/debug/playground/inputs/page.tsx b/apps/fullstack/app/debug/playground/inputs/page.tsx
index 5c1bf9b1..37fa81a3 100644
--- a/apps/fullstack/app/debug/playground/inputs/page.tsx
+++ b/apps/fullstack/app/debug/playground/inputs/page.tsx
@@ -3,6 +3,7 @@
 import React, { useState } from "react";
 
 import ColorInputs from "./color";
+import { ContextMenu } from "./contextMenu";
 import NumberInputs from "./number";
 import OptionInputs from "./options";
 import TextInputs from "./text";
@@ -266,6 +267,7 @@ const Page = () => {
                     showValid={state.showValid}
                     showInvalid={state.showInvalid}
                 />
+                <ContextMenu />
             </div>
         </div>
     );
diff --git a/apps/fullstack/components/editor/sidebar/previews/previewSnips.tsx b/apps/fullstack/components/editor/sidebar/previews/previewSnips.tsx
index e3b20521..9f11f964 100644
--- a/apps/fullstack/components/editor/sidebar/previews/previewSnips.tsx
+++ b/apps/fullstack/components/editor/sidebar/previews/previewSnips.tsx
@@ -82,6 +82,7 @@ export function PreviewSnips({
     );
 }
 
+import useMobileSafeContextMenu from "lib/hooks/useMobileSafeContextMenu";
 import {
     TbLink,
     TbLinkOff,
@@ -131,7 +132,7 @@ function PreviewSingleSnip({
 }) {
     const canvasRef = useRef<HTMLCanvasElement>(null);
 
-    // Prevernts hydration mismatch
+    // Prevents hydration mismatch
     const [info, setInfo] = useState<Info>({
         width: 0,
         height: 0,
@@ -139,8 +140,14 @@ function PreviewSingleSnip({
         id: -1,
     });
 
+    // Context menu
     const [showContext, setShowContext] = useState(false);
 
+    const contextMenuProps = useMobileSafeContextMenu((e) => {
+        e.preventDefault();
+        setShowContext((p) => !p);
+    });
+
     //Render snip
     useEffect(() => {
         const ctx = canvasRef.current!.getContext("2d")!;
@@ -208,17 +215,12 @@ function PreviewSingleSnip({
                 className={styles.snip}
                 data-active={active}
                 data-hidden={hidden}
-                onContextMenu={(e) => {
-                    e.preventDefault();
-                    setShowContext((p) => !p);
-                    // Handle outside click
-                    //document.addEventListener("click", outsideClickListener);
-                }}
                 onPointerMove={() => {
                     if (newBadge) {
                         resetNewBadge?.();
                     }
                 }}
+                {...contextMenuProps}
             >
                 <FlipCard
                     flipped={showContext}
diff --git a/apps/fullstack/lib/hooks/useMobileSafeContextMenu.ts b/apps/fullstack/lib/hooks/useMobileSafeContextMenu.ts
new file mode 100644
index 00000000..0f6da2fb
--- /dev/null
+++ b/apps/fullstack/lib/hooks/useMobileSafeContextMenu.ts
@@ -0,0 +1,54 @@
+import { UIEvent, useRef } from "react";
+
+/**
+ * A custom hook to handle context menu events in a mobile-friendly way.
+ * It triggers a custom context menu callback after a long press on touch devices
+ * or a right-click on desktop devices.
+ *
+ * @param onContextMenu - The callback function to execute when the context menu is triggered.
+ * @param duration - The duration (in milliseconds) to wait before triggering the context menu on touch devices. Default is 500ms.
+ * @returns An object containing event handlers to be spread onto the target element.
+ *
+ * @example
+ * const elemProps = useMobileSafeContextMenu((event) => {
+ *   console.log("Context menu triggered", event);
+ * });
+ *
+ * return (
+ *   <div
+ *     {...elemProps}
+ *   >
+ *     Right-click or long-press me
+ *   </div>
+ * );
+ */
+const useMobileSafeContextMenu = (
+    onContextMenu: (event: UIEvent) => void,
+    duration = 500,
+) => {
+    const pressTimer = useRef<number | null>(null);
+
+    const start = (event: UIEvent) => {
+        pressTimer.current = window.setTimeout(() => {
+            onContextMenu(event); // Trigger the context menu callback
+        }, duration);
+    };
+
+    const stop = () => {
+        if (pressTimer.current !== null) {
+            window.clearTimeout(pressTimer.current);
+        }
+    };
+
+    return {
+        onTouchStart: start,
+        onTouchEnd: stop,
+        onTouchCancel: stop,
+        onContextMenu: (event: UIEvent) => {
+            event.preventDefault(); // Prevent default context menu
+            onContextMenu(event);
+        },
+    };
+};
+
+export default useMobileSafeContextMenu;
-- 
GitLab