enhance reconnect message handling and improve countdown display in chat

This commit is contained in:
Tommy Chow 2026-01-06 18:22:21 -05:00
parent 7a3f78bc7a
commit d296a20aef
2 changed files with 95 additions and 35 deletions

View file

@ -132,6 +132,12 @@ abstract class ChatStoreBase with Store {
// The current time to wait between retries for exponential backoff.
var _backoffTime = 0;
// Reference to the reconnect message for in-place updates.
IRCMessage? _reconnectMessage;
// Timestamp when reconnection started, for calculating elapsed time.
DateTime? _reconnectStartTime;
/// The scroll controller that controls auto-scroll and resume-scroll behavior.
final scrollController = ScrollController();
@ -612,6 +618,22 @@ abstract class ChatStoreBase with Store {
}
}
// Transform reconnect message to summary on successful connection
if (_reconnectMessage != null && _reconnectStartTime != null) {
final elapsed =
DateTime.now().difference(_reconnectStartTime!).inSeconds;
final attempts = _retries;
final index = _messages.indexOf(_reconnectMessage!);
if (index != -1) {
_messages[index] = IRCMessage.createNotice(
message:
'Reconnected ($attempts ${attempts == 1 ? 'attempt' : 'attempts'}, ${elapsed}s)',
);
}
}
_reconnectMessage = null;
_reconnectStartTime = null;
// Reset exponential backoff if successfully connected.
_retries = 0;
_backoffTime = 0;
@ -827,6 +849,14 @@ abstract class ChatStoreBase with Store {
}
if (_retries >= _maxRetries) {
// Remove the reconnect message before showing final disconnect notice
if (_reconnectMessage != null) {
final index = _messages.indexOf(_reconnectMessage!);
if (index != -1) _messages.removeAt(index);
_reconnectMessage = null;
}
_reconnectStartTime = null;
// Add directly to messages, not buffer, so it shows immediately
_messages.add(
IRCMessage.createNotice(
@ -842,53 +872,78 @@ abstract class ChatStoreBase with Store {
return;
}
// Increment the retry count.
_retries++;
// Start exponential backoff only after first failed attempt (capped at 8s).
// First retry is immediate to catch brief network hiccups.
if (_retries > 1) {
final newBackoff = _backoffTime == 0 ? 1 : _backoffTime * 2;
_backoffTime = newBackoff > 8 ? 8 : newBackoff;
}
// Helper to update the single reconnect message in place (or add if first time)
void updateReconnectMessage(String text) {
final msg = IRCMessage.createNotice(message: text);
if (_reconnectMessage == null) {
_reconnectStartTime ??= DateTime.now(); // Record when reconnection started
_reconnectMessage = msg;
_messages.add(_reconnectMessage!);
} else {
final index = _messages.indexOf(_reconnectMessage!);
if (index != -1) {
_reconnectMessage = msg;
_messages[index] = _reconnectMessage!;
} else {
// Reference was lost (e.g., message limit cleanup) - re-add
_reconnectMessage = msg;
_messages.add(_reconnectMessage!);
}
}
}
// Countdown phase (if backoff time > 0)
if (_backoffTime > 0) {
// Show countdown message for backoff time
var remainingSeconds = _backoffTime;
_messages.add(
IRCMessage.createNotice(
message:
'Connection lost. Reconnecting in ${remainingSeconds}s...',
),
updateReconnectMessage(
'Reconnecting in ${remainingSeconds}s... (attempt $_retries of $_maxRetries)',
);
// Update countdown every second
await Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
remainingSeconds--;
// Find and replace the countdown message
final index = _messages.indexWhere(
(msg) =>
msg.message?.contains('Connection lost. Reconnecting in') ??
false,
);
if (index != -1 && remainingSeconds > 0) {
_messages[index] = IRCMessage.createNotice(
message:
'Connection lost. Reconnecting in ${remainingSeconds}s...',
);
return true; // Continue loop
} else {
// Remove countdown message when done
if (index != -1) _messages.removeAt(index);
// Abort countdown if dispose was called
if (_shouldDisconnect) {
if (_reconnectMessage != null) {
final index = _messages.indexOf(_reconnectMessage!);
if (index != -1) _messages.removeAt(index);
_reconnectMessage = null;
}
_reconnectStartTime = null;
return false; // Exit loop
}
remainingSeconds--;
if (remainingSeconds > 0) {
updateReconnectMessage(
'Reconnecting in ${remainingSeconds}s... (attempt $_retries of $_maxRetries)',
);
return true; // Continue loop
}
return false; // Exit loop
});
// Don't proceed with reconnection if disposed
if (_shouldDisconnect) return;
}
// Increase the backoff time for the next retry.
_backoffTime == 0 ? _backoffTime++ : _backoffTime *= 2;
// Increment the retry count and attempt the reconnect.
_retries++;
// Add directly to messages, not buffer, so it shows immediately
_messages.add(
IRCMessage.createNotice(
message: 'Reconnecting... (attempt $_retries of $_maxRetries)',
),
// Attempting phase - update same message, no add/remove
updateReconnectMessage(
'Reconnecting... (attempt $_retries of $_maxRetries)',
);
_channelListener?.cancel();
connectToChat(isReconnect: true);
},

View file

@ -363,6 +363,11 @@ class ChatMessage extends StatelessWidget {
);
break;
case Command.notice:
// Use tabular figures so countdown numbers don't shift horizontally
final noticeStyle = TextStyle(
color: messageHeaderTextColor,
fontFeatures: const [FontFeature.tabularFigures()],
);
if (ircMessage.actionCallback != null &&
ircMessage.actionLabel != null) {
renderMessage = Text.rich(
@ -370,7 +375,7 @@ class ChatMessage extends StatelessWidget {
children: [
TextSpan(
text: ircMessage.message ?? '',
style: TextStyle(color: messageHeaderTextColor),
style: noticeStyle,
),
TextSpan(text: ' '),
TextSpan(
@ -390,7 +395,7 @@ class ChatMessage extends StatelessWidget {
} else {
renderMessage = Text.rich(
TextSpan(text: ircMessage.message),
style: TextStyle(color: messageHeaderTextColor),
style: noticeStyle,
);
}
break;