Fix: Mobile swipe-back from workspace chat list to workspace list #2

Open
opened 2026-07-14 06:55:08 +00:00 by swallow · 0 comments
Owner

Problem

On mobile, the task detail page supports swiping right from the left edge to return to the task list. However, the workspace detail page (SpacesPage / SpaceDetail) does not have this swipe-back gesture on the chat list pane.

Users expect consistent mobile navigation across the app — swiping right from the left edge should always return them to the previous level (workspace list → task detail → back to workspace list).

Root Cause

SpaceDetail.tsx's SpaceChat component has a chat list pane on the left and a conversation detail pane on the right (via SwipeableTabs for mobile).

  • The conversation detail pane already has swipe-back via SwipeableTabs' onSwipeBackFromFirstonSelectSpaceTask(0) (returns to chat list).
  • The chat list pane has no edge swipe handler — it does not call onSelectSpace(undefined) on right-swipe.

App.tsx has a top-level useEdgeSwipe hook for opening the navigation drawer, but it doesn't cover the chat list inside SpaceDetail.

Proposed Fix

In ui/src/components/spaces/SpaceDetail.tsx:

  1. Pass onSelectSpace to SpaceChat so the chat list pane knows when to return to the workspace list.
  2. Add useEdgeSwipe (existing hook) to the chat list <div> with onOpen: () => onSelectSpace?.(undefined).
  3. Enable conditionally: only when onSelectSpace is provided, spaceTaskId == null (chat list view, not conversation), and !showCreate (no create dialog open).

Diff

diff --git a/ui/src/components/spaces/SpaceDetail.tsx b/ui/src/components/spaces/SpaceDetail.tsx
index 62d9fef..921c526 100644
--- a/ui/src/components/spaces/SpaceDetail.tsx
+++ b/ui/src/components/spaces/SpaceDetail.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState, type TouchEvent } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useQuery, useQueryClient } from '@tanstack/react-query';
 import { useSpaces, useArchiveSpace } from '../../hooks/useSpaces';
@@ -30,6 +30,7 @@ import { SpaceSettings } from './SpaceSettings';
 import { SpaceCalendar } from './SpaceCalendar';
 import { SchedulesPage } from '../../pages/SpacesPage';
 import { SpaceApps } from './SpaceApps';
+import { useEdgeSwipe } from '../../hooks/useEdgeSwipe';
 import { useAuthState } from '../../App';
 import { SkeletonChatPane } from '../shared/Skeleton';
 import { EmptyState } from '../shared/EmptyState';
@@ -189,6 +190,7 @@ export function SpaceDetail({ spaceId, spaceTaskId, initialTab, onInitialTabAppl
             spaceId={spaceId}
             isPersonalSpace={space.kind === 'personal'}
             spaceTaskId={spaceTaskId}
+            onSelectSpace={onSelectSpace}
             onSelectSpaceTask={onSelectSpaceTask}
             onCreateTask={onCreateTask}
             filter={chatFilter}
@@ -426,6 +428,7 @@ function SpaceChat({
   spaceId,
   isPersonalSpace,
   spaceTaskId,
+  onSelectSpace,
   onSelectSpaceTask,
   filter,
   onFilterChange,
@@ -433,6 +436,7 @@ function SpaceChat({
   spaceId: string;
   isPersonalSpace: boolean;
   spaceTaskId?: number;
+  onSelectSpace?: (id: string | undefined) => void;
   onSelectSpaceTask: (id: number) => void;
   onCreateTask: SpaceDetailProps['onCreateTask'];
   filter: SpaceChatFilter;
@@ -501,10 +505,19 @@ function SpaceChat({
     onSelectSpaceTask(created.task.id);
   }, [qc, onSelectSpaceTask]);
 
+  // 右スワイプでチャット一覧(ワークスペース一覧)に戻る(モバイル操作感)。
+  // チャット一覧画面のみで有効(会話はSwipeableTabsが処理)。
+  const edgeSwipe = useEdgeSwipe({
+    enabled: !!onSelectSpace && spaceTaskId == null && !showCreate,
+    onOpen: () => { onSelectSpace?.(undefined); },
+  });
+
   return (
     <div className="flex h-full min-h-0">
-      {/* 左: チャット一覧。会話を開いている狭幅では隠す(会話が一覧を置き換える)。 */}
+      {/* 左: チャット一覧。右スワイプでワークスペース一覧へ戻る。
+          会話を開いている狭幅では隠す(会話が一覧を置き換える)。 */}
       <div
+        {...edgeSwipe}
         className={`w-full md:w-[280px] md:shrink-0 flex flex-col min-h-0 overflow-hidden border-r border-hairline ${
           spaceTaskId != null || showCreate ? 'hidden md:flex' : 'flex'
         }`}

Notes

  • The useEdgeSwipe hook is already tested and used in App.tsx for the nav drawer. Reusing it for consistency.
  • Edge width: 20px, open threshold: 60px / 22% width, vertical tolerance: 40px (from useEdgeSwipe defaults).
  • No new dependencies or components needed.
## Problem On mobile, the task detail page supports swiping right from the left edge to return to the task list. However, the workspace detail page (`SpacesPage` / `SpaceDetail`) does not have this swipe-back gesture on the chat list pane. Users expect consistent mobile navigation across the app — swiping right from the left edge should always return them to the previous level (workspace list → task detail → back to workspace list). ## Root Cause `SpaceDetail.tsx`'s `SpaceChat` component has a chat list pane on the left and a conversation detail pane on the right (via `SwipeableTabs` for mobile). - The **conversation detail pane** already has swipe-back via `SwipeableTabs`' `onSwipeBackFromFirst` → `onSelectSpaceTask(0)` (returns to chat list). - The **chat list pane** has no edge swipe handler — it does not call `onSelectSpace(undefined)` on right-swipe. `App.tsx` has a top-level `useEdgeSwipe` hook for opening the navigation drawer, but it doesn't cover the chat list inside `SpaceDetail`. ## Proposed Fix In `ui/src/components/spaces/SpaceDetail.tsx`: 1. **Pass `onSelectSpace` to `SpaceChat`** so the chat list pane knows when to return to the workspace list. 2. **Add `useEdgeSwipe`** (existing hook) to the chat list `<div>` with `onOpen: () => onSelectSpace?.(undefined)`. 3. **Enable conditionally**: only when `onSelectSpace` is provided, `spaceTaskId == null` (chat list view, not conversation), and `!showCreate` (no create dialog open). ## Diff ```diff diff --git a/ui/src/components/spaces/SpaceDetail.tsx b/ui/src/components/spaces/SpaceDetail.tsx index 62d9fef..921c526 100644 --- a/ui/src/components/spaces/SpaceDetail.tsx +++ b/ui/src/components/spaces/SpaceDetail.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState, type TouchEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useSpaces, useArchiveSpace } from '../../hooks/useSpaces'; @@ -30,6 +30,7 @@ import { SpaceSettings } from './SpaceSettings'; import { SpaceCalendar } from './SpaceCalendar'; import { SchedulesPage } from '../../pages/SpacesPage'; import { SpaceApps } from './SpaceApps'; +import { useEdgeSwipe } from '../../hooks/useEdgeSwipe'; import { useAuthState } from '../../App'; import { SkeletonChatPane } from '../shared/Skeleton'; import { EmptyState } from '../shared/EmptyState'; @@ -189,6 +190,7 @@ export function SpaceDetail({ spaceId, spaceTaskId, initialTab, onInitialTabAppl spaceId={spaceId} isPersonalSpace={space.kind === 'personal'} spaceTaskId={spaceTaskId} + onSelectSpace={onSelectSpace} onSelectSpaceTask={onSelectSpaceTask} onCreateTask={onCreateTask} filter={chatFilter} @@ -426,6 +428,7 @@ function SpaceChat({ spaceId, isPersonalSpace, spaceTaskId, + onSelectSpace, onSelectSpaceTask, filter, onFilterChange, @@ -433,6 +436,7 @@ function SpaceChat({ spaceId: string; isPersonalSpace: boolean; spaceTaskId?: number; + onSelectSpace?: (id: string | undefined) => void; onSelectSpaceTask: (id: number) => void; onCreateTask: SpaceDetailProps['onCreateTask']; filter: SpaceChatFilter; @@ -501,10 +505,19 @@ function SpaceChat({ onSelectSpaceTask(created.task.id); }, [qc, onSelectSpaceTask]); + // 右スワイプでチャット一覧(ワークスペース一覧)に戻る(モバイル操作感)。 + // チャット一覧画面のみで有効(会話はSwipeableTabsが処理)。 + const edgeSwipe = useEdgeSwipe({ + enabled: !!onSelectSpace && spaceTaskId == null && !showCreate, + onOpen: () => { onSelectSpace?.(undefined); }, + }); + return ( <div className="flex h-full min-h-0"> - {/* 左: チャット一覧。会話を開いている狭幅では隠す(会話が一覧を置き換える)。 */} + {/* 左: チャット一覧。右スワイプでワークスペース一覧へ戻る。 + 会話を開いている狭幅では隠す(会話が一覧を置き換える)。 */} <div + {...edgeSwipe} className={`w-full md:w-[280px] md:shrink-0 flex flex-col min-h-0 overflow-hidden border-r border-hairline ${ spaceTaskId != null || showCreate ? 'hidden md:flex' : 'flex' }`} ``` ## Notes - The `useEdgeSwipe` hook is already tested and used in `App.tsx` for the nav drawer. Reusing it for consistency. - Edge width: 20px, open threshold: 60px / 22% width, vertical tolerance: 40px (from `useEdgeSwipe` defaults). - No new dependencies or components needed.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: swallow/maestro#2
No description provided.