* feat: implement floating banner system - Add FloatingBanner component with gesture support and keyboard awareness - Implement BannerManager singleton for banner lifecycle management - Create floating banner screen with SafeAreaProvider integration - Add comprehensive banner configuration types and positioning - Update Banner component to use modern gesture handling - Enhance BannerItem with improved typography and spacing (40px height) - Add ConnectionBanner improvements with better sizing - Remove ConnectionBanner from channel list (moved to floating system) - Update screens constants (remove FLOATING_BANNER - handled as overlay) - Add i18n support for limited network connection message The system provides: - Auto-hide functionality with customizable duration - Position-aware rendering (top/bottom with keyboard adjustment) - Tablet-specific offset handling - Swipe-to-dismiss with configurable thresholds - Custom component support alongside default banner items - Comprehensive test coverage with device-specific scenarios * docs: add floating banner system documentation and cleanup - Add comprehensive floating-banner.md with architecture diagrams - Remove incompatible connection_banner/index.ts file - Update device.ts hooks for better keyboard handling - Simplify screens/index.tsx floating banner registration - Update test/setup.ts to remove deprecated keyboard mocks - Clean up keyboard height logic and ESLint issues The documentation covers: - System architecture and component relationships - API reference and usage patterns - Performance considerations and best practices - Integration points and troubleshooting guide - Comprehensive testing strategy All tests now pass with the updated setup. * fix issue with translation file * some self cleanup. * renamed index.tsx => Banner.tsx * creaete meaningful tests for Banner component and all the hooks. * fix tests * cleanup based on initial review by AI * dismissible was set to true, changing to what was configured. * making title and message optional * feat(MM-65145): Network connectivity/performance observer * add MONITOR_NETWORK_PERFORMANCE * i18n stuff * remove unused props * Undo some AI changes that caught by another AI * network connectivity observation md * addressed some comments in PR * more fixes based on PR review. * added future enhancement * dismissOverlay will be awaited * delay dismissing overlay so we don't have to show a new one all the time * make the banner stackable * Fix issue with last banner dismissal delayed by 2s * update floating-banner test * put back an extra space * add a todo to use namespace * add comment on priority order for connectivity performance * use static const vs magic number * add a guard against adding performanceSubject when server has been removed * fix failing tests * clean-up based on review by @enahum * fix failing test * fix failiing tests * rename confusing var * update changes to types * fixed issue with swipe not working on android * performance and connectivity use the same id so that 2 banners won't appear * fix issue w/ android not registering touch events behind the overlay * fix failing test * hideBanner now needs id. * Connection status unknown added in en.json * update the doc * animate the banner moving up when bottom banner first appear. * removed unused functions and update tests * add useMemo and useCallback * update jsdoc to say dismissable is default true * fix failing test
21 KiB
Network Connectivity Observation System
Table of Contents
- Overview
- Architecture
- System Components
- Data Flow
- Technical Specifications
- Banner Display Logic
- Performance Detection Algorithm
- Configuration
Overview
The Network Connectivity Observation System is a comprehensive monitoring solution that tracks network performance and connectivity status in real-time, providing contextual feedback to users through an intelligent banner system.
Key Features
- Real-time Performance Monitoring: Tracks request latency to detect network degradation
- Early Detection System: Identifies slow requests before completion (2s threshold)
- Smart Banner Management: Context-aware banner display with anti-spam mechanisms
- Multi-Server Support: Independent tracking per server URL
- Lifecycle Management: Handles app state transitions (background/foreground)
Architecture
System Diagram
graph TB
subgraph NCSM["NetworkConnectivitySubscriptionManager<br/>Manages 5 Subscriptions"]
direction LR
SUB1[1. AppState]
SUB2[2. NetInfo]
SUB3[3. Active Servers]
SUB4[4. WebSocket State]
SUB5[5. Performance State]
end
NPM["NetworkPerformanceManager<br/>• Request Tracking<br/>• Performance Detection<br/>• Observable Streams<br/>• Sliding Window 20 requests"]
NCM["NetworkConnectivityManager<br/>• updateState()<br/>• updatePerformanceState()<br/>• Banner Priority Logic<br/>• Suppression Control"]
BM[BannerManager<br/>• Show/Hide Banner<br/>• Auto-Hide<br/>• Cleanup]
CB[ConnectionBanner<br/>FloatingBanner]
NCSM -->|updates| NCM
NCM -->|controls| BM
BM -->|renders| CB
style NCSM fill:#e1f5ff
style NCM fill:#fff4e1
style NPM fill:#f0e1ff
style BM fill:#d4edda
System Components
1. NetworkPerformanceManager
Purpose: Monitors request performance and detects network degradation
Key Responsibilities:
- Track active requests with unique IDs
- Detect slow requests (≥2000ms) early via timers
- Maintain sliding window of request outcomes (20 requests)
- Calculate performance state based on slow request percentage
- Emit performance state changes via RxJS observables
Public API:
class NetworkPerformanceManager {
// Start tracking a request, returns unique ID
startRequestTracking(serverUrl: string, url: string): string
// Complete tracking with metrics
completeRequestTracking(serverUrl: string, requestId: string, metrics: ClientResponseMetrics): void
// Cancel tracking on failure
cancelRequestTracking(serverUrl: string, requestId: string): void
// Observe performance state changes
observePerformanceState(serverUrl: string): Observable<NetworkPerformanceState>
// Get current state
getCurrentPerformanceState(serverUrl: string): NetworkPerformanceState
// Cleanup
removeServer(serverUrl: string): void
}
2. NetworkConnectivityManager
Purpose: Orchestrates banner display based on connectivity and performance states
Key Responsibilities:
- Maintain current WebSocket, network, and performance state
- Implement banner display priority logic
- Handle suppression mechanisms
- Manage first connection vs reconnection scenarios
Public API:
class NetworkConnectivityManager {
// Initialize with server URL
init(serverUrl: string | null): void
// Update connection status
setServerConnectionStatus(connected: boolean, serverUrl: string | null): void
// Update WebSocket/network state
updateState(
websocketState: WebsocketConnectedState,
netInfo: {isInternetReachable: boolean | null},
appState: string
): void
// Update performance state
updatePerformanceState(performanceState: NetworkPerformanceState): void
// Cleanup
cleanup(): void
shutdown(): void
}
3. NetworkConnectivitySubscriptionManager
Purpose: Coordinates all network-related subscriptions and server lifecycle
Key Responsibilities:
- Subscribe to app state changes (active/background)
- Subscribe to network info changes
- Subscribe to active server changes
- Subscribe to WebSocket state per server
- Subscribe to performance state per server
Public API:
class NetworkConnectivitySubscriptionManager {
// Initialize all subscriptions
init(): void
// Stop subscriptions (preserves AppState listener)
stop(): void
// Complete shutdown
shutdown(): void
}
4. ClientTracking (Enhanced)
Purpose: Integrate performance tracking into network requests
Integration Points:
class ClientTracking {
doFetchWithTracking = async (url: string, options: ClientOptions) => {
// 1. Start performance tracking
const performanceRequestId = this.startNetworkPerformanceTracking(url);
try {
// 2. Execute request
response = await request(url, this.buildRequestOptions(options));
// 3. Complete tracking with metrics
this.completeNetworkPerformanceTracking(performanceRequestId, url, response.metrics);
} catch (error) {
// 4. Cancel tracking on error
this.cancelNetworkPerformanceTracking(performanceRequestId);
throw error;
}
}
}
Data Flow
1. Request Performance Tracking Flow
flowchart TD
Start[HTTP Request Starts] --> CT[ClientTracking.doFetchWithTracking]
CT -->|startNetworkPerformanceTracking| NPM[NetworkPerformanceManager]
NPM -->|1. Generate unique requestId<br/>2. Set 2s timer<br/>3. Store in activeRequests| Decision{Request Outcome}
Decision -->|Completes < 2s| Complete[Record Outcome<br/>wasEarly: false]
Decision -->|Timer Triggers @ 2s| Early[Early Detection<br/>Record Outcome<br/>wasEarly: true]
Decision -->|Fails| Cancel[Cancel Tracking<br/>No outcome recorded]
Complete --> Update[Update Performance State]
Early --> Update
Update --> Steps[1. Add to outcomes window max 20<br/>2. Calculate slow %<br/>3. Determine state<br/>4. Emit if changed]
style NPM fill:#f0e1ff
style Update fill:#e1f5ff
style Complete fill:#d4edda
style Early fill:#fff3cd
style Cancel fill:#f8d7da
2. Subscription and State Flow
sequenceDiagram
participant Init as app/init/app.ts
participant NCSM as NetworkConnectivitySubscriptionManager
participant NCM as NetworkConnectivityManager
participant WSM as WebsocketManager
participant NPM as NetworkPerformanceManager
participant DB as Active Servers DB
Init->>NCM: 1. initialize()<br/>init(activeServerUrl)
Init->>NCSM: 2. start()<br/>init()
NCSM->>NCSM: AppState.addEventListener()
NCSM->>NCSM: NetInfo.addEventListener()
NCSM->>DB: subscribeActiveServers()
DB->>NCSM: Active Server Changed<br/>findMostRecentServer() → serverUrl
NCSM->>NCM: setServerConnectionStatus(true, serverUrl)
NCSM->>WSM: observeWebsocketState(serverUrl)
NCSM->>NPM: observePerformanceState(serverUrl)
loop State Updates
WSM-->>NCSM: Emit WebSocket State
NCSM->>NCM: updateState(websocketState, netInfo, appState)
NCM->>NCM: updateBanner()<br/>Priority Order:<br/>1. Disconnected<br/>2. Performance<br/>3. Connecting<br/>4. Reconnection<br/>5. Connected
NPM-->>NCSM: Emit Performance State
NCSM->>NCM: updatePerformanceState(performanceState)
NCM->>NCM: updateBanner()
end
Technical Specifications
Performance Detection Algorithm
Constants
const SLOW_REQUEST_THRESHOLD = 2000; // 2 seconds
const SLOW_REQUEST_PERCENTAGE_THRESHOLD = 0.7; // 70%
const REQUEST_OUTCOME_WINDOW_SIZE = 20; // Last 20 requests
const MINIMUM_REQUESTS_FOR_INITIAL_DETECTION = 4; // First detection
const MINIMUM_REQUESTS_FOR_SUBSEQUENT_DETECTION = 10; // Later detections
State Calculation Logic
function calculatePerformanceStateFromOutcomes(
outcomes: RequestOutcome[],
isInitialDetection: boolean
): NetworkPerformanceState {
const minimumRequests = isInitialDetection
? MINIMUM_REQUESTS_FOR_INITIAL_DETECTION // 4 requests
: MINIMUM_REQUESTS_FOR_SUBSEQUENT_DETECTION; // 10 requests
// Not enough data yet for initial detection
if (isInitialDetection && outcomes.length < minimumRequests) {
return 'normal';
}
const slowRequestCount = outcomes.filter(o => o.isSlow).length;
const slowPercentage = slowRequestCount / outcomes.length;
// Slow if ≥70% of requests are slow
return slowPercentage >= SLOW_REQUEST_PERCENTAGE_THRESHOLD
? 'slow'
: 'normal';
}
Early Detection System
// When request tracking starts
startRequestTracking(serverUrl: string, url: string): string {
const requestId = generateUniqueId();
// Set timer for early detection
const checkTimer = setTimeout(() => {
// If still active after 2s, mark as slow
if (activeRequests[requestId]) {
recordRequestOutcome({
timestamp: Date.now(),
isSlow: true,
wasEarlyDetection: true // Flag for early detection
});
clearActiveRequest(requestId);
}
}, SLOW_REQUEST_THRESHOLD); // 2000ms
activeRequests[requestId] = { id, url, startTime, checkTimer };
return requestId;
}
// When request completes
completeRequestTracking(serverUrl: string, requestId: string, metrics: ClientResponseMetrics) {
const wasEarlyDetected = !activeRequests[requestId]; // Already removed by timer
clearActiveRequest(requestId);
// Only record if not already recorded by early detection
if (!wasEarlyDetected) {
recordRequestOutcome({
timestamp: Date.now(),
isSlow: metrics.latency >= SLOW_REQUEST_THRESHOLD,
wasEarlyDetection: false
});
}
}
Sliding Window Management
recordRequestOutcome(serverUrl: string, outcome: RequestOutcome) {
if (!requestOutcomes[serverUrl]) {
requestOutcomes[serverUrl] = [];
}
requestOutcomes[serverUrl].push(outcome);
// Keep only last 20 requests
if (requestOutcomes[serverUrl].length > REQUEST_OUTCOME_WINDOW_SIZE) {
requestOutcomes[serverUrl] = requestOutcomes[serverUrl].slice(-REQUEST_OUTCOME_WINDOW_SIZE);
}
// Recalculate and emit new state
const newState = calculatePerformanceStateFromOutcomes(
requestOutcomes[serverUrl],
isInitialDetection[serverUrl]
);
performanceSubject.next(newState);
}
Banner Display Logic
Priority Order (First Match Wins)
private updateBanner() {
// 0. Hide banner if no server or app in background
if (!currentServerUrl || appState === 'background') {
BannerManager.hideBanner();
return;
}
// 1. HIGHEST PRIORITY: Disconnected state
if (shouldShowDisconnectedBanner(websocketState, isOnAppStart)) {
showConnectivity(false); // Persistent banner
return;
}
// 2. Performance degradation
if (shouldShowPerformanceBanner(performanceState, performanceSuppressed)) {
showPerformance(10000); // Auto-hide after 10s
return;
}
// 3. Connecting state
if (shouldShowConnectingBanner(websocketState, isOnAppStart)) {
showConnectivity(false); // Persistent banner
return;
}
// 4. Reconnection success
if (shouldShowReconnectionBanner(websocketState, previousState, isOnAppStart)) {
showConnectivity(true, 3000); // Auto-hide after 3s
return;
}
// 5. LOWEST PRIORITY: Connected (hide banner)
BannerManager.hideBanner();
}
Banner Decision Functions
Disconnected Banner
shouldShowDisconnectedBanner(
websocketState: WebsocketConnectedState | null,
isOnAppStart: boolean
): boolean {
return websocketState === 'not_connected' && !isOnAppStart;
}
- Shows: When disconnected after initial connection
- Hides: On first connection (app start)
- Duration: Persistent until state changes
- Messages (internationalized):
- "The server is not reachable" (
connection_banner.not_reachable) - when internet is reachable but server is not - "Unable to connect to network" (
connection_banner.not_connected) - when internet is not reachable - "Connection status unknown" (
connection_banner.status_unknown) - when websocketState or netInfo is null
- "The server is not reachable" (
Performance Banner
shouldShowPerformanceBanner(
performanceState: NetworkPerformanceState | null,
performanceSuppressed: boolean
): boolean {
return performanceState === 'slow' && !performanceSuppressed;
}
- Shows: When performance degrades to 'slow'
- Suppression: User can dismiss; suppressed until performance returns to 'normal'
- Duration: Auto-hide after 10 seconds
- Message: "Limited network connection" (
connection_banner.limited_network_connection)
Connecting Banner
shouldShowConnectingBanner(
websocketState: WebsocketConnectedState | null,
isOnAppStart: boolean
): boolean {
return websocketState === 'connecting' && !isOnAppStart;
}
- Shows: When reconnecting after initial connection
- Hides: During first connection
- Duration: Persistent until state changes
- Message: "Connecting..." (
connection_banner.connecting)
Reconnection Banner
shouldShowReconnectionBanner(
websocketState: WebsocketConnectedState | null,
previousWebsocketState: WebsocketConnectedState | null,
isOnAppStart: boolean
): boolean {
return websocketState === 'connected' &&
previousWebsocketState !== 'connected' &&
!isOnAppStart;
}
- Shows: When connection restored after being disconnected/connecting
- Hides: On first connection
- Duration: Auto-hide after 3 seconds
- Message: "Connection restored" (
connection_banner.connected)
Message Internationalization
All banner messages are internationalized using formatMessage() for proper localization:
private getConnectionMessage(): string {
// Guard against uninitialized state
if (!this.websocketState || !this.netInfo) {
return this.intl.formatMessage({
id: 'connection_banner.status_unknown',
defaultMessage: 'Connection status unknown'
});
}
return getConnectionMessageText(
this.websocketState,
this.netInfo.isInternetReachable,
this.intl.formatMessage
);
}
Message Keys:
connection_banner.status_unknown- When websocketState or netInfo is nullconnection_banner.connected- Connection restoredconnection_banner.connecting- Connecting...connection_banner.not_reachable- Server not reachable (but internet is)connection_banner.not_connected- Unable to connect to networkconnection_banner.limited_network_connection- Performance degraded
Suppression Mechanisms
Performance Suppression
// User dismisses performance banner
onDismiss: () => {
this.performanceSuppressedUntilNormal = true;
}
// Reset when performance returns to normal
updatePerformanceState(performanceState: NetworkPerformanceState) {
if (this.performanceSuppressedUntilNormal && performanceState === 'normal') {
this.performanceSuppressedUntilNormal = false;
}
}
First Connection Logic
// isOnAppStart flag prevents banners on initial connection
updateState(websocketState, netInfo, appState) {
this.previousWebsocketState = this.websocketState;
this.websocketState = websocketState;
this.updateBanner();
// Clear flag only after first successful connection
if (websocketState === 'connected' && this.isOnAppStart) {
this.isOnAppStart = false;
}
}
Configuration
Config Flags
assets/base/config.json
{
"CollectNetworkMetrics": false,
"MonitorNetworkPerformance": true
}
CollectNetworkMetrics (Existing):
- Controls telemetry/analytics collection
- Used by
PerformanceMetricsManager - Tracks request groups, parallel requests, etc.
MonitorNetworkPerformance (New):
- Controls real-time performance monitoring
- Used by
NetworkPerformanceManager - Enables early detection and banner display
- Independent from
CollectNetworkMetrics
Integration in ClientTracking
// Network metrics collection (existing)
if (groupLabel && CollectNetworkMetrics) {
this.incrementRequestCount(groupLabel);
this.trackRequest(groupLabel, url, response.metrics);
this.decrementRequestCount(groupLabel);
}
// Performance monitoring (new)
if (MonitorNetworkPerformance) {
const requestId = NetworkPerformanceManager.startRequestTracking(baseUrl, url);
try {
response = await request(url, options);
NetworkPerformanceManager.completeRequestTracking(baseUrl, requestId, response.metrics);
} catch (error) {
NetworkPerformanceManager.cancelRequestTracking(baseUrl, requestId);
}
}
Lifecycle Management
App State Transitions
// AppState: active → background
handleAppStateChange(nextAppState: AppStateStatus) {
if (nextAppState !== 'active') {
// 1. Clear all active request timers
clearAllActiveRequests();
// 2. Reset performance state to normal
performanceSubjects.forEach(subject => subject.next('normal'));
// 3. Hide all banners
BannerManager.hideBanner();
// 4. Stop subscriptions (except AppState listener)
NetworkConnectivitySubscriptionManager.stop();
}
}
// AppState: background → active
handleAppStateChange(nextAppState: AppStateStatus) {
if (nextAppState === 'active') {
// Restart all subscriptions
NetworkConnectivitySubscriptionManager.init();
}
}
Server Lifecycle
// Server added/activated
handleActiveServersChange(servers: Server[]) {
const activeServer = findMostRecentServer(servers);
if (activeServer) {
// 1. Set connection status
NetworkConnectivityManager.setServerConnectionStatus(true, activeServer.url);
// 2. Subscribe to WebSocket state
websocketSubscription = WebsocketManager
.observeWebsocketState(activeServer.url)
.subscribe(state => {
NetworkConnectivityManager.updateState(state, netInfo, appState);
});
// 3. Subscribe to performance state
performanceSubscription = NetworkPerformanceManager
.observePerformanceState(activeServer.url)
.subscribe(state => {
NetworkConnectivityManager.updatePerformanceState(state);
});
}
}
// Server removed/logout
terminateSession(serverUrl: string) {
// 1. Clear connection status
NetworkConnectivityManager.setServerConnectionStatus(false, serverUrl);
// 2. Remove performance tracking
NetworkPerformanceManager.removeServer(serverUrl);
// 3. Stop subscriptions
NetworkConnectivitySubscriptionManager.stop();
}
Performance Characteristics
Memory Usage
- Request Outcomes: Max 20 per server (sliding window)
- Active Requests: Cleared on completion or 2s timeout
- Timers: One per active request, auto-cleared
Detection Speed
- Early Detection: 2 seconds (timer-based)
- Initial Detection: 4-8 slow requests (4 minimum)
- Subsequent Detection: 10-20 requests (10 minimum with 70% threshold)
Banner Timing
- Disconnected: Immediate, persistent
- Connecting: Immediate, persistent
- Performance: After threshold met, auto-hide 10s
- Reconnected: Immediate, auto-hide 3s
Error Handling
Request Failure
try {
response = await request(url, options);
completeRequestTracking(requestId, response.metrics);
} catch (error) {
// Cancel tracking - no outcome recorded
cancelRequestTracking(requestId);
throw error;
}
Missing Metrics
completeRequestTracking(requestId, metrics) {
if (!metrics) {
// Silently skip - no outcome recorded
clearActiveRequest(requestId);
return;
}
// ... record outcome
}
Subscription Cleanup
// Safe to call multiple times
stop() {
websocketSubscription?.unsubscribe();
performanceSubscription?.unsubscribe();
netInfoSubscription?.();
activeServersUnsubscriber?.unsubscribe();
// Clear references
websocketSubscription = undefined;
performanceSubscription = undefined;
// ...
}
Testing Strategy
Unit Tests
- NetworkPerformanceManager: Request tracking, state calculation, sliding window
- NetworkConnectivityManager: Banner decision functions, state transitions
- NetworkConnectivitySubscriptionManager: Subscription lifecycle, server changes
Test Coverage
- Early detection (timer triggers)
- Request completion before/after threshold
- State transitions (normal ↔ slow)
- Banner priority order
- Suppression mechanisms
- App lifecycle (background/foreground)
- Server lifecycle (add/remove)
- Error scenarios (missing metrics, failed requests)
Future Enhancements
Potential Improvements
- Adaptive Thresholds: Adjust SLOW_REQUEST_THRESHOLD based on historical performance
- Network Type Awareness: Different thresholds for WiFi vs Cellular
- Exponential Backoff: For repeated performance banners
- Metrics Reporting: Send detection events to analytics
- User Preferences: Allow users to configure sensitivity
Known Limitations
- Single Server Focus: Only monitors most recently active server
- Fixed Thresholds: 2s threshold may not suit all use cases
- No Geographical Context: Doesn't account for server location
- Binary States: Only 'normal' vs 'slow' (no 'excellent' or 'poor')