You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							360 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							360 lines
						
					
					
						
							11 KiB
						
					
					
				
								/**
							 | 
						|
								 * Stale Data UX Snippets
							 | 
						|
								 * 
							 | 
						|
								 * Platform-specific implementations for showing stale data banners
							 | 
						|
								 * when polling hasn't succeeded for extended periods
							 | 
						|
								 */
							 | 
						|
								
							 | 
						|
								// Common configuration
							 | 
						|
								const STALE_DATA_CONFIG = {
							 | 
						|
								  staleThresholdHours: 4,
							 | 
						|
								  criticalThresholdHours: 24,
							 | 
						|
								  bannerAutoDismissMs: 10000
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// Common i18n keys
							 | 
						|
								const I18N_KEYS = {
							 | 
						|
								  'staleness.banner.title': 'Data may be outdated',
							 | 
						|
								  'staleness.banner.message': 'Last updated {hours} hours ago. Tap to refresh.',
							 | 
						|
								  'staleness.banner.critical': 'Data is very outdated. Please refresh.',
							 | 
						|
								  'staleness.banner.action_refresh': 'Refresh Now',
							 | 
						|
								  'staleness.banner.action_settings': 'Settings',
							 | 
						|
								  'staleness.banner.dismiss': 'Dismiss'
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Android Implementation
							 | 
						|
								 */
							 | 
						|
								class AndroidStaleDataUX {
							 | 
						|
								  private context: Record<string, unknown>; // Android Context
							 | 
						|
								  private notificationManager: Record<string, unknown>; // NotificationManager
							 | 
						|
								  
							 | 
						|
								  constructor(context: Record<string, unknown>) {
							 | 
						|
								    this.context = context;
							 | 
						|
								    this.notificationManager = context.getSystemService('notification');
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  showStalenessBanner(hoursSinceUpdate: number): void {
							 | 
						|
								    const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
							 | 
						|
								    
							 | 
						|
								    const title = this.context.getString(I18N_KEYS['staleness.banner.title']);
							 | 
						|
								    const message = isCritical 
							 | 
						|
								      ? this.context.getString(I18N_KEYS['staleness.banner.critical'])
							 | 
						|
								      : this.context.getString(I18N_KEYS['staleness.banner.message'], hoursSinceUpdate);
							 | 
						|
								    
							 | 
						|
								    // Create notification
							 | 
						|
								    const notification = {
							 | 
						|
								      smallIcon: 'ic_warning',
							 | 
						|
								      contentTitle: title,
							 | 
						|
								      contentText: message,
							 | 
						|
								      priority: isCritical ? 'high' : 'normal',
							 | 
						|
								      autoCancel: true,
							 | 
						|
								      actions: [
							 | 
						|
								        {
							 | 
						|
								          title: this.context.getString(I18N_KEYS['staleness.banner.action_refresh']),
							 | 
						|
								          intent: this.createRefreshIntent()
							 | 
						|
								        },
							 | 
						|
								        {
							 | 
						|
								          title: this.context.getString(I18N_KEYS['staleness.banner.action_settings']),
							 | 
						|
								          intent: this.createSettingsIntent()
							 | 
						|
								        }
							 | 
						|
								      ]
							 | 
						|
								    };
							 | 
						|
								    
							 | 
						|
								    this.notificationManager.notify('stale_data_warning', notification);
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  private createRefreshIntent(): Record<string, unknown> {
							 | 
						|
								    // Create PendingIntent for refresh action
							 | 
						|
								    return {
							 | 
						|
								      action: 'com.timesafari.dailynotification.REFRESH_DATA',
							 | 
						|
								      flags: ['FLAG_UPDATE_CURRENT']
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  private createSettingsIntent(): Record<string, unknown> {
							 | 
						|
								    // Create PendingIntent for settings action
							 | 
						|
								    return {
							 | 
						|
								      action: 'com.timesafari.dailynotification.OPEN_SETTINGS',
							 | 
						|
								      flags: ['FLAG_UPDATE_CURRENT']
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  showInAppBanner(_hoursSinceUpdate: number): void {
							 | 
						|
								    // Show banner in app UI (Snackbar or similar)
							 | 
						|
								    // const message = this.context.getString(
							 | 
						|
								    //   I18N_KEYS['staleness.banner.message'], 
							 | 
						|
								    //   hoursSinceUpdate
							 | 
						|
								    // );
							 | 
						|
								    
							 | 
						|
								    // Create Snackbar (example implementation)
							 | 
						|
								    // const snackbar = {
							 | 
						|
								    //   message,
							 | 
						|
								    //   duration: 'LENGTH_INDEFINITE',
							 | 
						|
								    //   action: {
							 | 
						|
								    //     text: this.context.getString(I18N_KEYS['staleness.banner.action_refresh']),
							 | 
						|
								    //     callback: () => this.refreshData()
							 | 
						|
								    //   }
							 | 
						|
								    // };
							 | 
						|
								    
							 | 
						|
								    // Show snackbar
							 | 
						|
								    // Showing Android in-app banner (example implementation)
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  private refreshData(): void {
							 | 
						|
								    // Trigger manual refresh
							 | 
						|
								    // Refreshing data on Android (example implementation)
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * iOS Implementation
							 | 
						|
								 */
							 | 
						|
								class iOSStaleDataUX {
							 | 
						|
								  private viewController: Record<string, unknown>; // UIViewController
							 | 
						|
								  
							 | 
						|
								  constructor(viewController: Record<string, unknown>) {
							 | 
						|
								    this.viewController = viewController;
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  showStalenessBanner(hoursSinceUpdate: number): void {
							 | 
						|
								    const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
							 | 
						|
								    
							 | 
						|
								    const title = NSLocalizedString(I18N_KEYS['staleness.banner.title'], '');
							 | 
						|
								    const message = isCritical 
							 | 
						|
								      ? NSLocalizedString(I18N_KEYS['staleness.banner.critical'], '')
							 | 
						|
								      : NSLocalizedString(I18N_KEYS['staleness.banner.message'], '').replace('{hours}', hoursSinceUpdate.toString());
							 | 
						|
								    
							 | 
						|
								    // Create alert controller
							 | 
						|
								    const alert = {
							 | 
						|
								      title,
							 | 
						|
								      message,
							 | 
						|
								      preferredStyle: 'alert',
							 | 
						|
								      actions: [
							 | 
						|
								        {
							 | 
						|
								          title: NSLocalizedString(I18N_KEYS['staleness.banner.action_refresh'], ''),
							 | 
						|
								          style: 'default',
							 | 
						|
								          handler: () => this.refreshData()
							 | 
						|
								        },
							 | 
						|
								        {
							 | 
						|
								          title: NSLocalizedString(I18N_KEYS['staleness.banner.action_settings'], ''),
							 | 
						|
								          style: 'default',
							 | 
						|
								          handler: () => this.openSettings()
							 | 
						|
								        },
							 | 
						|
								        {
							 | 
						|
								          title: NSLocalizedString(I18N_KEYS['staleness.banner.dismiss'], ''),
							 | 
						|
								          style: 'cancel'
							 | 
						|
								        }
							 | 
						|
								      ]
							 | 
						|
								    };
							 | 
						|
								    
							 | 
						|
								    this.viewController.present(alert, { animated: true });
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  showBannerView(_hoursSinceUpdate: number): void {
							 | 
						|
								    // Create banner view (example implementation)
							 | 
						|
								    // const banner = {
							 | 
						|
								    //   title: NSLocalizedString(I18N_KEYS['staleness.banner.title'], ''),
							 | 
						|
								    //   message: NSLocalizedString(I18N_KEYS['staleness.banner.message'], '').replace('{hours}', hoursSinceUpdate.toString()),
							 | 
						|
								    //   backgroundColor: 'systemYellow',
							 | 
						|
								    //   textColor: 'label',
							 | 
						|
								    //   actions: [
							 | 
						|
								    //     {
							 | 
						|
								    //       title: NSLocalizedString(I18N_KEYS['staleness.banner.action_refresh'], ''),
							 | 
						|
								    //       action: () => this.refreshData()
							 | 
						|
								    //     }
							 | 
						|
								    //   ]
							 | 
						|
								    // };
							 | 
						|
								    
							 | 
						|
								    // Show banner
							 | 
						|
								    // Showing iOS banner view (example implementation)
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  private refreshData(): void {
							 | 
						|
								    // Refreshing data on iOS (example implementation)
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  private openSettings(): void {
							 | 
						|
								    // Opening settings on iOS (example implementation)
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Web Implementation
							 | 
						|
								 */
							 | 
						|
								class WebStaleDataUX {
							 | 
						|
								  private container: HTMLElement;
							 | 
						|
								  
							 | 
						|
								  constructor(container: HTMLElement = document.body) {
							 | 
						|
								    this.container = container;
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  showStalenessBanner(hoursSinceUpdate: number): void {
							 | 
						|
								    const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
							 | 
						|
								    
							 | 
						|
								    const banner = document.createElement('div');
							 | 
						|
								    banner.className = `staleness-banner ${isCritical ? 'critical' : 'warning'}`;
							 | 
						|
								    banner.innerHTML = `
							 | 
						|
								      <div class="banner-content">
							 | 
						|
								        <div class="banner-icon">⚠️</div>
							 | 
						|
								        <div class="banner-text">
							 | 
						|
								          <div class="banner-title">${I18N_KEYS['staleness.banner.title']}</div>
							 | 
						|
								          <div class="banner-message">
							 | 
						|
								            ${isCritical 
							 | 
						|
								              ? I18N_KEYS['staleness.banner.critical']
							 | 
						|
								              : I18N_KEYS['staleness.banner.message'].replace('{hours}', hoursSinceUpdate.toString())
							 | 
						|
								            }
							 | 
						|
								          </div>
							 | 
						|
								        </div>
							 | 
						|
								        <div class="banner-actions">
							 | 
						|
								          <button class="btn-refresh" onclick="window.refreshData()">
							 | 
						|
								            ${I18N_KEYS['staleness.banner.action_refresh']}
							 | 
						|
								          </button>
							 | 
						|
								          <button class="btn-settings" onclick="window.openSettings()">
							 | 
						|
								            ${I18N_KEYS['staleness.banner.action_settings']}
							 | 
						|
								          </button>
							 | 
						|
								          <button class="btn-dismiss" onclick="this.parentElement.parentElement.remove()">
							 | 
						|
								            ${I18N_KEYS['staleness.banner.dismiss']}
							 | 
						|
								          </button>
							 | 
						|
								        </div>
							 | 
						|
								      </div>
							 | 
						|
								    `;
							 | 
						|
								    
							 | 
						|
								    // Add styles
							 | 
						|
								    banner.style.cssText = `
							 | 
						|
								      position: fixed;
							 | 
						|
								      top: 0;
							 | 
						|
								      left: 0;
							 | 
						|
								      right: 0;
							 | 
						|
								      background: ${isCritical ? '#ff6b6b' : '#ffd93d'};
							 | 
						|
								      color: ${isCritical ? 'white' : 'black'};
							 | 
						|
								      padding: 12px;
							 | 
						|
								      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
							 | 
						|
								      z-index: 1000;
							 | 
						|
								      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
							 | 
						|
								    `;
							 | 
						|
								    
							 | 
						|
								    this.container.appendChild(banner);
							 | 
						|
								    
							 | 
						|
								    // Auto-dismiss after timeout
							 | 
						|
								    setTimeout(() => {
							 | 
						|
								      if (banner.parentElement) {
							 | 
						|
								        banner.remove();
							 | 
						|
								      }
							 | 
						|
								    }, STALE_DATA_CONFIG.bannerAutoDismissMs);
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  showToast(hoursSinceUpdate: number): void {
							 | 
						|
								    const toast = document.createElement('div');
							 | 
						|
								    toast.className = 'staleness-toast';
							 | 
						|
								    toast.innerHTML = `
							 | 
						|
								      <div class="toast-content">
							 | 
						|
								        <span class="toast-icon">⚠️</span>
							 | 
						|
								        <span class="toast-message">
							 | 
						|
								          ${I18N_KEYS['staleness.banner.message'].replace('{hours}', hoursSinceUpdate.toString())}
							 | 
						|
								        </span>
							 | 
						|
								        <button class="toast-action" onclick="window.refreshData()">
							 | 
						|
								          ${I18N_KEYS['staleness.banner.action_refresh']}
							 | 
						|
								        </button>
							 | 
						|
								      </div>
							 | 
						|
								    `;
							 | 
						|
								    
							 | 
						|
								    // Add styles
							 | 
						|
								    toast.style.cssText = `
							 | 
						|
								      position: fixed;
							 | 
						|
								      bottom: 20px;
							 | 
						|
								      left: 50%;
							 | 
						|
								      transform: translateX(-50%);
							 | 
						|
								      background: #333;
							 | 
						|
								      color: white;
							 | 
						|
								      padding: 12px 16px;
							 | 
						|
								      border-radius: 8px;
							 | 
						|
								      box-shadow: 0 4px 12px rgba(0,0,0,0.3);
							 | 
						|
								      z-index: 1000;
							 | 
						|
								      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
							 | 
						|
								      display: flex;
							 | 
						|
								      align-items: center;
							 | 
						|
								      gap: 12px;
							 | 
						|
								    `;
							 | 
						|
								    
							 | 
						|
								    this.container.appendChild(toast);
							 | 
						|
								    
							 | 
						|
								    // Auto-dismiss
							 | 
						|
								    setTimeout(() => {
							 | 
						|
								      if (toast.parentElement) {
							 | 
						|
								        toast.remove();
							 | 
						|
								      }
							 | 
						|
								    }, STALE_DATA_CONFIG.bannerAutoDismissMs);
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Stale Data Manager
							 | 
						|
								 */
							 | 
						|
								class StaleDataManager {
							 | 
						|
								  private platform: 'android' | 'ios' | 'web';
							 | 
						|
								  private ux: AndroidStaleDataUX | iOSStaleDataUX | WebStaleDataUX;
							 | 
						|
								  private lastSuccessfulPoll = 0;
							 | 
						|
								  
							 | 
						|
								  constructor(platform: 'android' | 'ios' | 'web', context?: Record<string, unknown>) {
							 | 
						|
								    this.platform = platform;
							 | 
						|
								    
							 | 
						|
								    switch (platform) {
							 | 
						|
								      case 'android':
							 | 
						|
								        this.ux = new AndroidStaleDataUX(context);
							 | 
						|
								        break;
							 | 
						|
								      case 'ios':
							 | 
						|
								        this.ux = new iOSStaleDataUX(context);
							 | 
						|
								        break;
							 | 
						|
								      case 'web':
							 | 
						|
								        this.ux = new WebStaleDataUX(context);
							 | 
						|
								        break;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  updateLastSuccessfulPoll(): void {
							 | 
						|
								    this.lastSuccessfulPoll = Date.now();
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  checkAndShowStaleDataBanner(): void {
							 | 
						|
								    const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
							 | 
						|
								    
							 | 
						|
								    if (hoursSinceUpdate >= STALE_DATA_CONFIG.staleThresholdHours) {
							 | 
						|
								      this.ux.showStalenessBanner(Math.floor(hoursSinceUpdate));
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  isDataStale(): boolean {
							 | 
						|
								    const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
							 | 
						|
								    return hoursSinceUpdate >= STALE_DATA_CONFIG.staleThresholdHours;
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  isDataCritical(): boolean {
							 | 
						|
								    const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
							 | 
						|
								    return hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  getHoursSinceUpdate(): number {
							 | 
						|
								    return (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Global functions for web
							 | 
						|
								if (typeof window !== 'undefined') {
							 | 
						|
								  (window as Record<string, unknown>).refreshData = (): void => {
							 | 
						|
								    // Refreshing data from web banner (example implementation)
							 | 
						|
								  };
							 | 
						|
								  
							 | 
						|
								  (window as Record<string, unknown>).openSettings = (): void => {
							 | 
						|
								    // Opening settings from web banner (example implementation)
							 | 
						|
								  };
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export { 
							 | 
						|
								  StaleDataManager, 
							 | 
						|
								  AndroidStaleDataUX, 
							 | 
						|
								  iOSStaleDataUX, 
							 | 
						|
								  WebStaleDataUX,
							 | 
						|
								  STALE_DATA_CONFIG,
							 | 
						|
								  I18N_KEYS
							 | 
						|
								};
							 | 
						|
								
							 |