Removed push notification banner

This commit is contained in:
Armin Ronacher 2025-06-23 15:07:49 +02:00
parent a4525bacb6
commit 79b82e4977
4 changed files with 11 additions and 262 deletions

View file

@ -19,7 +19,6 @@ import './components/session-view.js';
import './components/session-card.js';
import './components/file-browser.js';
import './components/log-viewer.js';
import './components/notification-permission-banner.js';
import './components/notification-settings.js';
import './components/notification-status.js';
@ -555,9 +554,6 @@ export class VibeTunnelApp extends LitElement {
@browser-cancel=${() => (this.showFileBrowser = false)}
></file-browser>
<!-- Notification Permission Banner -->
<notification-permission-banner></notification-permission-banner>
<!-- Notification Settings Modal -->
<notification-settings
.visible=${this.showNotificationSettings}

View file

@ -1,247 +0,0 @@
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import {
pushNotificationService,
type NotificationPreferences,
} from '../services/push-notification-service.js';
import { createLogger } from '../utils/logger.js';
const logger = createLogger('notification-permission-banner');
@customElement('notification-permission-banner')
export class NotificationPermissionBanner extends LitElement {
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
@state() private permission: NotificationPermission = 'default';
@state() private isVisible = false;
@state() private isLoading = false;
@state() private preferences: NotificationPreferences | null = null;
@state() private isDismissed = false;
private permissionChangeUnsubscribe?: () => void;
connectedCallback() {
super.connectedCallback();
this.initializeComponent();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.permissionChangeUnsubscribe) {
this.permissionChangeUnsubscribe();
}
}
private async initializeComponent(): Promise<void> {
if (!pushNotificationService.isSupported()) {
this.isVisible = false;
return;
}
this.permission = pushNotificationService.getPermission();
this.preferences = pushNotificationService.loadPreferences();
this.isDismissed = this.loadDismissedState();
// Show banner if notifications are supported but not granted and not dismissed
this.isVisible = this.shouldShowBanner();
// Listen for permission changes
this.permissionChangeUnsubscribe = pushNotificationService.onPermissionChange((permission) => {
this.permission = permission;
this.isVisible = this.shouldShowBanner();
});
}
private shouldShowBanner(): boolean {
return (
pushNotificationService.isSupported() &&
this.permission === 'default' &&
!this.isDismissed &&
(!this.preferences || !this.preferences.enabled)
);
}
private async handleEnable(): Promise<void> {
this.isLoading = true;
try {
const permission = await pushNotificationService.requestPermission();
if (permission === 'granted') {
// Subscribe to push notifications
await pushNotificationService.subscribe();
// Enable notifications in preferences
const preferences = pushNotificationService.loadPreferences();
preferences.enabled = true;
pushNotificationService.savePreferences(preferences);
// Hide banner
this.isVisible = false;
// Dispatch success event
this.dispatchEvent(
new CustomEvent('notification-enabled', {
detail: { success: true },
})
);
logger.log('notifications enabled successfully');
} else {
logger.warn('notification permission denied');
this.dispatchEvent(
new CustomEvent('notification-enabled', {
detail: { success: false, reason: 'Permission denied' },
})
);
}
} catch (error) {
logger.error('failed to enable notifications:', error);
this.dispatchEvent(
new CustomEvent('notification-enabled', {
detail: { success: false, reason: error.message },
})
);
} finally {
this.isLoading = false;
}
}
private handleDismiss(): void {
this.isVisible = false;
this.isDismissed = true;
this.saveDismissedState(true);
this.dispatchEvent(new CustomEvent('banner-dismissed'));
logger.log('notification banner dismissed');
}
private handleNotNow(): void {
this.isVisible = false;
// Don't mark as permanently dismissed for "not now"
this.dispatchEvent(new CustomEvent('banner-dismissed'));
logger.log('notification banner postponed');
}
private loadDismissedState(): boolean {
try {
const dismissed = localStorage.getItem('vibetunnel-notification-banner-dismissed');
return dismissed === 'true';
} catch (error) {
logger.error('failed to load dismissed state:', error);
return false;
}
}
private saveDismissedState(dismissed: boolean): void {
try {
localStorage.setItem('vibetunnel-notification-banner-dismissed', String(dismissed));
} catch (error) {
logger.error('failed to save dismissed state:', error);
}
}
render() {
if (!this.isVisible) {
return html``;
}
return html`
<div
class="fixed top-0 left-0 right-0 z-40 bg-gradient-to-r from-dark-accent to-dark-accent-hover border-b border-dark-border"
>
<div class="max-w-6xl mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<!-- Bell icon -->
<div class="flex-shrink-0">
<svg
class="w-5 h-5 text-dark-text"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 17h5l-3.5-3.5A7 7 0 0 1 17 10a7 7 0 0 0-14 0 7 7 0 0 1 .5 3.5L0 17h5m10 0v1a3 3 0 0 1-6 0v-1m6 0H9"
/>
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-dark-text">
Get notified about your terminal sessions
</p>
<p class="text-xs text-dark-text-secondary mt-1">
Receive notifications when processes complete, encounter errors, or require
attention
</p>
</div>
</div>
<div class="flex items-center space-x-2 ml-4">
<button
@click=${this.handleEnable}
?disabled=${this.isLoading}
class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md bg-dark-accent-hover text-dark-text hover:bg-dark-text hover:text-dark-bg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
${this.isLoading
? html`
<svg
class="animate-spin -ml-1 mr-2 h-3 w-3 text-current"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Enabling...
`
: 'Enable Notifications'}
</button>
<button
@click=${this.handleNotNow}
class="px-3 py-1.5 text-xs font-medium text-dark-text-secondary hover:text-dark-text transition-colors"
>
Not now
</button>
<button
@click=${this.handleDismiss}
class="flex-shrink-0 p-1 text-dark-text-secondary hover:text-dark-text transition-colors"
title="Dismiss permanently"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
`;
}
}

View file

@ -245,11 +245,7 @@ export class PushNotificationService {
* Check if push notifications are supported
*/
isSupported(): boolean {
return (
'serviceWorker' in navigator &&
'PushManager' in window &&
'Notification' in window
);
return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
}
/**

View file

@ -321,25 +321,29 @@ export class PtyManager extends EventEmitter {
// Check for bell character (ASCII 7) - filter out OSC sequences
if (data.includes('\x07')) {
logger.debug(`Bell data in session ${session.id}: ${JSON.stringify(data)}`);
// Count total bells and OSC-terminated bells
const totalBells = (data.match(/\x07/g) || []).length;
// Count OSC sequences terminated with bell: \x1b]...\x07
const oscMatches = data.match(/\x1b]([^\x07\x1b]|\x1b[^]])*\x07/g) || [];
const oscTerminatedBells = oscMatches.length;
// If there are more bells than OSC terminators, we have real bells
const realBells = totalBells - oscTerminatedBells;
if (realBells > 0) {
logger.debug(`Real bell(s) detected in session ${session.id}: ${realBells} bells (${oscTerminatedBells} OSC-terminated)`);
logger.debug(
`Real bell(s) detected in session ${session.id}: ${realBells} bells (${oscTerminatedBells} OSC-terminated)`
);
this.emit('bell', {
sessionInfo: session.sessionInfo,
timestamp: new Date(),
});
} else {
logger.debug(`Ignoring OSC sequence bells in session ${session.id}: ${oscTerminatedBells} OSC bells, ${realBells} real bells`);
logger.debug(
`Ignoring OSC sequence bells in session ${session.id}: ${oscTerminatedBells} OSC bells, ${realBells} real bells`
);
}
}