mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Removed push notification banner
This commit is contained in:
parent
a4525bacb6
commit
79b82e4977
4 changed files with 11 additions and 262 deletions
|
|
@ -19,7 +19,6 @@ import './components/session-view.js';
|
||||||
import './components/session-card.js';
|
import './components/session-card.js';
|
||||||
import './components/file-browser.js';
|
import './components/file-browser.js';
|
||||||
import './components/log-viewer.js';
|
import './components/log-viewer.js';
|
||||||
import './components/notification-permission-banner.js';
|
|
||||||
import './components/notification-settings.js';
|
import './components/notification-settings.js';
|
||||||
import './components/notification-status.js';
|
import './components/notification-status.js';
|
||||||
|
|
||||||
|
|
@ -555,9 +554,6 @@ export class VibeTunnelApp extends LitElement {
|
||||||
@browser-cancel=${() => (this.showFileBrowser = false)}
|
@browser-cancel=${() => (this.showFileBrowser = false)}
|
||||||
></file-browser>
|
></file-browser>
|
||||||
|
|
||||||
<!-- Notification Permission Banner -->
|
|
||||||
<notification-permission-banner></notification-permission-banner>
|
|
||||||
|
|
||||||
<!-- Notification Settings Modal -->
|
<!-- Notification Settings Modal -->
|
||||||
<notification-settings
|
<notification-settings
|
||||||
.visible=${this.showNotificationSettings}
|
.visible=${this.showNotificationSettings}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -245,11 +245,7 @@ export class PushNotificationService {
|
||||||
* Check if push notifications are supported
|
* Check if push notifications are supported
|
||||||
*/
|
*/
|
||||||
isSupported(): boolean {
|
isSupported(): boolean {
|
||||||
return (
|
return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
|
||||||
'serviceWorker' in navigator &&
|
|
||||||
'PushManager' in window &&
|
|
||||||
'Notification' in window
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -321,25 +321,29 @@ export class PtyManager extends EventEmitter {
|
||||||
// Check for bell character (ASCII 7) - filter out OSC sequences
|
// Check for bell character (ASCII 7) - filter out OSC sequences
|
||||||
if (data.includes('\x07')) {
|
if (data.includes('\x07')) {
|
||||||
logger.debug(`Bell data in session ${session.id}: ${JSON.stringify(data)}`);
|
logger.debug(`Bell data in session ${session.id}: ${JSON.stringify(data)}`);
|
||||||
|
|
||||||
// Count total bells and OSC-terminated bells
|
// Count total bells and OSC-terminated bells
|
||||||
const totalBells = (data.match(/\x07/g) || []).length;
|
const totalBells = (data.match(/\x07/g) || []).length;
|
||||||
|
|
||||||
// Count OSC sequences terminated with bell: \x1b]...\x07
|
// Count OSC sequences terminated with bell: \x1b]...\x07
|
||||||
const oscMatches = data.match(/\x1b]([^\x07\x1b]|\x1b[^]])*\x07/g) || [];
|
const oscMatches = data.match(/\x1b]([^\x07\x1b]|\x1b[^]])*\x07/g) || [];
|
||||||
const oscTerminatedBells = oscMatches.length;
|
const oscTerminatedBells = oscMatches.length;
|
||||||
|
|
||||||
// If there are more bells than OSC terminators, we have real bells
|
// If there are more bells than OSC terminators, we have real bells
|
||||||
const realBells = totalBells - oscTerminatedBells;
|
const realBells = totalBells - oscTerminatedBells;
|
||||||
|
|
||||||
if (realBells > 0) {
|
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', {
|
this.emit('bell', {
|
||||||
sessionInfo: session.sessionInfo,
|
sessionInfo: session.sessionInfo,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
} else {
|
} 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`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue