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
10 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
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
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 = () => {
// Refreshing data from web banner (example implementation)
};
(window as Record<string, unknown>).openSettings = () => {
// Opening settings from web banner (example implementation)
};
}
export {
StaleDataManager,
AndroidStaleDataUX,
iOSStaleDataUX,
WebStaleDataUX,
STALE_DATA_CONFIG,
I18N_KEYS
};