Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Improved chat title tap area for easier navigation to contact info
- Optimized relay connection error banner with intelligent 30-second delay and immediate dismissal on reconnection

### Deprecated

Expand Down
111 changes: 111 additions & 0 deletions lib/config/providers/delayed_relay_error_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';

import 'package:whitenoise/config/providers/relay_status_provider.dart';

class DelayedRelayErrorState {
final bool shouldShowBanner;
final bool isDelayActive;

const DelayedRelayErrorState({
this.shouldShowBanner = false,
this.isDelayActive = false,
});

DelayedRelayErrorState copyWith({
bool? shouldShowBanner,
bool? isDelayActive,
}) {
return DelayedRelayErrorState(
shouldShowBanner: shouldShowBanner ?? this.shouldShowBanner,
isDelayActive: isDelayActive ?? this.isDelayActive,
);
}
}

class DelayedRelayErrorNotifier extends Notifier<DelayedRelayErrorState> {
static const Duration _delayDuration = Duration(seconds: 30);
final _logger = Logger('DelayedRelayErrorNotifier');

Timer? _delayTimer;
bool _lastConnectionStatus = true;

@override
DelayedRelayErrorState build() {
ref.listen<AsyncValue<bool>>(allRelayTypesConnectionProvider, (previous, next) {
next.when(
data: (isConnected) => _handleConnectionStatusChange(isConnected),
loading: () {
_logger.info('Relay connection status is loading');
},
error: (error, stackTrace) {
_logger.warning('Error in relay connection status: $error');
_handleConnectionStatusChange(false);
},
);
});

return const DelayedRelayErrorState();
}

void _handleConnectionStatusChange(bool isConnected) {
_logger.info('Relay connection status changed: $isConnected (was: $_lastConnectionStatus)');

if (isConnected && !_lastConnectionStatus) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I'm understanding this:

  1. Any time we get a change in connection status, this _handleConnectionStatusChange method is called.
  2. If we're entering a not connected state then we show the banner and start the timer to check in 30 seconds.
  3. If we're entering a connected state then we hide the banner and cancel the timer?

How we we hear about these changes in connection status?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we enter a not connected state we delay for 30 seconds, and if after that 30 seconds connection is not back we show the banner

_logger.info('Connection restored - hiding banner immediately');
_cancelDelayTimer();
state = state.copyWith(shouldShowBanner: false, isDelayActive: false);
} else if (!isConnected && _lastConnectionStatus) {
_logger.info('Connection lost - starting 30-second delay timer');
_startDelayTimer();
}

_lastConnectionStatus = isConnected;
}

void _startDelayTimer() {
_cancelDelayTimer();
state = state.copyWith(isDelayActive: true, shouldShowBanner: false);
_delayTimer = Timer(_delayDuration, () {
_logger.info('30-second delay completed - checking if banner should still be shown');
final currentConnectionAsync = ref.read(allRelayTypesConnectionProvider);
currentConnectionAsync.when(
data: (isConnected) {
if (!isConnected) {
_logger.info('Still disconnected after delay - showing banner');
state = state.copyWith(shouldShowBanner: true, isDelayActive: false);
} else {
_logger.info('Connection restored during delay - not showing banner');
state = state.copyWith(shouldShowBanner: false, isDelayActive: false);
}
},
loading: () {
state = state.copyWith(shouldShowBanner: false, isDelayActive: false);
},
error: (error, stackTrace) {
_logger.warning('Error checking connection status after delay: $error');
state = state.copyWith(shouldShowBanner: true, isDelayActive: false);
},
);
});
}

void _cancelDelayTimer() {
if (_delayTimer != null) {
_logger.info('Cancelling delay timer');
_delayTimer!.cancel();
_delayTimer = null;
}
}

void dispose() {
_cancelDelayTimer();
}
}

final delayedRelayErrorProvider =
NotifierProvider<DelayedRelayErrorNotifier, DelayedRelayErrorState>(
DelayedRelayErrorNotifier.new,
);
5 changes: 5 additions & 0 deletions lib/config/providers/polling_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
import 'package:whitenoise/config/providers/active_account_provider.dart';
import 'package:whitenoise/config/providers/chat_provider.dart';
import 'package:whitenoise/config/providers/group_provider.dart';
import 'package:whitenoise/config/providers/relay_status_provider.dart';
import 'package:whitenoise/config/providers/welcomes_provider.dart';

class PollingNotifier extends Notifier<bool> {
Expand Down Expand Up @@ -76,6 +77,7 @@ class PollingNotifier extends Notifier<bool> {
// Load all data fully on first run
await ref.read(welcomesProvider.notifier).loadWelcomes();
await ref.read(groupsProvider.notifier).loadGroups();
await ref.read(relayStatusProvider.notifier).refreshStatuses();
ref.invalidate(activeAccountProvider);

// Load messages for all groups in a build-safe way
Expand Down Expand Up @@ -115,6 +117,9 @@ class PollingNotifier extends Notifier<bool> {
// Check for new groups incrementally
await ref.read(groupsProvider.notifier).checkForNewGroups();

// Check relay status regularly
await ref.read(relayStatusProvider.notifier).refreshStatuses();

// Check for new messages incrementally
final groups = ref.read(groupsProvider).groups;
if (groups != null && groups.isNotEmpty) {
Expand Down
2 changes: 0 additions & 2 deletions lib/config/providers/relay_status_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,7 @@ final relayStatusProvider = NotifierProvider<RelayStatusNotifier, RelayStatusSta
RelayStatusNotifier.new,
);

// Provider for checking if all relay types have at least one connected relay
final allRelayTypesConnectionProvider = FutureProvider<bool>((ref) async {
// Watch the relay status provider to trigger rebuilds when statuses change
ref.watch(relayStatusProvider);

final notifier = ref.read(relayStatusProvider.notifier);
Expand Down
22 changes: 8 additions & 14 deletions lib/ui/contact_list/chat_list_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:whitenoise/config/providers/chat_provider.dart';
import 'package:whitenoise/config/providers/delayed_relay_error_provider.dart';
import 'package:whitenoise/config/providers/group_provider.dart';
import 'package:whitenoise/config/providers/polling_provider.dart';
import 'package:whitenoise/config/providers/profile_ready_card_visibility_provider.dart';
Expand Down Expand Up @@ -289,12 +290,8 @@ class _ChatListScreenState extends ConsumerState<ChatListScreen> with TickerProv
(item.lastMessage?.content?.toLowerCase().contains(searchLower) ?? false);
}).toList();

final allRelayTypesConnectionAsync = ref.watch(allRelayTypesConnectionProvider);
final notAllRelayTypesConnected = allRelayTypesConnectionAsync.when(
data: (allConnected) => !allConnected,
loading: () => true,
error: (_, _) => true,
);
final delayedRelayErrorState = ref.watch(delayedRelayErrorProvider);
final shouldShowRelayError = delayedRelayErrorState.shouldShowBanner;

return GestureDetector(
onTap: _unfocusSearchIfNeeded,
Expand All @@ -320,19 +317,17 @@ class _ChatListScreenState extends ConsumerState<ChatListScreen> with TickerProv
actions: [
IconButton(
onPressed:
notAllRelayTypesConnected
shouldShowRelayError
? null
: () {
_unfocusSearchIfNeeded();
NewChatBottomSheet.show(context);
},
icon: WnImage(
notAllRelayTypesConnected
? AssetsPaths.icOffChat
: AssetsPaths.icAddNewChat,
shouldShowRelayError ? AssetsPaths.icOffChat : AssetsPaths.icAddNewChat,
size: 21.w,
color: context.colors.solidNeutralWhite.withValues(
alpha: notAllRelayTypesConnected ? 0.5 : 1.0,
alpha: shouldShowRelayError ? 0.5 : 1.0,
),
),
),
Expand All @@ -341,7 +336,7 @@ class _ChatListScreenState extends ConsumerState<ChatListScreen> with TickerProv
pinned: true,
),

if (notAllRelayTypesConnected)
if (shouldShowRelayError)
SliverToBoxAdapter(
child: SizedBox(height: 100.h),
),
Expand Down Expand Up @@ -438,7 +433,6 @@ class _ChatListScreenState extends ConsumerState<ChatListScreen> with TickerProv
).animate().fade(),
),
),

SliverPadding(
padding: EdgeInsets.only(top: 8.h, bottom: 32.h),
sliver: SliverList.separated(
Expand All @@ -461,7 +455,7 @@ class _ChatListScreenState extends ConsumerState<ChatListScreen> with TickerProv
],
),

if (notAllRelayTypesConnected)
if (shouldShowRelayError)
Positioned(
top: 64.h + kToolbarHeight,
left: 0,
Expand Down
4 changes: 2 additions & 2 deletions lib/ui/core/themes/src/colors_dark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ class DarkAppColors {
/// base/muted, base/input
static const baseMuted = Color(0xff474C47);

static const gray100 = Color(0xffF5F5F5);
static const gray200 = Color(0xFFE5E5E5);
static const gray100 = Color(0xff404040);
static const gray200 = Color(0xff2A2A2A);

/// Text/Default/Secondary
static const textDefaultSecondary = Color(0xffCDCECD);
Expand Down