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
52 changes: 41 additions & 11 deletions lib/model/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ class UpdateMachine {
InitialSnapshot? result;
try {
result = await registerQueue(connection);
} catch (e, s) {
} catch (e, stackTrace) {
stopAndThrowIfNoAccount();
// TODO(#890): tell user if initial-fetch errors persist, or look non-transient
final ZulipVersionData? zulipVersionData;
Expand All @@ -1192,7 +1192,20 @@ class UpdateMachine {
assert(debugLog('Error fetching initial snapshot: $e'));
// Print stack trace in its own log entry; log entries are truncated
// at 1 kiB (at least on Android), and stack can be longer than that.
assert(debugLog('Stack:\n$s'));
assert(debugLog('Stack:\n$stackTrace'));
if (e case NetworkException(cause: SocketException())) {
// A [SocketException] is common when the device is asleep.
} else {
// TODO: When the error seems transient, do keep retrying but
// don't spam this feedback.
// TODO(#1948) Break the retry loop on non-transient errors.
_reportConnectionErrorToUserAndPromiseRetry(e,
realmUrl: connection.realmUrl,
// The stack trace is mostly useful for
// `MalformedServerResponseException`s, and will be noise for
// routine exceptions like from network problems.
stackTrace: e is ApiRequestException ? null : stackTrace);
}
}
assert(debugLog('Backing off, then will retry…'));
await (backoffMachine ??= BackoffMachine()).wait();
Expand Down Expand Up @@ -1339,9 +1352,9 @@ class UpdateMachine {
lastEventId = events.last.id;
}
}
} catch (e) {
} catch (e, stackTrace) {
if (_disposed) return;
await _handlePollError(e);
await _handlePollError(e, stackTrace);
assert(_disposed);
return;
}
Expand Down Expand Up @@ -1458,7 +1471,7 @@ class UpdateMachine {
/// See also:
/// * [_handlePollRequestError], which handles certain errors
/// and causes them not to reach this method.
Future<void> _handlePollError(Object error) async {
Future<void> _handlePollError(Object error, StackTrace stackTrace) async {
// An error occurred, other than the transient request errors we retry on.
// This means either a lost/expired event queue on the server (which is
// normal after the app is offline for a period like 10 minutes),
Expand Down Expand Up @@ -1493,9 +1506,14 @@ class UpdateMachine {
isUnexpected = true;

default:
assert(debugLog('BUG: Unexpected error in event polling: $error\n' // TODO(log)
'Replacing event queue…'));
_reportToUserErrorConnectingToServer(error);
assert(debugLog('BUG: Unexpected error in event polling: $error')); // TODO(log)
// Print stack trace in its own log entry; log entries are truncated
// at 1 kiB (at least on Android), and stack can be longer than that.
assert(debugLog('Stack trace:\n$stackTrace'));
assert(debugLog('Replacing event queue…'));
_reportConnectionErrorToUserAndPromiseRetry(error,
realmUrl: store.realmUrl,
stackTrace: stackTrace);
// Similar story to the _EventHandlingException case;
// separate only so that that other case can print more context.
// The bug here could be in the server if it's an ApiRequestException,
Expand Down Expand Up @@ -1526,16 +1544,28 @@ class UpdateMachine {
void _maybeReportToUserTransientError(Object error) {
_accumulatedTransientFailureCount++;
if (_accumulatedTransientFailureCount > transientFailureCountNotifyThreshold) {
_reportToUserErrorConnectingToServer(error);
_reportConnectionErrorToUserAndPromiseRetry(error, realmUrl: store.realmUrl);
}
}

void _reportToUserErrorConnectingToServer(Object error) {
/// Give brief UI feedback that we failed to connect to the server
/// and that we'll try again.
static void _reportConnectionErrorToUserAndPromiseRetry(
Object error, {
StackTrace? stackTrace,
required Uri realmUrl,
}) {
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;

final details = StringBuffer()..write(error.toString());
if (stackTrace != null) {
details.write('\nStack:\n$stackTrace');
}

reportErrorToUserBriefly(
zulipLocalizations.errorConnectingToServerShort,
details: zulipLocalizations.errorConnectingToServerDetails(
store.realmUrl.toString(), error.toString()));
realmUrl.toString(), details.toString()));
}

/// Cleans up resources and tells the instance not to make new API requests.
Expand Down
9 changes: 8 additions & 1 deletion lib/widgets/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ Widget? _adaptiveContent(Widget? content) {
case TargetPlatform.macOS:
// A [SingleChildScrollView] (wrapping both title and content) is already
// created by [CupertinoAlertDialog].
return content;
return DefaultTextStyle.merge(
// The "alert description" is start-aligned in one example in Apple's
// HIG document:
// https://developer.apple.com/design/human-interface-guidelines/alerts#Anatomy
// (Confusingly, in 2025-10, it's center-aligned in the graphic at the
// *top* of that page; shrug.)
textAlign: TextAlign.start,
child: content);
}
}

Expand Down