feat: Electron desktop app — hardening, IPC bridge, and dev launch fixes
- Dev/prod URL auto-detection (Vite 5173 in dev, server 3000 in prod) - Typed IPC bridge via preload (notifications, badge, window controls, updates, deep links) - Native OS notifications via NotificationController with window focus suppression - Auto-update via electron-updater with UpdateToast UI - Deep linking (backspace:// protocol) for macOS and Windows/Linux - Window state persistence (position, size, maximize across restarts) - Tray icon with graceful fallback when icon asset missing - Suppress PWA service worker polling/reloads inside Electron - Platform detection layer (isElectron, getElectronAPI) - Root workspace scripts (dev:desktop, build:desktop) - Document BACKSPACE_URL and BACKSPACE_UPDATE_URL env vars
This commit is contained in:
@@ -2,10 +2,12 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { isElectron } from '../../platform/platform';
|
||||
|
||||
export function InviteModal() {
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copiedDeepLink, setCopiedDeepLink] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const activeModal = useUIStore((s) => s.activeModal);
|
||||
@@ -19,6 +21,13 @@ export function InviteModal() {
|
||||
const isOpen = activeModal === 'invite';
|
||||
const inviteUrl = inviteCode ? `${instanceOrigin || window.location.origin}/join/${inviteCode}` : '';
|
||||
|
||||
// Deep link for Electron desktop app
|
||||
const deepLinkUrl = inviteCode
|
||||
? instanceOrigin
|
||||
? `backspace://join/${inviteCode}@${new URL(instanceOrigin).host}`
|
||||
: `backspace://join/${inviteCode}`
|
||||
: '';
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && currentSpaceId) {
|
||||
setIsLoading(true);
|
||||
@@ -52,6 +61,17 @@ export function InviteModal() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyDeepLink = async () => {
|
||||
if (!deepLinkUrl) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(deepLinkUrl);
|
||||
setCopiedDeepLink(true);
|
||||
setTimeout(() => setCopiedDeepLink(false), 2000);
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={closeModal} title="Invite Friends">
|
||||
<p className="text-txt-secondary text-sm mb-4">
|
||||
@@ -81,6 +101,26 @@ export function InviteModal() {
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
{isElectron() && deepLinkUrl && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={deepLinkUrl}
|
||||
readOnly
|
||||
className="input-standard flex-1 font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopyDeepLink}
|
||||
className={`px-4 py-2 text-sm font-medium rounded transition-colors ${
|
||||
copiedDeepLink
|
||||
? 'bg-status-online text-white'
|
||||
: 'bg-surface-elevated hover:bg-surface-elevated/80 text-txt-secondary'
|
||||
}`}
|
||||
>
|
||||
{copiedDeepLink ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user