Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ class CupertinoConversationListState
),
);
}),
// targetSdk 36 means Android always draws this behind the navigation bar
// and ignores systemNavigationBarColor, and a CustomScrollView (unlike the
// ListView the Material list uses) does not consume MediaQuery padding on
// its own, so the last chat sat under the gesture pill and could not be
// scrolled clear of it. The Samsung list already gets this from a SafeArea.
SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom)),
],
)),
),
Expand Down
29 changes: 25 additions & 4 deletions lib/app/wrappers/tablet_mode_wrapper.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math';

import 'package:bluebubbles/helpers/ui/theme_helpers.dart';
Expand Down Expand Up @@ -43,6 +44,8 @@ class _TabletModeWrapperState extends OptimizedState<TabletModeWrapper> {
late final RxDouble _ratio;
double? _maxWidth;
bool? altLayoutCache;
StreamSubscription? _eventSub;
Worker? _ratioWorker;

get _width1 => max(min(_ratio * _maxWidth!, widget.maxWidthLeft ?? double.infinity), widget.minWidthLeft ?? double.negativeInfinity);

Expand All @@ -51,22 +54,40 @@ class _TabletModeWrapperState extends OptimizedState<TabletModeWrapper> {
@override
void initState() {
super.initState();
_ratio = RxDouble((ss.prefs.getDouble('splitRatio') ?? widget.initialRatio).clamp(widget.minRatio, widget.maxRatio));
eventDispatcher.stream.listen((event) {
_ratio = RxDouble(_clampRatio(ss.prefs.getDouble('splitRatio') ?? widget.initialRatio));
_eventSub = eventDispatcher.stream.listen((event) {
if (!mounted) return;
if (event.item1 == 'split-refresh') {
_ratio.value = ss.prefs.getDouble('splitRatio') ?? _ratio.value;
// splitRatio is a single global pref shared by every TabletModeWrapper, and the search
// view drives a deliberately narrow ratio through it. Re-clamp on read so a value that
// was saved by another pane, or for a different screen size, cannot land outside the
// bounds this wrapper was built with. Only the constructor clamped before.
_ratio.value = _clampRatio(ss.prefs.getDouble('splitRatio') ?? _ratio.value);
setState(() {});
} else if (event.item1 == 'override-split') {
// deliberately not clamped: goToSearch pushes this below minRatio and restores it after
_ratio.value = event.item2;
setState(() {});
}
});
debounce<double>(_ratio, (val) async {
_ratioWorker = debounce<double>(_ratio, (val) async {
await ss.prefs.setDouble('splitRatio', val);
eventDispatcher.emit('split-refresh', null);
});
}

double _clampRatio(double value) => value.clamp(widget.minRatio, widget.maxRatio);

@override
void dispose() {
// Neither of these was ever torn down. Every time this wrapper was rebuilt (skin change,
// theme switch, moving in and out of the split layout) it left another live listener and
// another debounce worker writing to the split pref.
_eventSub?.cancel();
_ratioWorker?.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
if (!showAltLayout) {
Expand Down
7 changes: 6 additions & 1 deletion lib/services/ui/chat/conversation_view_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class ConversationViewController extends StatefulController with GetSingleTicker
}

StreamSubscription<int>? shareSubscription;
StreamSubscription<bool>? keyboardSubscription;

@override
void onInit() {
Expand All @@ -158,7 +159,7 @@ class ConversationViewController extends StatefulController with GetSingleTicker
updateContactInfo();

textController.mentionables = mentionables;
KeyboardVisibilityController().onChange.listen((bool visible) async {
keyboardSubscription = KeyboardVisibilityController().onChange.listen((bool visible) async {
keyboardOpen = visible;
if (scrollController.hasClients) {
_keyboardOffset = scrollController.offset;
Expand Down Expand Up @@ -229,6 +230,10 @@ class ConversationViewController extends StatefulController with GetSingleTicker
scrollController.dispose();
headerBackFocusNode.dispose();
shareSubscription?.cancel();
// This one was never cancelled, so every chat that was opened left a listener running for the
// rest of the process, each of them reaching into a scroll controller disposed on the line
// above every time the keyboard opened or closed.
keyboardSubscription?.cancel();
super.onClose();
}

Expand Down
6 changes: 5 additions & 1 deletion lib/services/ui/navigator/navigator_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ class NavigatorService extends GetxService {
/// Returns widthChatListLeft if in tablet mode, and 0 otherwise
double widthChatListLeft(BuildContext context) => isTabletMode(context) ? _widthChatListLeft ?? 0 : 0;

/// Must agree with [ThemeHelpers.showAltLayout], which is what actually decides whether the
/// nested navigators (ids 1 and 2) get built. Without the isBubble check an Android chat bubble
/// wide enough to clear 600dp reported tablet mode while the panes were never created, so
/// push/pushLeft/backConversationView routed into navigators that are not in the tree.
bool isTabletMode(BuildContext context) => (!context.isPhone || context.width / context.height > 0.8) &&
ss.settings.tabletMode.value && context.width > 600;
ss.settings.tabletMode.value && context.width > 600 && !ls.isBubble;

/// grab the available screen width, returning the split screen width if applicable
/// this should *always* be used in place of context.width or similar
Expand Down