(null);
- const isTouchDevice = useIsTouchDevice();
+ const { shouldUseTouchUI } = useDeviceInfo();
+ const isTouchDevice = shouldUseTouchUI;
const handleOpenDialog = useCallback((reactionKey: string) => {
setDialogReaction(reactionKey);
diff --git a/components/waves/drops/participation/EndedParticipationDrop.tsx b/components/waves/drops/participation/EndedParticipationDrop.tsx
index 8c6e53b047..d74e57373a 100644
--- a/components/waves/drops/participation/EndedParticipationDrop.tsx
+++ b/components/waves/drops/participation/EndedParticipationDrop.tsx
@@ -8,7 +8,7 @@ import { getTimeAgoShort } from "@/helpers/Helpers";
import { getWaveRoute } from "@/helpers/navigation.helpers";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import useIsMobileDevice from "@/hooks/isMobileDevice";
-import useIsTouchDevice from "@/hooks/useIsTouchDevice";
+import useDeviceInfo from "@/hooks/useDeviceInfo";
import type { ActiveDropState } from "@/types/dropInteractionTypes";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -52,7 +52,8 @@ export default function EndedParticipationDrop({
const [longPressTriggered, setLongPressTriggered] = useState(false);
const [isSlideUp, setIsSlideUp] = useState(false);
const isMobile = useIsMobileDevice();
- const hasTouch = useIsTouchDevice() || isMobile;
+ const { shouldUseTouchUI } = useDeviceInfo();
+ const hasTouch = shouldUseTouchUI || isMobile;
const handleNavigation = (e: React.MouseEvent, path: string) => {
e.preventDefault();
diff --git a/components/waves/drops/participation/OngoingParticipationDrop.tsx b/components/waves/drops/participation/OngoingParticipationDrop.tsx
index 0a40c90fa7..ecaae2eaa5 100644
--- a/components/waves/drops/participation/OngoingParticipationDrop.tsx
+++ b/components/waves/drops/participation/OngoingParticipationDrop.tsx
@@ -14,7 +14,7 @@ import ParticipationDropHeader from "./ParticipationDropHeader";
import ParticipationDropContent from "./ParticipationDropContent";
import ParticipationDropMetadata from "./ParticipationDropMetadata";
import ParticipationDropFooter from "./ParticipationDropFooter";
-import useIsTouchDevice from "@/hooks/useIsTouchDevice";
+import useDeviceInfo from "@/hooks/useDeviceInfo";
interface OngoingParticipationDropProps {
readonly drop: ExtendedDrop;
@@ -41,7 +41,8 @@ export default function OngoingParticipationDrop({
}: OngoingParticipationDropProps) {
const isActiveDrop = activeDrop?.drop.id === drop.id;
const isMobile = useIsMobileDevice();
- const hasTouch = useIsTouchDevice() || isMobile;
+ const { shouldUseTouchUI } = useDeviceInfo();
+ const hasTouch = shouldUseTouchUI || isMobile;
const [activePartIndex, setActivePartIndex] = useState(0);
const [longPressTriggered, setLongPressTriggered] = useState(false);
diff --git a/components/waves/drops/participation/ParticipationDropContent.tsx b/components/waves/drops/participation/ParticipationDropContent.tsx
index 3c6441dc31..a58c684d93 100644
--- a/components/waves/drops/participation/ParticipationDropContent.tsx
+++ b/components/waves/drops/participation/ParticipationDropContent.tsx
@@ -1,7 +1,7 @@
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import WaveDropContent from "../WaveDropContent";
-import useIsTouchDevice from "@/hooks/useIsTouchDevice";
+import useDeviceInfo from "@/hooks/useDeviceInfo";
interface ParticipationDropContentProps {
readonly drop: ExtendedDrop;
@@ -24,7 +24,8 @@ export default function ParticipationDropContent({
setLongPressTriggered,
isCompetitionDrop = false,
}: ParticipationDropContentProps) {
- const hasTouch = useIsTouchDevice();
+ const { shouldUseTouchUI } = useDeviceInfo();
+ const hasTouch = shouldUseTouchUI;
return (
diff --git a/components/waves/drops/winner/DefaultWinnerDrop.tsx b/components/waves/drops/winner/DefaultWinnerDrop.tsx
index 582d384ec2..a52bd4c89d 100644
--- a/components/waves/drops/winner/DefaultWinnerDrop.tsx
+++ b/components/waves/drops/winner/DefaultWinnerDrop.tsx
@@ -4,7 +4,7 @@ import type { ApiDrop } from "@/generated/models/ApiDrop";
import { getWaveRoute } from "@/helpers/navigation.helpers";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import useIsMobileDevice from "@/hooks/isMobileDevice";
-import useIsTouchDevice from "@/hooks/useIsTouchDevice";
+import useDeviceInfo from "@/hooks/useDeviceInfo";
import type { ActiveDropState } from "@/types/dropInteractionTypes";
import Link from "next/link";
import { memo, useCallback, useState } from "react";
@@ -106,7 +106,8 @@ const DefaultWinnerDrop = ({
const isActiveDrop = activeDrop?.drop.id === drop.id;
const isStorm = drop.parts.length > 1;
const isMobile = useIsMobileDevice();
- const hasTouch = useIsTouchDevice() || isMobile;
+ const { shouldUseTouchUI } = useDeviceInfo();
+ const hasTouch = shouldUseTouchUI || isMobile;
const effectiveRank = drop.winning_context?.place ?? drop.rank;
diff --git a/components/waves/gallery/WaveGalleryItem.tsx b/components/waves/gallery/WaveGalleryItem.tsx
index 7dc28f6afa..f54187b812 100644
--- a/components/waves/gallery/WaveGalleryItem.tsx
+++ b/components/waves/gallery/WaveGalleryItem.tsx
@@ -21,7 +21,7 @@ interface WaveGalleryItemProps {
export const WaveGalleryItem = memo(
({ drop, onDropClick }) => {
const isTabletOrSmaller = useMediaQuery("(max-width: 1023px)");
- const { hasTouchScreen } = useDeviceInfo();
+ const { shouldUseTouchUI } = useDeviceInfo();
const mediaImageScale = isTabletOrSmaller
? ImageScale.AUTOx450
: ImageScale.AUTOx1080;
@@ -37,13 +37,13 @@ export const WaveGalleryItem = memo(
onDropClick(drop);
};
- const transitionClasses = !hasTouchScreen
+ const transitionClasses = !shouldUseTouchUI
? "tw-transition-all tw-duration-300 tw-ease-out"
: "";
const containerClass = `tw-group ${transitionClasses} tw-relative tw-bg-iron-950/50 tw-rounded-xl tw-overflow-hidden desktop-hover:hover:tw-ring-1 desktop-hover:hover:tw-ring-iron-700`;
- const imageScaleClasses = hasTouchScreen
+ const imageScaleClasses = shouldUseTouchUI
? ""
: "tw-transform tw-duration-500 tw-ease-out group-hover:tw-scale-105";
diff --git a/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx b/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
index 95dbf652d3..e90d07cd48 100644
--- a/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
+++ b/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
@@ -30,12 +30,12 @@ export const DefaultWaveLeaderboardDrop: React.FC<
> = ({ drop, onDropClick }) => {
const { canShowVote, canDelete } = useDropInteractionRules(drop);
const [isVotingModalOpen, setIsVotingModalOpen] = useState(false);
- const { hasTouchScreen } = useDeviceInfo();
+ const { shouldUseTouchUI } = useDeviceInfo();
const isMobileScreen = useIsMobileScreen();
// Use the hook for long press interactions
const { isActive, setIsActive, touchHandlers } = useLongPressInteraction({
- hasTouchScreen,
+ hasTouchScreen: shouldUseTouchUI,
});
const getBorderClasses = () => {
@@ -131,7 +131,7 @@ export const DefaultWaveLeaderboardDrop: React.FC<
)}
{/* Mobile menu slide-up */}
- {hasTouchScreen &&
+ {shouldUseTouchUI &&
createPortal(
(
const [isHighlighting, setIsHighlighting] = useState(false);
const isMobileScreen = useIsMobileScreen();
const isTabletOrSmaller = useMediaQuery("(max-width: 1023px)");
- const { hasTouchScreen } = useDeviceInfo();
+ const { shouldUseTouchUI } = useDeviceInfo();
const { canShowVote } = useDropInteractionRules(drop);
const mediaImageScale = isTabletOrSmaller
? ImageScale.AUTOx450
@@ -57,7 +57,7 @@ export const WaveLeaderboardGalleryItem = memo(
const timerRef = useRef(null);
useEffect(() => {
- if (hasTouchScreen) {
+ if (shouldUseTouchUI) {
return;
}
@@ -87,7 +87,7 @@ export const WaveLeaderboardGalleryItem = memo(
timerRef.current = null;
}
};
- }, [activeSort, animationKey, hasTouchScreen]);
+ }, [activeSort, animationKey, shouldUseTouchUI]);
const hasUserVoted = drop.context_profile_context?.rating !== undefined;
@@ -128,19 +128,19 @@ export const WaveLeaderboardGalleryItem = memo(
setIsVotingModalOpen(true);
};
- const transitionClasses = !hasTouchScreen
+ const transitionClasses = !shouldUseTouchUI
? "tw-transition-all tw-duration-300 tw-ease-out"
: "";
const groupClasses = artFocused ? `tw-group ${transitionClasses}` : "";
const containerClass = `${groupClasses} tw-relative tw-bg-iron-950/50 tw-border tw-border-solid tw-border-iron-800 tw-rounded-lg desktop-hover:hover:tw-border-iron-700 tw-shadow-lg desktop-hover:hover:tw-shadow-xl`;
const highlightAnimation =
- isHighlighting && !hasTouchScreen ? "tw-animate-gallery-reveal" : "";
+ isHighlighting && !shouldUseTouchUI ? "tw-animate-gallery-reveal" : "";
const baseImageClasses =
"tw-aspect-square tw-relative tw-cursor-pointer tw-touch-none tw-overflow-hidden tw-bg-iron-900 tw-group/image";
- const imageScaleClasses = hasTouchScreen
+ const imageScaleClasses = shouldUseTouchUI
? ""
: `tw-transform tw-duration-700 tw-ease-out group-hover/image:tw-scale-105 ${highlightAnimation}`;
diff --git a/components/waves/winners/drops/DefaultWaveWinnerDrop.tsx b/components/waves/winners/drops/DefaultWaveWinnerDrop.tsx
index f80d63c00b..af3bf5673d 100644
--- a/components/waves/winners/drops/DefaultWaveWinnerDrop.tsx
+++ b/components/waves/winners/drops/DefaultWaveWinnerDrop.tsx
@@ -46,12 +46,10 @@ export const DefaultWaveWinnersDrop: React.FC = ({
winner,
onDropClick,
}) => {
- // Get device info from useDeviceInfo hook
- const { hasTouchScreen } = useDeviceInfo();
+ const { shouldUseTouchUI } = useDeviceInfo();
- // Use long press interaction hook with touch screen info from device hook
const { isActive, setIsActive, touchHandlers } = useLongPressInteraction({
- hasTouchScreen,
+ hasTouchScreen: shouldUseTouchUI,
});
const shadowClass = getRankShadowClass(winner.place);
@@ -83,7 +81,7 @@ export const DefaultWaveWinnersDrop: React.FC = ({
{/* Show open icon when not a touch device */}
- {!hasTouchScreen && (
+ {!shouldUseTouchUI && (
@@ -130,7 +128,7 @@ export const DefaultWaveWinnersDrop: React.FC = ({
{/* Touch slide-up menu */}
- {hasTouchScreen &&
+ {shouldUseTouchUI &&
createPortal(
= ({
wave,
onDropClick,
}) => {
- // Get device info from useDeviceInfo hook
- const { hasTouchScreen } = useDeviceInfo();
+ const { shouldUseTouchUI } = useDeviceInfo();
- // Use long press interaction hook with touch screen info from device hook
const { isActive, setIsActive, touchHandlers } = useLongPressInteraction({
- hasTouchScreen,
+ hasTouchScreen: shouldUseTouchUI,
});
const title =
@@ -82,7 +80,7 @@ export const MemesWaveWinnersDrop: React.FC = ({
return (
onDropClick(extendedDrop)}
- className="touch-select-none tw-cursor-pointer tw-rounded-xl tw-transition-all tw-duration-300 tw-ease-out tw-w-full"
+ className={`${shouldUseTouchUI ? "touch-select-none" : ""} tw-cursor-pointer tw-rounded-xl tw-transition-all tw-duration-300 tw-ease-out tw-w-full`.trim()}
>
@@ -140,7 +138,7 @@ export const MemesWaveWinnersDrop: React.FC = ({
- {!hasTouchScreen && (
+ {!shouldUseTouchUI && (
@@ -255,7 +253,7 @@ export const MemesWaveWinnersDrop: React.FC = ({
{/* Touch slide-up menu */}
- {hasTouchScreen &&
+ {shouldUseTouchUI &&
createPortal(
0;
+ const hasAnyFinePointer = win.matchMedia?.("(any-pointer: fine)")?.matches ?? false;
+ const hasPrimaryFinePointer = win.matchMedia?.("(pointer: fine)")?.matches ?? false;
+ const hasFinePointer = hasAnyFinePointer || hasPrimaryFinePointer;
+
+ const hasAnyHover = win.matchMedia?.("(any-hover: hover)")?.matches ?? false;
+ const hasPrimaryHover = win.matchMedia?.("(hover: hover)")?.matches ?? false;
+ const hasHover = hasAnyHover || hasPrimaryHover;
+
+ let shouldUseTouchUI = false;
+ if (!(hasFinePointer || hasHover) && maxTouchPoints > 0) {
+ const ua = nav.userAgent ?? "";
+ const isMobileUA = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
+ shouldUseTouchUI = isMobileUA;
+ }
+
const ua = nav.userAgent;
const uaDataMobile = nav.userAgentData?.mobile;
const classicMobile =
@@ -56,6 +73,7 @@ export default function useDeviceInfo(): DeviceInfo {
return {
isMobileDevice,
hasTouchScreen,
+ shouldUseTouchUI,
isApp: isCapacitor,
isAppleMobile: appleMobile,
};
@@ -76,6 +94,7 @@ export default function useDeviceInfo(): DeviceInfo {
if (
prev.isMobileDevice === next.isMobileDevice &&
prev.hasTouchScreen === next.hasTouchScreen &&
+ prev.shouldUseTouchUI === next.shouldUseTouchUI &&
prev.isApp === next.isApp &&
prev.isAppleMobile === next.isAppleMobile
) {
diff --git a/hooks/useIsTouchDevice.ts b/hooks/useIsTouchDevice.ts
deleted file mode 100644
index 158d7812c4..0000000000
--- a/hooks/useIsTouchDevice.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-
-export default function useIsTouchDevice(): boolean {
- const [isTouchDevice, setIsTouchDevice] = useState(false);
-
- useEffect(() => {
- if (typeof globalThis === "undefined") {
- return;
- }
-
- const win = globalThis as typeof globalThis & {
- matchMedia?: (query: string) => MediaQueryList;
- };
-
- const nav = globalThis.navigator as Navigator | undefined;
- const maxTouchPoints = nav?.maxTouchPoints ?? 0;
-
- // Prefer "any-*" media queries so hybrid devices (touchscreen + trackpad/mouse)
- // aren't misclassified as touch-only when the primary pointer is coarse.
- const hasAnyFinePointer = win.matchMedia?.("(any-pointer: fine)")?.matches ?? false;
- const hasPrimaryFinePointer = win.matchMedia?.("(pointer: fine)")?.matches ?? false;
- const hasFinePointer = hasAnyFinePointer || hasPrimaryFinePointer;
-
- const hasAnyHover = win.matchMedia?.("(any-hover: hover)")?.matches ?? false;
- const hasPrimaryHover = win.matchMedia?.("(hover: hover)")?.matches ?? false;
- const hasHover = hasAnyHover || hasPrimaryHover;
-
- if (hasFinePointer || hasHover) {
- setIsTouchDevice(false);
- return;
- }
-
- // If there's no fine pointer and the device advertises touch points, treat it
- // as touch (important for first-touch interactions like long-press menus).
- if (maxTouchPoints > 0) {
- setIsTouchDevice(true);
- return;
- }
-
- const onTouchStart = () => {
- setIsTouchDevice(true);
- globalThis.removeEventListener("touchstart", onTouchStart);
- };
-
- globalThis.addEventListener("touchstart", onTouchStart, { passive: true });
-
- return () => {
- globalThis.removeEventListener("touchstart", onTouchStart);
- };
- }, []);
-
- return isTouchDevice;
-}