diff --git a/README.md b/README.md index c2bde80..c2158af 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ when you're ready", "Waiting for Bob and Mika · 2/3 ready", "Bob is loading…") or what went wrong, with Retry or Cancel when it helps. Chat and activity open in a panel on the right (chat button or Tab), ready to type; it shows messages, and its "Activity" toggle adds joins, ready, seeks and the like between them. Joins, leaves, chat and -other news also pop up bottom left with the person's avatar. Unread chat +other news also pop up bottom left with the person's avatar; click a message to open the chat. Unread chat shows as a count on the chat buttons and, in fullscreen with the controls hidden, as a small chip top right. diff --git a/apps/desktop/lib/src/schedule/ui/schedule_page.dart b/apps/desktop/lib/src/schedule/ui/schedule_page.dart index 5351441..b7995b8 100644 --- a/apps/desktop/lib/src/schedule/ui/schedule_page.dart +++ b/apps/desktop/lib/src/schedule/ui/schedule_page.dart @@ -501,7 +501,6 @@ class _SchedulePageState extends State { (byDay[DateTime(l.year, l.month, l.day)] ??= []).add(e); } return HScroll( - wheel: false, controller: _scroll, builder: (c) => ListView.separated( key: const ValueKey('schedule-board'), diff --git a/apps/desktop/lib/src/shell/app_shell.dart b/apps/desktop/lib/src/shell/app_shell.dart index fbd7059..a8144c1 100644 --- a/apps/desktop/lib/src/shell/app_shell.dart +++ b/apps/desktop/lib/src/shell/app_shell.dart @@ -669,7 +669,11 @@ class AppShellState extends State with SingleTickerProviderStateMixin ), ), if (online && _shown != PlayerMode.none) _playerLayer(area), - Positioned(left: 16, bottom: _fullscreen ? 90 : 16, child: const PartyToasts()), + Positioned(left: 16, bottom: _fullscreen ? 90 : 16, child: PartyToasts( + onOpenChat: () { + if (!_chatOpen) _toggleChat(); + }, + )), ], ); }); diff --git a/apps/desktop/lib/src/shell/motion.dart b/apps/desktop/lib/src/shell/motion.dart index 1531708..e1a8cc4 100644 --- a/apps/desktop/lib/src/shell/motion.dart +++ b/apps/desktop/lib/src/shell/motion.dart @@ -90,17 +90,17 @@ Future showPopover( }, ); -/// Horizontal lists for mouse users: drag with the mouse, and the plain -/// (vertical) wheel moves them sideways. At either end the wheel is left -/// to the page, so a row never traps vertical scrolling. +/// Horizontal lists for mouse users: drag with the mouse or Shift+wheel; +/// the plain wheel stays vertical (the page scrolls under the cursor, not +/// the row). class HScroll extends StatefulWidget { final ScrollController? controller; final Widget Function(ScrollController controller) builder; - /// Map the plain (vertical) wheel to sideways scrolling. Off where the - /// children scroll vertically themselves (the schedule's day columns): - /// there only mouse drag and Shift+wheel move sideways. + /// Also map the plain (vertical) wheel to sideways scrolling (at either + /// end the wheel goes to the page). Off by default: one wheel doing two + /// things was confusing. final bool wheel; - const HScroll({super.key, this.controller, required this.builder, this.wheel = true}); + const HScroll({super.key, this.controller, required this.builder, this.wheel = false}); @override State createState() => _HScrollState(); diff --git a/apps/desktop/lib/src/shell/party_strip.dart b/apps/desktop/lib/src/shell/party_strip.dart index 186d67c..6ab26b9 100644 --- a/apps/desktop/lib/src/shell/party_strip.dart +++ b/apps/desktop/lib/src/shell/party_strip.dart @@ -92,6 +92,7 @@ class PartyStrip extends StatelessWidget { message: 'Your profile', child: InkWell( key: const ValueKey('strip-profile'), + canRequestFocus: false, customBorder: const CircleBorder(), onTap: onProfile, child: StatusAvatar( diff --git a/apps/desktop/lib/src/shell/toasts.dart b/apps/desktop/lib/src/shell/toasts.dart index 94f341c..1ec780d 100644 --- a/apps/desktop/lib/src/shell/toasts.dart +++ b/apps/desktop/lib/src/shell/toasts.dart @@ -43,10 +43,13 @@ ShellToast shellToastFor(ToastItem t, AvatarInfo Function(String name) avatar) { } /// The coordinator's toasts as cards with avatars, bottom-left: each -/// slides in, stays [life], then slides out and collapses its gap. +/// slides in, stays [life], then slides out and collapses its gap. A click +/// on a chat toast opens the chat ([onOpenChat]); the rest let clicks +/// through. class PartyToasts extends StatefulWidget { final Duration life; - const PartyToasts({super.key, this.life = const Duration(seconds: 5)}); + final VoidCallback? onOpenChat; + const PartyToasts({super.key, this.life = const Duration(seconds: 5), this.onOpenChat}); @override State createState() => _PartyToastsState(); @@ -101,13 +104,37 @@ class _PartyToastsState extends State { super.dispose(); } + void _open(ShellToast t) { + widget.onOpenChat?.call(); + setState(() => t.leaving = true); + } + @override - Widget build(BuildContext context) => IgnorePointer(child: ToastColumn(toasts: _shown.reversed.take(6).toList().reversed.toList())); + Widget build(BuildContext context) => ToastColumn( + toasts: _shown.reversed.take(6).toList().reversed.toList(), + onTap: widget.onOpenChat == null ? null : _open, + ); } class ToastColumn extends StatelessWidget { final List toasts; - const ToastColumn({super.key, required this.toasts}); + /// Clicks on chat toasts; null (or any other toast): clicks pass through. + final ValueChanged? onTap; + const ToastColumn({super.key, required this.toasts, this.onTap}); + + Widget _card(ShellToast t) { + final card = ToastCard(toast: t); + final tap = onTap; + if (!t.chat || tap == null || t.leaving) return IgnorePointer(child: card); + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + key: ValueKey('toast-open-chat-${t.id}'), + onTap: () => tap(t), + child: card, + ), + ); + } @override Widget build(BuildContext context) { @@ -139,7 +166,7 @@ class ToastColumn extends StatelessWidget { duration: easeTime, curve: ease, opacity: t.leaving ? 0 : 1, - child: Padding(padding: const EdgeInsets.only(top: 8), child: ToastCard(toast: t)), + child: Padding(padding: const EdgeInsets.only(top: 8), child: _card(t)), ), ), ), diff --git a/apps/desktop/lib/src/widgets/app_keys.dart b/apps/desktop/lib/src/widgets/app_keys.dart index 339f7d1..0ede7fd 100644 --- a/apps/desktop/lib/src/widgets/app_keys.dart +++ b/apps/desktop/lib/src/widgets/app_keys.dart @@ -4,13 +4,15 @@ import 'package:flutter/services.dart'; import '../state/session_state.dart'; import 'text_focus.dart'; -/// Tab and Shift+Tab never move focus (the app's [Shortcuts] override): -/// a button that got focus that way would be pressed by Space, the ready -/// key. +/// Tab and Shift+Tab never move focus, and Space never presses the +/// focused button (the app's [Shortcuts] override): Space is the ready key, +/// and a button keeps focus after a click. Text fields still type spaces +/// (their own shortcuts come first). Map get appShortcuts => { ...WidgetsApp.defaultShortcuts, const SingleActivator(LogicalKeyboardKey.tab): const DoNothingIntent(), const SingleActivator(LogicalKeyboardKey.tab, shift: true): const DoNothingIntent(), + const SingleActivator(LogicalKeyboardKey.space): const DoNothingIntent(), }; /// The window's two app-wide keys, ahead of whatever has focus: while in a diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1d8c5d4..dea21e5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -525,8 +525,9 @@ One window, `AppShell` (`lib/src/shell/`), whatever the connection state through for views that replace each other (the old one gone after 35%, the new one fading and scaling in from 0.985), `showPopover` (cards that fade and scale in from where they open; they scroll when the window is short) -and `HScroll` (horizontal rows: mouse drag, the vertical wheel scrolls them -sideways until an end, then the page). +and `HScroll` (horizontal rows: mouse drag or Shift+wheel; the plain wheel +always scrolls the page). Space never presses a focused button +(`appShortcuts`): it is the ready key. - **Party strip** (`party_strip.dart`, 60 px): your avatar (profile card; 42 px like the party's), the Library / Schedule / Settings tabs with a growing underline (an @@ -687,7 +688,8 @@ sideways until an end, then the page). **Toasts** (`toasts.dart`): the coordinator's toasts bottom left, with the avatar of their `ToastItem.actor` (feed and chat toasts; the app's own have none); each - slides in, stays 5 s, then slides out and collapses its gap. + slides in, stays 5 s, then slides out and collapses its gap. A click on + a chat toast opens the chat; the others let clicks through. Settings persist in two Hive boxes: `connect_form` (last form, recent servers and links, the profile picture and colour; saved even when a join