Skip to content

Commit 93144aa

Browse files
author
Oscar
committed
BEAP Inbox: fix duplicate popup, 3-column layout, build05
Made-with: Cursor
1 parent 8250db4 commit 93144aa

7 files changed

Lines changed: 225 additions & 59 deletions

File tree

code/apps/electron-vite-project/electron/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,7 @@ async function createWindow() {
997997
}
998998
})
999999
// Handle BEAP Inbox button from dashboard - open popup in Chrome extension
1000+
ipcMain.removeAllListeners('OPEN_BEAP_INBOX')
10001001
ipcMain.on('OPEN_BEAP_INBOX', () => {
10011002
console.log('[MAIN] 📨 BEAP Inbox requested from dashboard')
10021003

@@ -1047,6 +1048,7 @@ async function createWindow() {
10471048
}
10481049
})
10491050

1051+
ipcMain.removeAllListeners('OPEN_HANDSHAKE_REQUEST')
10501052
ipcMain.on('OPEN_HANDSHAKE_REQUEST', () => {
10511053
console.log('[MAIN] 📨 Handshake Request popup requested from dashboard')
10521054
let bounds = { x: 100, y: 100, width: 520, height: 720 }

code/apps/extension-chromium/src/background.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,8 @@ function connectToWebSocketServer(forceReconnect = false): Promise<boolean> {
10071007
const dashboardBounds = data.bounds && typeof data.bounds === 'object' ? data.bounds : null
10081008
const dashboardWindowState = typeof data.windowState === 'string' ? data.windowState : 'normal'
10091009

1010-
let url = chrome.runtime.getURL('src/popup-chat.html')
1010+
const baseUrl = chrome.runtime.getURL('src/popup-chat.html')
1011+
let url = baseUrl
10111012
const params: string[] = []
10121013
if (themeHint) params.push('t=' + encodeURIComponent(themeHint))
10131014
if (launchModeHint) params.push('launchMode=' + encodeURIComponent(launchModeHint))
@@ -1033,14 +1034,16 @@ function connectToWebSocketServer(forceReconnect = false): Promise<boolean> {
10331034
console.log('[BG] 📌 Tracking popup window id for focus-restore:', winId)
10341035
}
10351036

1036-
// Prevent duplicates: if our tracked popup already exists, update its bounds and focus
1037+
// Prevent duplicates: find ANY existing popup with our popup-chat.html URL (not just tracked id)
10371038
try {
1038-
chrome.windows.getAll({ populate: false, windowTypes: ['popup'] }, (wins) => {
1039-
const existing = dashboardPopupWindowId !== null
1040-
&& wins.find(w => w.id === dashboardPopupWindowId)
1039+
chrome.windows.getAll({ populate: true, windowTypes: ['popup'] }, (wins) => {
1040+
const existing = wins.find(w => {
1041+
const tabs = w.tabs ?? []
1042+
return tabs.some(t => t.url?.startsWith(baseUrl))
1043+
})
10411044
if (existing && existing.id) {
10421045
trackPopupId(existing.id)
1043-
// Update existing popup: set state to match dashboard, set bounds, and focus
1046+
// Focus existing popup instead of creating a new one
10441047
if (shouldMaximize) {
10451048
chrome.windows.update(existing.id, { focused: true, state: 'maximized' })
10461049
} else {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* BeapImportZone — Inline drop zone for importing BEAP messages or handshake capsules
3+
*
4+
* Matches HandshakeView's Import Capsule style. Drop .beap files or click to browse.
5+
* Uses importFromFile from the ingress pipeline.
6+
*
7+
* @version 1.0.0
8+
*/
9+
10+
import React, { useState, useCallback, useRef } from 'react'
11+
import { importFromFile } from '../../ingress/importPipeline'
12+
13+
const ACCEPTED_TYPES = '.beap,.json,.txt,.beap.json,.beap.txt'
14+
15+
interface BeapImportZoneProps {
16+
theme?: 'default' | 'dark' | 'professional'
17+
onImported?: () => void
18+
}
19+
20+
export const BeapImportZone: React.FC<BeapImportZoneProps> = ({
21+
theme = 'default',
22+
onImported,
23+
}) => {
24+
const isProfessional = theme === 'professional'
25+
const textColor = isProfessional ? '#1f2937' : 'white'
26+
const mutedColor = isProfessional ? '#6b7280' : 'rgba(255,255,255,0.6)'
27+
const borderColor = isProfessional ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.15)'
28+
const [isDragOver, setIsDragOver] = useState(false)
29+
const [importing, setImporting] = useState(false)
30+
const [error, setError] = useState<string | null>(null)
31+
const fileInputRef = useRef<HTMLInputElement>(null)
32+
33+
const handleDrop = useCallback(
34+
async (e: React.DragEvent) => {
35+
e.preventDefault()
36+
e.stopPropagation()
37+
setIsDragOver(false)
38+
const file = e.dataTransfer.files?.[0]
39+
if (!file) return
40+
setImporting(true)
41+
setError(null)
42+
try {
43+
const result = await importFromFile(file)
44+
if (result.success) {
45+
onImported?.()
46+
} else {
47+
setError(result.error ?? 'Import failed')
48+
}
49+
} catch (err) {
50+
setError(err instanceof Error ? err.message : 'Import failed')
51+
} finally {
52+
setImporting(false)
53+
}
54+
},
55+
[onImported],
56+
)
57+
58+
const handleFileInputChange = useCallback(
59+
async (e: React.ChangeEvent<HTMLInputElement>) => {
60+
const file = e.target.files?.[0]
61+
if (!file) return
62+
e.target.value = ''
63+
setImporting(true)
64+
setError(null)
65+
try {
66+
const result = await importFromFile(file)
67+
if (result.success) {
68+
onImported?.()
69+
} else {
70+
setError(result.error ?? 'Import failed')
71+
}
72+
} catch (err) {
73+
setError(err instanceof Error ? err.message : 'Import failed')
74+
} finally {
75+
setImporting(false)
76+
}
77+
},
78+
[onImported],
79+
)
80+
81+
return (
82+
<div style={{ marginTop: '12px' }}>
83+
<input
84+
ref={fileInputRef}
85+
type="file"
86+
accept={ACCEPTED_TYPES}
87+
onChange={handleFileInputChange}
88+
style={{ display: 'none' }}
89+
/>
90+
<div
91+
onDragOver={(e) => {
92+
e.preventDefault()
93+
e.stopPropagation()
94+
setIsDragOver(true)
95+
}}
96+
onDragLeave={(e) => {
97+
e.preventDefault()
98+
e.stopPropagation()
99+
setIsDragOver(false)
100+
}}
101+
onDrop={handleDrop}
102+
onClick={() => !importing && fileInputRef.current?.click()}
103+
style={{
104+
border: `2px dashed ${isDragOver ? (isProfessional ? '#7c3aed' : '#a78bfa') : borderColor}`,
105+
borderRadius: '10px',
106+
padding: '24px 16px',
107+
textAlign: 'center',
108+
background: isDragOver
109+
? isProfessional
110+
? 'rgba(124,58,237,0.06)'
111+
: 'rgba(167,139,250,0.08)'
112+
: isProfessional
113+
? 'rgba(0,0,0,0.02)'
114+
: 'rgba(255,255,255,0.04)',
115+
cursor: importing ? 'not-allowed' : 'pointer',
116+
transition: 'all 0.2s ease',
117+
}}
118+
>
119+
<div style={{ fontSize: '28px', marginBottom: '8px', opacity: 0.7 }}>
120+
📦
121+
</div>
122+
<div
123+
style={{
124+
fontSize: '12px',
125+
fontWeight: 600,
126+
color: textColor,
127+
marginBottom: '4px',
128+
}}
129+
>
130+
{importing ? 'Importing…' : 'Drop a .beap file here or click to browse'}
131+
</div>
132+
<div style={{ fontSize: '11px', color: mutedColor }}>
133+
BEAP messages or handshake requests
134+
</div>
135+
</div>
136+
{error && (
137+
<div
138+
style={{
139+
marginTop: '8px',
140+
fontSize: '11px',
141+
color: '#ef4444',
142+
padding: '6px 10px',
143+
background: 'rgba(239,68,68,0.1)',
144+
borderRadius: '6px',
145+
}}
146+
>
147+
{error}
148+
</div>
149+
)}
150+
</div>
151+
)
152+
}

code/apps/extension-chromium/src/beap-messages/components/BeapInboxView.tsx

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ import React, {
5151
} from 'react'
5252
import { BeapInboxSidebar } from './BeapInboxSidebar'
5353
import { BeapMessageDetailPanel } from './BeapMessageDetailPanel'
54+
import { BeapImportZone } from './BeapImportZone'
5455
import type { BeapMessageDetailPanelHandle } from './BeapMessageDetailPanel'
5556
import { useBeapInboxStore } from '../useBeapInboxStore'
57+
import { useBeapMessagesStore } from '../useBeapMessagesStore'
5658
import { useInboxKeyboardNav } from '../hooks/useInboxKeyboardNav'
5759
import { useMediaQuery, NARROW_VIEWPORT } from '../hooks/useMediaQuery'
5860
import { usePendingP2PBeapIngestion } from '../hooks/usePendingP2PBeapIngestion'
@@ -202,6 +204,7 @@ export const BeapInboxView = React.forwardRef<BeapInboxViewHandle, BeapInboxView
202204
// Store
203205
const getInboxMessages = useBeapInboxStore((s) => s.getInboxMessages)
204206
const selectedMessageId = useBeapInboxStore((s) => s.selectedMessageId)
207+
const pendingCount = useBeapMessagesStore((s) => s.getPendingVerificationMessages().length)
205208

206209
// Local state
207210
const [isLoading, setIsLoading] = useState(true)
@@ -285,21 +288,21 @@ export const BeapInboxView = React.forwardRef<BeapInboxViewHandle, BeapInboxView
285288
{isLoading ? (
286289
<InboxSkeleton isProfessional={isProfessional} />
287290
) : (
288-
/* ── Single-message split view (sidebar collapsible for responsive layout) ── */
291+
/* ── 3-column layout (Handshake-style): list | details | import ── */
289292
<div
290293
style={{
291294
flex: 1,
292-
display: 'flex',
293-
flexDirection: 'row',
295+
display: 'grid',
296+
gridTemplateColumns: selectedMessageId
297+
? (sidebarCollapsed ? '40px 1fr' : '220px 1fr')
298+
: (sidebarCollapsed ? '40px 1fr 280px' : '220px 1fr 280px'),
294299
overflow: 'hidden',
295300
minHeight: 0,
296301
}}
297302
>
298303
{/* Left: sidebar list (collapsible) */}
299304
<div
300305
style={{
301-
width: sidebarCollapsed ? '40px' : '220px',
302-
minWidth: sidebarCollapsed ? '40px' : '220px',
303306
flexShrink: 0,
304307
borderRight: `1px solid ${borderColor}`,
305308
display: 'flex',
@@ -353,8 +356,8 @@ export const BeapInboxView = React.forwardRef<BeapInboxViewHandle, BeapInboxView
353356
)}
354357
</div>
355358

356-
{/* Right: detail panel */}
357-
<div style={{ flex: 1, display: 'flex', overflow: 'hidden', minHeight: 0 }}>
359+
{/* Center: detail panel */}
360+
<div style={{ flex: 1, display: 'flex', overflow: 'hidden', minHeight: 0, minWidth: 0 }}>
358361
{selectedMessageId ? (
359362
<BeapMessageDetailPanel
360363
ref={detailPanelRef}
@@ -372,6 +375,47 @@ export const BeapInboxView = React.forwardRef<BeapInboxViewHandle, BeapInboxView
372375
/>
373376
)}
374377
</div>
378+
379+
{/* Right: Pending + Import (only when no message selected, like Handshake) */}
380+
{!selectedMessageId && (
381+
<div
382+
style={{
383+
borderLeft: `1px solid ${borderColor}`,
384+
display: 'flex',
385+
flexDirection: 'column',
386+
overflow: 'hidden',
387+
minWidth: 0,
388+
}}
389+
>
390+
<div
391+
style={{
392+
padding: '14px 12px',
393+
borderBottom: `1px solid ${borderColor}`,
394+
fontSize: '13px',
395+
fontWeight: 700,
396+
color: textColor,
397+
}}
398+
>
399+
Pending ({pendingCount})
400+
</div>
401+
<div style={{ flex: 1, overflowY: 'auto', padding: '8px' }}>
402+
{pendingCount === 0 ? (
403+
<div
404+
style={{
405+
padding: '20px 12px',
406+
textAlign: 'center',
407+
color: mutedColor,
408+
fontSize: '11px',
409+
lineHeight: 1.6,
410+
}}
411+
>
412+
No pending verification.
413+
</div>
414+
) : null}
415+
<BeapImportZone theme={theme} onImported={() => {}} />
416+
</div>
417+
</div>
418+
)}
375419
</div>
376420
)}
377421
</div>

code/apps/extension-chromium/src/beap-messages/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ export {
6262
BeapMessageDetailPanel,
6363
BeapBulkInbox,
6464
BeapReplyComposer,
65+
BeapInboxView,
6566
} from './components'
66-
export type { BeapMessageDetailPanelProps, BeapMessageDetailPanelHandle } from './components'
67+
export type { BeapMessageDetailPanelProps, BeapMessageDetailPanelHandle, BeapInboxViewProps, BeapInboxViewHandle } from './components'
6768
export type { BeapBulkInboxProps, BeapBulkInboxHandle } from './components'
6869
export type { BeapReplyComposerProps } from './components'
6970

code/apps/extension-chromium/src/popup-chat.tsx

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { HandshakeRequestForm } from './handshake/components/HandshakeRequestFor
3030
import { SendHandshakeDelivery } from './handshake/components/SendHandshakeDelivery'
3131
import { useHandshakes } from './handshake/useHandshakes'
3232
import { sendViaHandshakeRefresh } from './beap-builder/handshakeRefresh'
33-
import { RecipientModeSwitch, RecipientHandshakeSelect, DeliveryMethodPanel, executeDeliveryAction } from './beap-messages'
33+
import { RecipientModeSwitch, RecipientHandshakeSelect, DeliveryMethodPanel, executeDeliveryAction, BeapInboxView } from './beap-messages'
3434
import { useBeapInboxStore } from './beap-messages/useBeapInboxStore'
3535
import type { RecipientMode, SelectedHandshakeRecipient, SelectedRecipient, DeliveryMethod, BeapPackageConfig } from './beap-messages'
3636
import {
@@ -828,51 +828,15 @@ function PopupChatApp() {
828828
`}</style>
829829

830830
{/* ========================================== */}
831-
{/* INBOX VIEW - Placeholder (same as docked) */}
831+
{/* INBOX VIEW - Full 3-column layout (Handshake-style) */}
832832
{/* ========================================== */}
833833
{beapSubmode === 'inbox' && (
834-
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', position: 'relative' }}>
835-
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 20px', textAlign: 'center' }}>
836-
<span style={{ fontSize: '48px', marginBottom: '16px' }}>📥</span>
837-
<div style={{ fontSize: '18px', fontWeight: '600', color: textColor, marginBottom: '8px' }}>BEAP Inbox</div>
838-
<div style={{ fontSize: '13px', color: mutedColor, maxWidth: '280px' }}>
839-
Received BEAP™ packages will appear here. All packages are verified before display.
840-
</div>
841-
</div>
842-
{/* FAB - New Draft Button */}
843-
<button
844-
onClick={() => setBeapSubmode('draft')}
845-
title="New Draft"
846-
style={{
847-
position: 'absolute',
848-
bottom: '20px',
849-
right: '20px',
850-
width: '48px',
851-
height: '48px',
852-
borderRadius: '50%',
853-
border: 'none',
854-
background: theme === 'pro' ? 'rgba(255,255,255,0.9)' : theme === 'dark' ? '#3b82f6' : '#9333ea',
855-
color: theme === 'pro' ? '#9333ea' : 'white',
856-
fontSize: '24px',
857-
fontWeight: '300',
858-
cursor: 'pointer',
859-
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
860-
display: 'flex',
861-
alignItems: 'center',
862-
justifyContent: 'center',
863-
transition: 'transform 0.15s, box-shadow 0.15s'
864-
}}
865-
onMouseEnter={(e) => {
866-
e.currentTarget.style.transform = 'scale(1.08)'
867-
e.currentTarget.style.boxShadow = '0 6px 16px rgba(0,0,0,0.25)'
868-
}}
869-
onMouseLeave={(e) => {
870-
e.currentTarget.style.transform = 'scale(1)'
871-
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)'
872-
}}
873-
>
874-
+
875-
</button>
834+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
835+
<BeapInboxView
836+
theme={isStandard ? 'professional' : isPro ? 'default' : 'dark'}
837+
onNavigateToDraft={() => setBeapSubmode('draft')}
838+
onNavigateToWRGuard={() => setDockedWorkspace('wrguard')}
839+
/>
876840
</div>
877841
)}
878842

code/apps/extension-chromium/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default defineConfig({
6969
},
7070
},
7171
build: {
72-
outDir: 'build02',
72+
outDir: 'build05',
7373
// Disable Vite's modulepreload polyfill — it references `document` and
7474
// `window` which don't exist in Chrome extension service workers (MV3).
7575
modulePreload: false,

0 commit comments

Comments
 (0)