Skip to content

feat: configurable mobile top buttons! #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 30, 2025

Conversation

mgDentist
Copy link

@mgDentist mgDentist commented May 21, 2025

User description

Implements mobileButtons configuration in appConfig.ts and MobileTopButtons component.
Supports various button types (command, keypress, chat, pause) and integrates with
existing control system via contro.inputSchema.


PR Type

Enhancement


Description

  • Introduces configurable mobile buttons via mobileButtons in app config

  • Adds support for multiple button types (command, keypress, chat, pause, modal)

  • Implements dynamic rendering and action handling for mobile buttons

  • Updates configuration and typings to support new button system


Changes walkthrough 📝

Relevant files
Enhancement
appConfig.ts
Add types and config entry for mobile button customization

src/appConfig.ts

  • Defines MobileButtonType and MobileButtonConfig types for mobile
    buttons
  • Adds mobileButtons array to AppConfig type
  • Prepares config structure for flexible mobile button definitions
  • +12/-0   
    config.json
    Add mobileButtons configuration with multiple button types

    config.json

  • Adds mobileButtons array with various button definitions
  • Specifies button types, labels, commands, and modals for mobile
    controls
  • Expands configuration to support new mobile button system
  • +41/-1   
    MobileTopButtons.tsx
    Implement dynamic mobile button rendering and actions       

    src/react/MobileTopButtons.tsx

  • Refactors to render mobile buttons from config if present
  • Implements action handlers for command, keypress, chat, pause, and
    modal buttons
  • Adds support for toggling zoom and handling custom commands
  • Retains default buttons as fallback if config is absent
  • +143/-29

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • Summary by CodeRabbit

    • New Features
      • Added support for customizable mobile control buttons with configurable actions and long-press behaviors.
      • Introduced a debug HUD toggle accessible via the F3 key and a modal for F3 long-press actions.
      • Added a players list toggle accessible via the Tab key.
      • Managed debug overlay visibility through a global state for consistent UI updates.
    • Style
      • Added new styles for mobile buttons with pixelart icons and tab button styling.

    Copy link

    codesandbox bot commented May 21, 2025

    Review or Edit in CodeSandbox

    Open the branch in Web EditorVS CodeInsiders

    Open Preview

    Copy link

    coderabbitai bot commented May 21, 2025

    Walkthrough

    A new mobile button configuration system was introduced. The configuration file now supports defining an array of mobile button actions, and corresponding TypeScript types were added to describe these buttons. The React component for mobile top buttons was refactored to dynamically render and handle actions based on the configuration, replacing previously hardcoded buttons. Additionally, F3 key handling was enhanced with debug HUD toggling and long press modal selection, and the debug overlay was updated to use global reactive state for visibility.

    Changes

    File(s) Change Summary
    config.json Added a new mobileButtons array property specifying button configurations for mobile interface controls.
    src/appConfig.ts Added CustomAction, ActionType, ActionHoldConfig, and MobileButtonConfig types; extended AppConfig interface with optional mobileButtons array.
    src/react/MobileTopButtons.tsx Refactored component to render mobile buttons dynamically based on config; implemented pointer event handling for short and long press actions; removed hardcoded buttons and old event logic.
    src/controls.ts Added key bindings for playersList, debugOverlay, and debugOverlayHelpMenu; enhanced debug overlay toggle and long press modal logic; updated UI pause/back handling; added exported functions for mobile button action handling.
    src/globalState.ts Added showDebugHud boolean property to global UI state proxy to control debug HUD visibility.
    src/react/DebugOverlay.tsx Updated debug overlay to use global reactive state showDebugHud for visibility instead of local component state; adjusted F3 key handling accordingly.
    src/react/MobileTopButtons.module.css & .d.ts Added .tab-btn CSS class and corresponding type declarations for styling mobile buttons with pixelart icons and labels.

    Suggested labels

    Review effort 4/5

    Poem

    🐇 Hopping through code with buttons anew,
    Configured with care, each action in view.
    From tap to long press, the commands now align,
    Debug HUD shines bright, in a global design.
    Mobile controls bloom, no longer confined—
    A rabbit’s delight, so cleverly designed! 🌿✨


    📜 Recent review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between b53c8b6 and 4edaa29.

    📒 Files selected for processing (1)
    • config.json (1 hunks)
    🚧 Files skipped from review as they are similar to previous changes (1)
    • config.json
    ⏰ Context from checks skipped due to timeout of 90000ms (1)
    • GitHub Check: build-and-deploy

    Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

    ❤️ Share
    🪧 Tips

    Chat

    There are 3 ways to chat with CodeRabbit:

    • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
      • I pushed a fix in commit <commit_id>, please review it.
      • Explain this complex logic.
      • Open a follow-up GitHub issue for this discussion.
    • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
      • @coderabbitai explain this code block.
      • @coderabbitai modularize this function.
    • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
      • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
      • @coderabbitai read src/utils.ts and explain its main purpose.
      • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
      • @coderabbitai help me debug CodeRabbit configuration file.

    Support

    Need help? Create a ticket on our support page for assistance with any issues or questions.

    Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

    CodeRabbit Commands (Invoked using PR comments)

    • @coderabbitai pause to pause the reviews on a PR.
    • @coderabbitai resume to resume the paused reviews.
    • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
    • @coderabbitai full review to do a full review from scratch and review all the files again.
    • @coderabbitai summary to regenerate the summary of the PR.
    • @coderabbitai generate docstrings to generate docstrings for this PR.
    • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
    • @coderabbitai resolve resolve all the CodeRabbit review comments.
    • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
    • @coderabbitai help to get help.

    Other keywords and placeholders

    • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
    • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
    • Add @coderabbitai anywhere in the PR title to generate the title automatically.

    CodeRabbit Configuration File (.coderabbit.yaml)

    • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
    • Please see the configuration documentation for more information.
    • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

    Documentation and Community

    • Visit our Documentation for detailed information on how to use CodeRabbit.
    • Join our Discord Community to get help, request features, and share feedback.
    • Follow us on X/Twitter for updates and announcements.

    Copy link

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Error Handling

    The command execution has error handling but doesn't provide user feedback when commands fail. Consider adding user-visible feedback for failed commands.

    try {
      const keybind = contro.inputSchema?.commands?.[schema]?.[action]?.keys?.[0]
    
      if (keybind) {
        document.dispatchEvent(new KeyboardEvent('keydown', { code: keybind }))
        document.dispatchEvent(new KeyboardEvent('keyup', { code: keybind }))
      } else {
        console.warn(`Button command not found: ${button.command}`)
      }
    } catch (error) {
      console.error('Error executing command:', error)
    }
    
    Hardcoded Values

    Several hardcoded values for specific commands (jump, sneak, zoom) could be moved to a configuration or constants file for better maintainability.

    if (button.command === 'general.zoom') {
      toggleZoom()
      return
    }
    
    if (button.command === 'general.jump') {
      document.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space' }))
      setTimeout(() => {
        document.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space' }))
      }, 100)
      return
    }
    
    if (button.command === 'general.sneak') {
      document.dispatchEvent(new KeyboardEvent('keydown', { code: 'ShiftLeft' }))
      setTimeout(() => {
        document.dispatchEvent(new KeyboardEvent('keyup', { code: 'ShiftLeft' }))
      }, 100)
      return
    }
    
    Timeout Magic Numbers

    The code uses magic numbers for timeouts (100ms) in jump and sneak actions. These values should be defined as constants with meaningful names.

      setTimeout(() => {
        document.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space' }))
      }, 100)
      return
    }
    
    if (button.command === 'general.sneak') {
      document.dispatchEvent(new KeyboardEvent('keydown', { code: 'ShiftLeft' }))
      setTimeout(() => {
        document.dispatchEvent(new KeyboardEvent('keyup', { code: 'ShiftLeft' }))
      }, 100)
    

    Copy link

    qodo-merge-for-open-source bot commented May 21, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Fix command parsing error

    The current implementation doesn't handle potential errors when splitting the
    command string. If button.command is undefined or doesn't contain a period, the
    destructuring will fail. Add a check to ensure the command is properly
    formatted.

    src/react/MobileTopButtons.tsx [79-85]

     const handleButtonAction = (button: MobileButtonConfig) => {
       if (button.type === 'command' && button.command) {
    -    const [schema, action] = button.command.split('.')
    -    if (!schema || !action) {
    +    const parts = button.command.split('.')
    +    if (parts.length !== 2) {
           console.warn(`invalid command format: ${button.command}. Must be 'schema.action'`)
           return
         }
    +    const [schema, action] = parts
    • Apply / Chat
    Suggestion importance[1-10]: 7

    __

    Why: This suggestion improves robustness by explicitly checking the command format before destructuring, which could prevent runtime errors and improve maintainability, though the original code already has a basic check and warning.

    Medium
    Prevent zoom state persistence
    Suggestion Impact:The commit completely refactored the zoom functionality and mobile button handling. Instead of using the toggleZoom function with zoomActiveRef, it implemented a more comprehensive handleCommand system that properly manages button states including press and release events. This new implementation addresses the core issue by ensuring proper state cleanup.

    code diff:

    -  const zoomActiveRef = useRef(false)
     
       const showMobileControls = (bl) => {
         if (elRef.current) elRef.current.style.display = bl ? 'flex' : 'none'
    @@ -25,7 +26,7 @@
         })
       }, [])
     
    -  const onLongPress = async () => {
    +  const onF3LongPress = async () => {
         const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => {
           return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
         }).map(f3Keybind => {
    @@ -35,13 +36,6 @@
         const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
         if (f3Keybind) void f3Keybind.action()
       }
    -
    -  const defaultOptions = {
    -    shouldPreventDefault: true,
    -    delay: 500,
    -  }
    -  const longPressEvent = useLongPress(onLongPress, () => { }, defaultOptions)
    -
     
       const onChatLongPress = () => {
         document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
    @@ -55,142 +49,112 @@
         }
       }
     
    -  const chatLongPressEvent = useLongPress(
    -    onChatLongPress,
    -    onChatClick,
    -    {
    -      shouldPreventDefault: true,
    -      delay: 300,
    -    }
    -  )
    +  const handleCommand = (command: string | ActionHoldConfig, isDown: boolean) => {
    +    const commandString = typeof command === 'string' ? command : command.command
     
    -  const toggleZoom = () => {
    -    zoomActiveRef.current = !zoomActiveRef.current
    -
    -    gameAdditionalState.isZooming = zoomActiveRef.current
    -
    -    if (zoomActiveRef.current) {
    -      document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }))
    +    if (isDown) {
    +      switch (commandString) {
    +        case 'chat':
    +          onChatClick()
    +          break
    +        case 'pause':
    +          showModal({ reactType: 'pause-screen' })
    +          break
    +        default:
    +          if (commandString.startsWith('general.')) {
    +            if (commandString === 'general.inventory') {
    +              if (activeModalStack.at(-1)?.reactType?.startsWith?.('player_win:')) {
    +                hideCurrentModal()
    +              } else {
    +                document.exitPointerLock?.()
    +                contro.emit('trigger', { command: commandString } as any)
    +              }
    +            } else {
    +              contro.emit('trigger', { command: commandString } as any)
    +            }
    +          }
    +      }
         } else {
    -      document.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyC' }))
    +      switch (commandString) {
    +        case 'chat':
    +        case 'pause':
    +        case 'general.inventory':
    +        case 'general.drop':
    +          // No release action needed
    +          break
    +        default:
    +          if (commandString.startsWith('general.')) {
    +            contro.emit('release', { command: commandString } as any)
    +          }
    +      }
         }
       }

    The zoom toggle function doesn't handle component unmounting, which could lead
    to the zoom state being stuck if the component is unmounted while zooming. Add
    cleanup logic to ensure the zoom is properly released.

    src/react/MobileTopButtons.tsx [67-77]

     const toggleZoom = () => {
       zoomActiveRef.current = !zoomActiveRef.current
    -
       gameAdditionalState.isZooming = zoomActiveRef.current
     
       if (zoomActiveRef.current) {
         document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }))
       } else {
         document.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyC' }))
       }
     }
     
    +// Add in useEffect
    +useEffect(() => {
    +  return () => {
    +    if (zoomActiveRef.current) {
    +      document.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyC' }))
    +      gameAdditionalState.isZooming = false;
    +    }
    +  };
    +}, []);
    +

    [Suggestion processed]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion adds cleanup logic to prevent the zoom state from persisting if the component unmounts, which is a good practice for UI consistency and resource management, but the improvement is moderate since such unmount scenarios may be rare.

    Medium
    Prevent potential key stuck issues
    Suggestion Impact:The commit completely refactored the button handling system, including the jump and sneak actions. The new implementation in handleCommand() addresses the key stuck issues by using a more comprehensive approach with pointer capture and release events, ensuring proper cleanup of key states.

    code diff:

    +  const handleCommand = (command: string | ActionHoldConfig, isDown: boolean) => {
    +    const commandString = typeof command === 'string' ? command : command.command
     
    -  const toggleZoom = () => {
    -    zoomActiveRef.current = !zoomActiveRef.current
    -
    -    gameAdditionalState.isZooming = zoomActiveRef.current
    -
    -    if (zoomActiveRef.current) {
    -      document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }))
    +    if (isDown) {
    +      switch (commandString) {
    +        case 'chat':
    +          onChatClick()
    +          break
    +        case 'pause':
    +          showModal({ reactType: 'pause-screen' })
    +          break
    +        default:
    +          if (commandString.startsWith('general.')) {
    +            if (commandString === 'general.inventory') {
    +              if (activeModalStack.at(-1)?.reactType?.startsWith?.('player_win:')) {
    +                hideCurrentModal()
    +              } else {
    +                document.exitPointerLock?.()
    +                contro.emit('trigger', { command: commandString } as any)
    +              }
    +            } else {
    +              contro.emit('trigger', { command: commandString } as any)
    +            }
    +          }
    +      }
         } else {
    -      document.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyC' }))
    +      switch (commandString) {
    +        case 'chat':
    +        case 'pause':
    +        case 'general.inventory':
    +        case 'general.drop':
    +          // No release action needed
    +          break
    +        default:
    +          if (commandString.startsWith('general.')) {
    +            contro.emit('release', { command: commandString } as any)
    +          }
    +      }
         }
       }
     
    -  const handleButtonAction = (button: MobileButtonConfig) => {
    -    if (button.type === 'command' && button.command) {
    -      const [schema, action] = button.command.split('.')
    -      if (!schema || !action) {
    -        console.warn(`invalid command format: ${button.command}. Must be 'schema.action'`)
    -        return
    -      }
    -
    -      if (button.command === 'general.zoom') {
    -        toggleZoom()
    -        return
    -      }
    -
    -      if (button.command === 'general.jump') {
    -        document.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space' }))
    -        setTimeout(() => {
    -          document.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space' }))
    -        }, 100)
    -        return
    -      }
    -
    -      if (button.command === 'general.sneak') {
    -        document.dispatchEvent(new KeyboardEvent('keydown', { code: 'ShiftLeft' }))
    -        setTimeout(() => {
    -          document.dispatchEvent(new KeyboardEvent('keyup', { code: 'ShiftLeft' }))
    -        }, 100)
    -        return
    -      }
    -
    -      try {
    -        const keybind = contro.inputSchema?.commands?.[schema]?.[action]?.keys?.[0]
    -
    -        if (keybind) {
    -          document.dispatchEvent(new KeyboardEvent('keydown', { code: keybind }))
    -          document.dispatchEvent(new KeyboardEvent('keyup', { code: keybind }))
    -        } else {
    -          console.warn(`Button command not found: ${button.command}`)
    -        }
    -      } catch (error) {
    -        console.error('Error executing command:', error)
    -      }
    -    } else if (button.type === 'modal' && button.modal) {
    -      // Modal open
    -      showModal({ reactType: button.modal })
    -    } else if (button.type === 'keypress' && button.key) {
    -      // Keypress
    -      document.dispatchEvent(new KeyboardEvent('keydown', { code: button.key }))
    -      document.dispatchEvent(new KeyboardEvent('keyup', { code: button.key }))
    -    } else if (button.type === 'chat') {
    -      // Chat button
    -      if (activeModalStack.at(-1)?.reactType === 'chat') {
    -        hideCurrentModal()
    -      } else {
    -        showModal({ reactType: 'chat' })
    -      }
    -    } else if (button.type === 'pause') {
    -      // Pause screen
    -      showModal({ reactType: 'pause-screen' })
    -    }
    -  }
    -
    -  // Standard buttons that will be displayed if not specified in the config
    -  const defaultButtons = (
    -    <>
    -      <div
    -        className={styles['debug-btn']} onPointerDown={(e) => {
    -          window.dispatchEvent(new MouseEvent('mousedown', { button: 1 }))
    -        }}
    -      >S
    -      </div>
    -      <div
    -        className={styles['debug-btn']} onPointerDown={(e) => {
    -          document.dispatchEvent(new KeyboardEvent('keydown', { code: 'F3' }))
    -          document.dispatchEvent(new KeyboardEvent('keyup', { code: 'F3' }))
    -        }} {...longPressEvent}
    -      >F3
    -      </div>
    -      <div
    -        className={styles['chat-btn']}
    -        {...chatLongPressEvent}
    -        onPointerUp={(e) => {
    -          document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab' }))
    -        }}
    -      />
    -      <div
    -        className={styles['pause-btn']} onPointerDown={(e) => {
    -          e.stopPropagation()
    -          showModal({ reactType: 'pause-screen' })
    -        }}
    -      />
    -    </>
    -  )
    -
    -  // Displaying buttons from the configuration
       const renderConfigButtons = () => {
         return mobileButtonsConfig?.map((button, index) => {
           let className = styles['debug-btn']
           let label = button.label || button.icon || '?'
     
    -      if (button.type === 'chat') {
    +      if (button.action === 'chat') {
             className = styles['chat-btn']
             label = ''
    -      } else if (button.type === 'pause') {
    +      } else if (button.action === 'pause') {
             className = styles['pause-btn']
             label = ''
    +      }
    +
    +      const onPointerDown = (e) => {
    +        const elem = e.currentTarget as HTMLElement
    +        elem.setPointerCapture(e.pointerId)
    +
    +        if (button.actionHold) {
    +          const actionHold = button.actionHold as ExtendedActionHoldConfig
    +          if (actionHold.longPressAction) {
    +            const timerId = window.setTimeout(() => {
    +              if (actionHold.longPressAction === 'onF3LongPress') {
    +                void onF3LongPress()
    +              }
    +            }, 500)
    +            elem.dataset.longPressTimer = String(timerId)
    +            handleCommand(actionHold.command, true)
    +          } else {
    +            handleCommand(button.actionHold, true)
    +          }
    +        } else {
    +          handleCommand(button.action, true)
    +        }
    +      }
    +
    +      const onPointerUp = (e) => {
    +        const elem = e.currentTarget as HTMLElement
    +        elem.releasePointerCapture(e.pointerId)
    +
    +        // Очищаем таймер long press если он есть
    +        const timerId = elem.dataset.longPressTimer
    +        if (timerId) {
    +          clearTimeout(parseInt(timerId, 10))
    +          delete elem.dataset.longPressTimer
    +        }
    +
    +        if (button.actionHold) {
    +          const actionHold = button.actionHold as ExtendedActionHoldConfig
    +          if (actionHold.longPressAction) {
    +            handleCommand(actionHold.command, false)
    +          } else {
    +            handleCommand(button.actionHold, false)
    +          }
    +        } else {
    +          handleCommand(button.action, false)
    +        }

    The current implementation uses fixed timeouts for jump and sneak actions, which
    could lead to inconsistent behavior across devices with different performance
    characteristics. Consider implementing a more robust approach that ensures the
    keyup event is always fired, even if the component unmounts.

    src/react/MobileTopButtons.tsx [92-106]

     if (button.command === 'general.jump') {
    -  document.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space' }))
    -  setTimeout(() => {
    -    document.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space' }))
    +  const key = 'Space'
    +  document.dispatchEvent(new KeyboardEvent('keydown', { code: key }))
    +  const timeoutId = setTimeout(() => {
    +    document.dispatchEvent(new KeyboardEvent('keyup', { code: key }))
       }, 100)
    -  return
    +  return () => clearTimeout(timeoutId)
     }
     
     if (button.command === 'general.sneak') {
    -  document.dispatchEvent(new KeyboardEvent('keydown', { code: 'ShiftLeft' }))
    -  setTimeout(() => {
    -    document.dispatchEvent(new KeyboardEvent('keyup', { code: 'ShiftLeft' }))
    +  const key = 'ShiftLeft'
    +  document.dispatchEvent(new KeyboardEvent('keydown', { code: key }))
    +  const timeoutId = setTimeout(() => {
    +    document.dispatchEvent(new KeyboardEvent('keyup', { code: key }))
       }, 100)
    -  return
    +  return () => clearTimeout(timeoutId)
     }

    [Suggestion processed]

    Suggestion importance[1-10]: 6

    __

    Why: The suggestion addresses a potential issue with key events possibly being stuck by providing a cleanup function, but the current code context does not support returning a cleanup function from a non-hook event handler, so the suggestion is only partially applicable and would require further refactoring to be effective.

    Low
    • Update

    Sorry, something went wrong.

    Copy link

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 3

    🧹 Nitpick comments (4)
    src/appConfig.ts (2)

    8-17: ✅ Types look solid, but add JSDoc for easier discovery

    The new MobileButtonType union and the MobileButtonConfig interface read well and give consumers immediate type-safety.
    Adding a short JSDoc block (or inline comments) for every property—especially the optional ones—would help future contributors understand which combinations are valid (e.g. command + type: 'command', modal + type: 'modal', etc.).

    No functional blockers here.


    39-40: Consider marking mobileButtons as readonly to avoid runtime mutations

    Because the configuration is loaded once at boot time, treating it as immutable prevents accidental mutations during runtime:

    -  mobileButtons?: MobileButtonConfig[]
    +  readonly mobileButtons?: readonly MobileButtonConfig[]

    This preserves type-safety while signalling intent.

    config.json (1)

    72-78: Redundant modal field for chat / pause entries

    MobileTopButtons.tsx ignores button.modal for "chat" and "pause" types and hard-codes the target modal instead.
    If the field is meant for future use, add a comment in config.json; otherwise consider removing it to prevent confusion.

    src/react/MobileTopButtons.tsx (1)

    190-194: Use pointerup for key releases that should be held

    For actions like sneak (ShiftLeft) you currently press & release after 100 ms regardless of finger duration.
    If the intention is “hold while touching,” add a onPointerUp listener that triggers the keyup, and remove the artificial timeout.

    This yields more natural control over continuous actions.

    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro
    Cache: Disabled due to data retention organization setting
    Knowledge Base: Disabled due to data retention organization setting

    📥 Commits

    Reviewing files that changed from the base of the PR and between 5ea2ab9 and 17a0ac9.

    📒 Files selected for processing (3)
    • config.json (1 hunks)
    • src/appConfig.ts (2 hunks)
    • src/react/MobileTopButtons.tsx (3 hunks)
    🔇 Additional comments (1)
    config.json (1)

    40-45: Verify F3KeyboardEvent.code mapping

    The component dispatches:

    new KeyboardEvent('keydown', { code: 'F3' })

    While browsers accept 'F3' for code, it’s worth confirming on mobile Safari / Chrome to avoid silent no-ops.
    If problems appear, switch to 'F3'’s canonical code ('F3' is usually correct, but testing is advised).

    Comment on lines 172 to 184
    // Displaying buttons from the configuration
    const renderConfigButtons = () => {
    return mobileButtonsConfig?.map((button, index) => {
    let className = styles['debug-btn']
    let label = button.label || button.icon || '?'

    if (button.type === 'chat') {
    className = styles['chat-btn']
    label = ''
    } else if (button.type === 'pause') {
    className = styles['pause-btn']
    label = ''
    }
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion

    Long-press scoreboard lost for custom “chat” buttons

    The default chat button included chatLongPressEvent to open the player list (Tab).
    renderConfigButtons skips that handler, so users lose a useful gesture when they switch to config-driven buttons.

    Consider re-using chatLongPressEvent when button.type === 'chat':

    -        className = styles['chat-btn']
    -        label = ''
    +        className = styles['chat-btn']
    +        label = ''
    +        extraProps = chatLongPressEvent

    and spread extraProps on the <div>.

    Committable suggestion skipped: line range outside the PR's diff.

    🤖 Prompt for AI Agents
    In src/react/MobileTopButtons.tsx around lines 172 to 184, the
    renderConfigButtons function does not assign the chatLongPressEvent handler to
    custom chat buttons, causing loss of the long-press gesture functionality. To
    fix this, define an extraProps object that includes chatLongPressEvent when
    button.type is 'chat', and spread extraProps onto the button's <div> element so
    the long-press event is preserved for custom chat buttons.
    

    mgDentist and others added 4 commits May 21, 2025 21:47
    …vents

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy link

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 0

    🔭 Outside diff range comments (1)
    src/react/MobileTopButtons.tsx (1)

    40-50: 💡 Verification agent

    🧩 Analysis chain

    Potentially unused functions after refactor.

    The onChatLongPress and onChatClick functions may be unused if the component now relies entirely on the config-driven system. Consider removing them if they're no longer needed, or ensure they're properly integrated with the new system.


    🏁 Script executed:

    #!/bin/bash
    # Search for usage of onChatLongPress and onChatClick in the codebase
    echo "Searching for onChatLongPress usage:"
    rg "onChatLongPress" --type tsx --type ts
    echo ""
    echo "Searching for onChatClick usage:"
    rg "onChatClick" --type tsx --type ts

    Length of output: 304


    🏁 Script executed:

    #!/bin/bash
    # Retry searching for onChatLongPress and onChatClick without file-type filters
    echo "Searching for onChatLongPress usage:"
    rg "onChatLongPress" -n
    echo ""
    echo "Searching for onChatClick usage:"
    rg "onChatClick" -n

    Length of output: 402


    Remove unused onChatLongPress

    The onChatLongPress function is declared at src/react/MobileTopButtons.tsx:40 but never referenced elsewhere. You can safely remove it (and any related props/config) unless you plan to wire up a long‐press handler.

    The onChatClick function, however, is used at line 58 and should remain as is.

    • src/react/MobileTopButtons.tsx:40 – delete unused onChatLongPress declaration
    • src/react/MobileTopButtons.tsx:58 – (keep) onChatClick() invocation

    ♻️ Duplicate comments (1)
    src/react/MobileTopButtons.tsx (1)

    93-163: ⚠️ Potential issue

    Missing chat long-press functionality from previous implementation.

    The renderConfigButtons function doesn't integrate the existing onChatLongPress functionality for chat buttons, causing a regression from the previous hardcoded implementation where chat buttons had Tab key long-press support.

    The previous review comment about this issue wasn't fully addressed. When button.action === 'chat', the component should preserve the long-press gesture for opening the player list (Tab key).

    Consider adding long-press support for chat buttons:

          if (button.action === 'chat') {
            className = styles['chat-btn']
            label = ''
    +       // Add long press support for chat buttons
    +       const chatLongPressEvent = {
    +         onTouchStart: (e) => {
    +           const timerId = setTimeout(() => {
    +             onChatLongPress()
    +           }, 500)
    +           e.currentTarget.dataset.chatLongPressTimer = String(timerId)
    +         },
    +         onTouchEnd: (e) => {
    +           const timerId = e.currentTarget.dataset.chatLongPressTimer
    +           if (timerId) {
    +             clearTimeout(parseInt(timerId, 10))
    +             delete e.currentTarget.dataset.chatLongPressTimer
    +           }
    +         }
    +       }
          }

    Then spread these events onto the button's <div> element.

    🧹 Nitpick comments (1)
    src/react/MobileTopButtons.tsx (1)

    132-132: Replace Russian comment with English.

    The comment should be in English for consistency with the codebase.

    -        // Очищаем таймер long press если он есть
    +        // Clear long press timer if it exists
    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between 3345be2 and b5a31e3.

    📒 Files selected for processing (6)
    • config.json (1 hunks)
    • src/appConfig.ts (2 hunks)
    • src/controls.ts (4 hunks)
    • src/globalState.ts (1 hunks)
    • src/react/DebugOverlay.tsx (4 hunks)
    • src/react/MobileTopButtons.tsx (3 hunks)
    ✅ Files skipped from review due to trivial changes (1)
    • src/globalState.ts
    🚧 Files skipped from review as they are similar to previous changes (2)
    • config.json
    • src/appConfig.ts
    🧰 Additional context used
    🧬 Code Graph Analysis (2)
    src/react/DebugOverlay.tsx (1)
    src/globalState.ts (1)
    • miscUiState (113-134)
    src/controls.ts (2)
    src/react/KeybindingsScreen.tsx (1)
    • contro (27-268)
    src/globalState.ts (1)
    • miscUiState (113-134)
    🔇 Additional comments (5)
    src/react/DebugOverlay.tsx (1)

    2-5: LGTM! Clean migration to global state management.

    The transition from local component state to global reactive state is well-executed. Using useSnapshot(miscUiState) for reactive updates and miscUiState.showDebugHud for state management provides consistent debug HUD control across the application.

    Also applies to: 33-33, 60-60, 179-179

    src/controls.ts (1)

    68-68: Excellent F3 command implementation with comprehensive keybind support.

    The F3 handling logic is well-designed:

    • Properly tracks key state in hardcodedPressedKeys
    • Supports F3+key combinations for specific actions via f3Keybinds
    • Falls back to debug HUD toggle for standalone F3 press
    • Correctly handles both press and release events

    The integration with the existing keybind system is seamless.

    Also applies to: 403-418, 514-514

    src/react/MobileTopButtons.tsx (3)

    2-6: LGTM! Clean integration with global state and type system.

    The new imports properly integrate the component with the global state management (useSnapshot, miscUiState) and the new mobile button configuration types (MobileButtonConfig, ActionHoldConfig).


    10-12: Well-designed interface extension for long press actions.

    The ExtendedActionHoldConfig interface cleanly extends the base configuration to support special long press behaviors like the F3 menu.


    52-91: Robust unified command handling with proper event management.

    The handleCommand function effectively centralizes command processing for both string commands and ActionHoldConfig objects. The logic correctly handles:

    • Special cases (chat, pause, inventory)
    • General control commands with proper emit/release cycles
    • Command validation and routing

    mgDentist added 7 commits May 23, 2025 18:37

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    @zardoy
    Copy link
    Owner

    zardoy commented May 25, 2025

    /deploy

    Copy link

    Deployed to Vercel Preview: https://prismarine-4u73ykub6-zaro.vercel.app
    Playground
    Storybook

    Copy link

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 3

    ♻️ Duplicate comments (1)
    src/react/MobileTopButtons.tsx (1)

    54-57: Custom chat buttons lose long-press functionality

    The default chat button's long-press gesture to open the player list is not preserved when using configuration-driven buttons.

    Consider preserving the long-press event for chat buttons by checking the action type and applying the appropriate event handlers.

    🧹 Nitpick comments (1)
    config.json (1)

    41-82: Consider using consistent types for actionHold property

    The actionHold property is inconsistently typed across buttons - it's an empty string for most buttons but an object for the F3 button (lines 60-63). This could lead to type safety issues and runtime errors.

    Consider using null or omitting the property entirely when no hold action is needed:

         {
           "action": "general.zoom",
    -      "actionHold": "",
    +      "actionHold": null,
           "label": "Zoom",
           "icon": ""
         },

    Or ensure all buttons use the object format:

         {
           "action": "general.zoom",
    -      "actionHold": "",
    +      "actionHold": {
    +        "command": "",
    +        "longPressAction": ""
    +      },
           "label": "Zoom",
           "icon": ""
         },
    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between 058c5ec and d1d0b5c.

    📒 Files selected for processing (3)
    • config.json (1 hunks)
    • src/controls.ts (6 hunks)
    • src/react/MobileTopButtons.tsx (2 hunks)
    🔇 Additional comments (1)
    src/controls.ts (1)

    406-421:

    ✅ Verification successful

    Verify F3 key handling doesn't create race conditions

    The code adds 'F3' to hardcodedPressedKeys and then checks contro.pressedKeys for other keys, assuming 'F3' won't be in contro.pressedKeys. This assumption might not always hold true.

    Consider adding explicit filtering to ensure correct behavior:

     case 'general.debugOverlay':
       if (pressed) {
         hardcodedPressedKeys.add('F3')
    -    const pressedKey = [...contro.pressedKeys].find(key => key !== 'F3')
    +    // Explicitly filter out F3 to avoid race conditions
    +    const pressedKey = [...contro.pressedKeys].filter(key => key !== 'F3')[0]
         if (pressedKey) {
           const keybind = f3Keybinds.find((v) => v.key === pressedKey)
           if (keybind && (keybind.enabled?.() ?? true)) {
             void keybind.action()
           }
         } else {
           miscUiState.showDebugHud = !miscUiState.showDebugHud
         }
       } else {
         hardcodedPressedKeys.delete('F3')
       }
       break

    Additionally, verify that the F3 keydown event handler at line 758 properly prevents F3 from being added to contro.pressedKeys:


    🏁 Script executed:

    #!/bin/bash
    # Check if there's any preventDefault or stopPropagation for F3 key handling
    rg -A 5 -B 5 "keydown.*F3|F3.*keydown" 

    Length of output: 1548


    To inspect how handleF3 is implemented (and whether it calls preventDefault), let’s pull in its definition:


    🏁 Script executed:

    #!/bin/bash
    # Show handleF3 implementation to verify if it prevents default F3 handling
    rg -n "handleF3" -A5 -B5 src/react/DebugOverlay.tsx

    Length of output: 1351


    No changes needed for the debugOverlay handler

    The handleF3 listener in DebugOverlay.tsx already calls e.preventDefault(), and the hardcodedPressedKeys mechanism in controls.ts ensures that 'F3' is never present in contro.pressedKeys. Furthermore, using

    [...contro.pressedKeys].find(key => key !== 'F3')

    already filters out the F3 key correctly, so there’s no race condition and no additional filtering is required.

    Comment on lines 31 to 41
    const handleCommand = (command: string | ActionHoldConfig, isDown: boolean) => {
    const commandString = typeof command === 'string' ? command : command.command

    const defaultOptions = {
    shouldPreventDefault: true,
    delay: 500,
    if (!stringStartsWith(commandString, 'custom')) {
    if (isDown) {
    contro.emit('trigger', { command: commandString } as any)
    } else {
    contro.emit('release', { command: commandString } as any)
    }
    }
    }
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion

    Add validation for command existence before emitting events

    The handleCommand function emits commands without verifying they exist in the contro schema. This could lead to runtime errors if invalid commands are configured.

    Consider validating the command exists before emitting:

     const handleCommand = (command: string | ActionHoldConfig, isDown: boolean) => {
       const commandString = typeof command === 'string' ? command : command.command
    +  
    +  // Validate command exists in schema
    +  const [schema, action] = commandString.split('.')
    +  if (!contro.inputSchema?.commands?.[schema]?.[action]) {
    +    console.warn(`Invalid command: ${commandString}`)
    +    return
    +  }
    
       if (!stringStartsWith(commandString, 'custom')) {
         if (isDown) {
           contro.emit('trigger', { command: commandString } as any)
         } else {
           contro.emit('release', { command: commandString } as any)
         }
       }
     }

    Committable suggestion skipped: line range outside the PR's diff.

    🤖 Prompt for AI Agents
    In src/react/MobileTopButtons.tsx between lines 31 and 41, the handleCommand
    function emits events based on command strings without validating whether the
    command exists in the contro schema. Add a validation step to check if the
    command is valid before emitting events, and only emit if the command is
    recognized to prevent potential runtime errors.
    

    Comment on lines +938 to +947
    export const onF3LongPress = async () => {
    const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => {
    return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
    }).map(f3Keybind => {
    return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
    }))
    if (!select) return
    const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
    if (f3Keybind) void f3Keybind.action()
    }
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    ⚠️ Potential issue

    Fix keybind selection logic in onF3LongPress

    The modal displays formatted strings with key information (e.g., "Reload chunks (F3+KeyA)") but the selection logic searches for an exact match with mobileTitle, which will fail.

    Fix the selection logic:

     export const onF3LongPress = async () => {
    +  const enabledKeybinds = f3Keybinds.filter(f3Keybind => {
    +    return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
    +  })
       const select = await showOptionsModal('', enabledKeybinds.map(f3Keybind => {
         return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
       }))
       if (!select) return
    -  const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
    +  // Find by index since we modified the display string
    +  const selectedIndex = enabledKeybinds.findIndex(f3Keybind => 
    +    `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}` === select
    +  )
    +  const f3Keybind = enabledKeybinds[selectedIndex]
       if (f3Keybind) void f3Keybind.action()
     }

    Alternatively, pass the indices or original values to the modal for cleaner selection.

    📝 Committable suggestion

    ‼️ IMPORTANT
    Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    Suggested change
    export const onF3LongPress = async () => {
    const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => {
    return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
    }).map(f3Keybind => {
    return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
    }))
    if (!select) return
    const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
    if (f3Keybind) void f3Keybind.action()
    }
    export const onF3LongPress = async () => {
    // Pre-filter the enabled keybinds once
    const enabledKeybinds = f3Keybinds.filter(f3Keybind => {
    return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
    })
    // Show the modal based on the already-filtered list
    const select = await showOptionsModal(
    '',
    enabledKeybinds.map(f3Keybind => {
    return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
    })
    )
    if (!select) return
    // Find the index of the selected display string
    const selectedIndex = enabledKeybinds.findIndex(f3Keybind =>
    `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}` === select
    )
    const f3Keybind = enabledKeybinds[selectedIndex]
    if (f3Keybind) void f3Keybind.action()
    }
    🤖 Prompt for AI Agents
    In src/controls.ts around lines 938 to 947, the onF3LongPress function displays
    options with formatted strings that include key information, but the selection
    logic searches for an exact match with mobileTitle, which will not work because
    the displayed string includes additional key info. To fix this, modify the
    selection logic to match the selected string against the formatted options, or
    pass indices or original objects to the modal for accurate identification.
    Update the code to ensure the selected option correctly maps back to the
    corresponding f3Keybind object.
    

    Copy link

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 0

    ♻️ Duplicate comments (1)
    src/controls.ts (1)

    939-948: Fix keybind selection logic in onF3LongPress

    The modal displays formatted strings with key information but the selection logic searches for an exact match with mobileTitle, which will fail.

    🧹 Nitpick comments (1)
    src/controls.ts (1)

    957-978: Consider simplifying the command type handling logic

    The type narrowing in handleMobileButtonActionCommand is complex and could be clearer. The current implementation works but could benefit from better variable naming and structure.

    Consider this refactoring for better clarity:

    -export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => {
    -  const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command
    +export const handleMobileButtonActionCommand = (action: ActionType | ActionHoldConfig, isDown: boolean) => {
    +  // Extract the actual command/action from the input
    +  let commandOrAction: string | CustomAction
    +  if (typeof action === 'string') {
    +    commandOrAction = action
    +  } else if ('command' in action) {
    +    commandOrAction = action.command
    +  } else {
    +    commandOrAction = action as CustomAction
    +  }
    
    -  if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) {
    +  if (typeof commandOrAction === 'string' && !stringStartsWith(commandOrAction, 'custom')) {
         const event: CommandEventArgument<typeof contro['_commandsRaw']> = {
    -      command: commandValue as Command,
    +      command: commandOrAction as Command,
           schema: {
             keys: [],
             gamepad: []
           }
         }
         if (isDown) {
           contro.emit('trigger', event)
         } else {
           contro.emit('release', event)
         }
    -  } else if (typeof commandValue === 'object') {
    +  } else if (typeof commandOrAction === 'object') {
         if (isDown) {
    -      handleMobileButtonCustomAction(commandValue)
    +      handleMobileButtonCustomAction(commandOrAction)
         }
       }
     }
    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between d1d0b5c and 95a1a8e.

    📒 Files selected for processing (3)
    • src/appConfig.ts (2 hunks)
    • src/controls.ts (7 hunks)
    • src/react/MobileTopButtons.tsx (1 hunks)
    🚧 Files skipped from review as they are similar to previous changes (2)
    • src/appConfig.ts
    • src/react/MobileTopButtons.tsx
    ⏰ Context from checks skipped due to timeout of 90000ms (1)
    • GitHub Check: build-and-deploy
    🔇 Additional comments (4)
    src/controls.ts (4)

    31-31: LGTM!

    The import statement correctly brings in the types needed for mobile button configuration support.


    59-59: Key bindings look good!

    The new key bindings follow standard Minecraft conventions:

    • Tab for player list
    • F3 for debug overlay
    • H for select item
    • debugOverlayHelpMenu is appropriately set to null for mobile-only access

    Also applies to: 67-69


    407-427: Debug overlay implementation is well-structured!

    The F3 key handling correctly:

    • Tracks F3 state using hardcodedPressedKeys
    • Detects F3 + key combinations for keybinds
    • Toggles debug HUD when F3 is pressed alone
    • Provides mobile access via debugOverlayHelpMenu

    438-458: Good refactoring of UI command handling!

    The consolidation of ui.back and ui.pauseMenu logic eliminates duplication while maintaining the expected behavior. The switch statement improves readability.

    @zardoy
    Copy link
    Owner

    zardoy commented May 27, 2025

    /deploy

    Copy link

    Deployed to Vercel Preview: https://prismarine-1xtby9370-zaro.vercel.app
    Playground
    Storybook

    Copy link

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 1

    🧹 Nitpick comments (1)
    src/react/MobileTopButtons.tsx (1)

    30-46: Simplify the complex action extraction logic.

    The action extraction on line 31 is difficult to read and maintain. Consider extracting this into a helper function for better clarity.

    + const getActionForStyling = (button: MobileButtonConfig): string | undefined => {
    +   if (button.action) return button.action
    +   if (button.actionHold && typeof button.actionHold === 'object' && 'command' in button.actionHold) {
    +     return button.actionHold.command
    +   }
    +   return undefined
    + }
    
      const getButtonClassName = (button: MobileButtonConfig): string => {
    -   const actionForStyle = button.action || (button.actionHold && typeof button.actionHold === 'object' && 'command' in button.actionHold ? button.actionHold.command : undefined)
    +   const actionForStyle = getActionForStyling(button)
    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between 95a1a8e and b53c8b6.

    📒 Files selected for processing (2)
    • config.json (1 hunks)
    • src/react/MobileTopButtons.tsx (1 hunks)
    🚧 Files skipped from review as they are similar to previous changes (1)
    • config.json
    ⏰ Context from checks skipped due to timeout of 90000ms (1)
    • GitHub Check: build-and-deploy
    🔇 Additional comments (2)
    src/react/MobileTopButtons.tsx (2)

    1-28: LGTM! Clean setup with proper reactive state management.

    The imports are well-organized and the component setup correctly uses valtio's useSnapshot for reactive state management. The visibility control logic is straightforward and appropriate.


    156-162: LGTM! Clean conditional rendering.

    The component return logic correctly handles both configured and fallback scenarios with proper ref assignment.

    Comment on lines +58 to +154
    onChatLongPress,
    onChatClick,
    {
    shouldPreventDefault: true,
    delay: 300,
    }
    )
    if (longPressTimerIdRef.current) {
    clearTimeout(longPressTimerIdRef.current)
    longPressTimerIdRef.current = null
    }
    actionToShortPressRef.current = null

    const { actionHold, action } = button

    if (actionHold) {
    if (typeof actionHold === 'object' && 'command' in actionHold) {
    const config = actionHold
    if (config.longPressAction) {
    actionToShortPressRef.current = config.command
    longPressTimerIdRef.current = window.setTimeout(() => {
    handleMobileButtonLongPress(config)
    actionToShortPressRef.current = null
    longPressTimerIdRef.current = null
    }, config.duration || 500)
    } else {
    handleMobileButtonActionCommand(config.command, true)
    }
    } else if (action) {
    actionToShortPressRef.current = action
    longPressTimerIdRef.current = window.setTimeout(() => {
    handleMobileButtonActionCommand(actionHold, true)
    actionToShortPressRef.current = null
    longPressTimerIdRef.current = null
    }, 500)
    } else {
    handleMobileButtonActionCommand(actionHold, true)
    }
    } else if (action) {
    handleMobileButtonActionCommand(action, true)
    }
    }

    const onPointerUp = (e: React.PointerEvent) => {
    const elem = e.currentTarget as HTMLElement
    elem.releasePointerCapture(e.pointerId)

    const { actionHold, action } = button
    let wasShortPressHandled = false

    if (longPressTimerIdRef.current) {
    clearTimeout(longPressTimerIdRef.current)
    longPressTimerIdRef.current = null
    if (actionToShortPressRef.current) {
    handleMobileButtonActionCommand(actionToShortPressRef.current, true)
    handleMobileButtonActionCommand(actionToShortPressRef.current, false)
    wasShortPressHandled = true
    }
    }

    if (!wasShortPressHandled) {
    if (actionHold) {
    if (typeof actionHold === 'object' && 'command' in actionHold) {
    const config = actionHold
    if (config.longPressAction) {
    if (actionToShortPressRef.current === null) {
    if (typeof config.longPressAction === 'string') {
    handleMobileButtonActionCommand(config.longPressAction, false)
    }
    }
    } else {
    handleMobileButtonActionCommand(config.command, false)
    }
    } else if (action) {
    if (actionToShortPressRef.current === null) {
    handleMobileButtonActionCommand(actionHold, false)
    }
    } else {
    handleMobileButtonActionCommand(actionHold, false)
    }
    } else if (action) {
    handleMobileButtonActionCommand(action, false)
    }
    }
    actionToShortPressRef.current = null
    }

    return (
    <div
    key={index}
    className={className}
    onPointerDown={onPointerDown}
    onPointerUp={onPointerUp}
    onLostPointerCapture={onPointerUp}
    >
    {label}
    </div>
    )
    })
    }
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion

    Refactor complex pointer event logic for better maintainability.

    The pointer event handling logic is quite complex and difficult to follow. Consider breaking it down into smaller, focused functions for better readability and maintainability.

    + const handlePointerDown = (button: MobileButtonConfig, e: React.PointerEvent) => {
    +   const elem = e.currentTarget as HTMLElement
    +   elem.setPointerCapture(e.pointerId)
    +   
    +   // Clear any existing timer
    +   if (longPressTimerIdRef.current) {
    +     clearTimeout(longPressTimerIdRef.current)
    +     longPressTimerIdRef.current = null
    +   }
    +   actionToShortPressRef.current = null
    +   
    +   // Handle the specific button action
    +   // ... extract the logic here
    + }
    + 
    + const handlePointerUp = (button: MobileButtonConfig, e: React.PointerEvent) => {
    +   // ... extract the logic here
    + }
    
      const renderConfigButtons = () => {
        return mobileButtonsConfig?.map((button, index) => {
          // ...
    -     const onPointerDown = (e: React.PointerEvent) => {
    -       // ... complex logic
    -     }
    -     const onPointerUp = (e: React.PointerEvent) => {
    -       // ... complex logic  
    -     }
    +     const onPointerDown = (e: React.PointerEvent) => handlePointerDown(button, e)
    +     const onPointerUp = (e: React.PointerEvent) => handlePointerUp(button, e)

    Also consider adding cleanup on component unmount:

    + useEffect(() => {
    +   return () => {
    +     if (longPressTimerIdRef.current) {
    +       clearTimeout(longPressTimerIdRef.current)
    +     }
    +   }
    + }, [])

    Committable suggestion skipped: line range outside the PR's diff.

    🤖 Prompt for AI Agents
    In src/react/MobileTopButtons.tsx around lines 58 to 154, the onPointerDown and
    onPointerUp handlers contain complex nested logic that reduces readability and
    maintainability. Refactor by extracting smaller helper functions to handle
    distinct parts of the logic, such as processing long press setup, executing
    short press actions, and handling long press completions. Additionally, add a
    cleanup effect using useEffect to clear any active longPressTimerIdRef timeout
    when the component unmounts to prevent potential memory leaks or unexpected
    behavior.
    

    @zardoy
    Copy link
    Owner

    zardoy commented May 30, 2025

    /deploy

    Copy link

    Deployed to Vercel Preview: https://prismarine-5zjalt3kz-zaro.vercel.app
    Playground
    Storybook

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    @zardoy
    Copy link
    Owner

    zardoy commented May 30, 2025

    /deploy

    @zardoy zardoy changed the title feat: configurable mobile buttons with command support feat: configurable mobile top buttons! May 30, 2025
    @zardoy zardoy merged commit 087e167 into zardoy:next May 30, 2025
    3 checks passed
    Copy link

    Deployed to Vercel Preview: https://prismarine-abh4qbr4e-zaro.vercel.app
    Playground
    Storybook

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    None yet

    2 participants