feat: implement Phase 5 - permission flow and drag detection for data sovereignty
- Add VisibilityChangeModal for confirming visibility changes - Add VisibilityChangeManager to handle events and drag detection - GoogleItem shapes now dispatch visibility change events on badge click - Support both local->shared and shared->local transitions - Auto-detect when GoogleItems are dragged outside PrivateWorkspace - Session storage for "don't ask again" preference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a9f262d591
commit
af669beac2
|
|
@ -0,0 +1,200 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { useEditor, TLShapeId } from 'tldraw'
|
||||||
|
import { VisibilityChangeModal, shouldSkipVisibilityPrompt, setSkipVisibilityPrompt } from './VisibilityChangeModal'
|
||||||
|
import { updateItemVisibility, ItemVisibility } from '../shapes/GoogleItemShapeUtil'
|
||||||
|
import { findPrivateWorkspace, isShapeInPrivateWorkspace } from '../shapes/PrivateWorkspaceShapeUtil'
|
||||||
|
|
||||||
|
interface PendingChange {
|
||||||
|
shapeId: TLShapeId
|
||||||
|
currentVisibility: ItemVisibility
|
||||||
|
newVisibility: ItemVisibility
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VisibilityChangeManager() {
|
||||||
|
const editor = useEditor()
|
||||||
|
const [pendingChange, setPendingChange] = useState<PendingChange | null>(null)
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false)
|
||||||
|
|
||||||
|
// Detect dark mode
|
||||||
|
useEffect(() => {
|
||||||
|
const checkDarkMode = () => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
}
|
||||||
|
checkDarkMode()
|
||||||
|
|
||||||
|
// Watch for class changes
|
||||||
|
const observer = new MutationObserver(checkDarkMode)
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Handle visibility change requests from GoogleItem shapes
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChangeRequest = (event: CustomEvent<{
|
||||||
|
shapeId: TLShapeId
|
||||||
|
currentVisibility: ItemVisibility
|
||||||
|
newVisibility: ItemVisibility
|
||||||
|
title: string
|
||||||
|
}>) => {
|
||||||
|
const { shapeId, currentVisibility, newVisibility, title } = event.detail
|
||||||
|
|
||||||
|
// Check if user has opted to skip prompts
|
||||||
|
if (shouldSkipVisibilityPrompt()) {
|
||||||
|
// Apply change immediately
|
||||||
|
updateItemVisibility(editor, shapeId, newVisibility)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show confirmation modal
|
||||||
|
setPendingChange({
|
||||||
|
shapeId,
|
||||||
|
currentVisibility,
|
||||||
|
newVisibility,
|
||||||
|
title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('request-visibility-change', handleVisibilityChangeRequest as EventListener)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('request-visibility-change', handleVisibilityChangeRequest as EventListener)
|
||||||
|
}
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
// Handle drag detection - check when items leave the Private Workspace
|
||||||
|
// Track GoogleItem positions to detect when they move outside workspace
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return
|
||||||
|
|
||||||
|
// Track which GoogleItems were inside workspace at start of drag
|
||||||
|
const wasInWorkspace = new Map<TLShapeId, boolean>()
|
||||||
|
let isDragging = false
|
||||||
|
|
||||||
|
// Record initial positions when pointer goes down
|
||||||
|
const handlePointerDown = () => {
|
||||||
|
const workspace = findPrivateWorkspace(editor)
|
||||||
|
if (!workspace) return
|
||||||
|
|
||||||
|
const selectedIds = editor.getSelectedShapeIds()
|
||||||
|
wasInWorkspace.clear()
|
||||||
|
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
const shape = editor.getShape(id)
|
||||||
|
if (shape && shape.type === 'GoogleItem') {
|
||||||
|
const inWorkspace = isShapeInPrivateWorkspace(editor, id, workspace.id)
|
||||||
|
wasInWorkspace.set(id, inWorkspace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDragging = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for visibility changes when pointer goes up
|
||||||
|
const handlePointerUp = () => {
|
||||||
|
if (!isDragging || wasInWorkspace.size === 0) {
|
||||||
|
isDragging = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = findPrivateWorkspace(editor)
|
||||||
|
if (!workspace) {
|
||||||
|
wasInWorkspace.clear()
|
||||||
|
isDragging = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each tracked shape
|
||||||
|
wasInWorkspace.forEach((wasIn, id) => {
|
||||||
|
const shape = editor.getShape(id)
|
||||||
|
if (!shape || shape.type !== 'GoogleItem') return
|
||||||
|
|
||||||
|
const isNowIn = isShapeInPrivateWorkspace(editor, id, workspace.id)
|
||||||
|
|
||||||
|
// If shape was in workspace and is now outside, trigger visibility change
|
||||||
|
if (wasIn && !isNowIn) {
|
||||||
|
const itemShape = shape as any // GoogleItem shape
|
||||||
|
if (itemShape.props.visibility === 'local') {
|
||||||
|
// Trigger visibility change request
|
||||||
|
window.dispatchEvent(new CustomEvent('request-visibility-change', {
|
||||||
|
detail: {
|
||||||
|
shapeId: id,
|
||||||
|
currentVisibility: 'local',
|
||||||
|
newVisibility: 'shared',
|
||||||
|
title: itemShape.props.title || 'Untitled',
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wasInWorkspace.clear()
|
||||||
|
isDragging = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use DOM events for pointer tracking (more reliable with tldraw)
|
||||||
|
const canvas = document.querySelector('.tl-canvas')
|
||||||
|
if (canvas) {
|
||||||
|
canvas.addEventListener('pointerdown', handlePointerDown)
|
||||||
|
canvas.addEventListener('pointerup', handlePointerUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (canvas) {
|
||||||
|
canvas.removeEventListener('pointerdown', handlePointerDown)
|
||||||
|
canvas.removeEventListener('pointerup', handlePointerUp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
// Handle modal confirmation
|
||||||
|
const handleConfirm = useCallback((dontAskAgain: boolean) => {
|
||||||
|
if (!pendingChange) return
|
||||||
|
|
||||||
|
// Update the shape visibility
|
||||||
|
updateItemVisibility(editor, pendingChange.shapeId, pendingChange.newVisibility)
|
||||||
|
|
||||||
|
// Save preference if requested
|
||||||
|
if (dontAskAgain) {
|
||||||
|
setSkipVisibilityPrompt(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPendingChange(null)
|
||||||
|
}, [editor, pendingChange])
|
||||||
|
|
||||||
|
// Handle modal cancellation
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
if (!pendingChange) return
|
||||||
|
|
||||||
|
// If this was triggered by drag, move the shape back inside the workspace
|
||||||
|
const workspace = findPrivateWorkspace(editor)
|
||||||
|
if (workspace) {
|
||||||
|
const shape = editor.getShape(pendingChange.shapeId)
|
||||||
|
if (shape) {
|
||||||
|
// Move shape back inside workspace bounds
|
||||||
|
editor.updateShape({
|
||||||
|
id: pendingChange.shapeId,
|
||||||
|
type: shape.type,
|
||||||
|
x: workspace.x + 20,
|
||||||
|
y: workspace.y + 60,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPendingChange(null)
|
||||||
|
}, [editor, pendingChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VisibilityChangeModal
|
||||||
|
isOpen={pendingChange !== null}
|
||||||
|
itemTitle={pendingChange?.title || ''}
|
||||||
|
currentVisibility={pendingChange?.currentVisibility || 'local'}
|
||||||
|
newVisibility={pendingChange?.newVisibility || 'shared'}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
interface VisibilityChangeModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
itemTitle: string
|
||||||
|
currentVisibility: 'local' | 'shared'
|
||||||
|
newVisibility: 'local' | 'shared'
|
||||||
|
onConfirm: (dontAskAgain: boolean) => void
|
||||||
|
onCancel: () => void
|
||||||
|
isDarkMode: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VisibilityChangeModal({
|
||||||
|
isOpen,
|
||||||
|
itemTitle,
|
||||||
|
currentVisibility,
|
||||||
|
newVisibility,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
isDarkMode,
|
||||||
|
}: VisibilityChangeModalProps) {
|
||||||
|
const [dontAskAgain, setDontAskAgain] = useState(false)
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Handle escape key
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return
|
||||||
|
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleEscape)
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape)
|
||||||
|
}, [isOpen, onCancel])
|
||||||
|
|
||||||
|
// Dark mode colors
|
||||||
|
const colors = isDarkMode ? {
|
||||||
|
bg: '#1f2937',
|
||||||
|
cardBg: '#252525',
|
||||||
|
cardBorder: '#404040',
|
||||||
|
text: '#e4e4e7',
|
||||||
|
textMuted: '#a1a1aa',
|
||||||
|
textHeading: '#f4f4f5',
|
||||||
|
warningBg: 'rgba(251, 191, 36, 0.15)',
|
||||||
|
warningBorder: 'rgba(251, 191, 36, 0.3)',
|
||||||
|
warningText: '#fbbf24',
|
||||||
|
btnPrimaryBg: '#6366f1',
|
||||||
|
btnPrimaryText: '#ffffff',
|
||||||
|
btnSecondaryBg: '#333333',
|
||||||
|
btnSecondaryText: '#e4e4e4',
|
||||||
|
checkboxBg: '#333333',
|
||||||
|
checkboxBorder: '#555555',
|
||||||
|
localColor: '#6366f1',
|
||||||
|
sharedColor: '#22c55e',
|
||||||
|
} : {
|
||||||
|
bg: '#ffffff',
|
||||||
|
cardBg: '#f9fafb',
|
||||||
|
cardBorder: '#e5e7eb',
|
||||||
|
text: '#374151',
|
||||||
|
textMuted: '#6b7280',
|
||||||
|
textHeading: '#1f2937',
|
||||||
|
warningBg: 'rgba(251, 191, 36, 0.1)',
|
||||||
|
warningBorder: 'rgba(251, 191, 36, 0.3)',
|
||||||
|
warningText: '#92400e',
|
||||||
|
btnPrimaryBg: '#6366f1',
|
||||||
|
btnPrimaryText: '#ffffff',
|
||||||
|
btnSecondaryBg: '#f3f4f6',
|
||||||
|
btnSecondaryText: '#374151',
|
||||||
|
checkboxBg: '#ffffff',
|
||||||
|
checkboxBorder: '#d1d5db',
|
||||||
|
localColor: '#6366f1',
|
||||||
|
sharedColor: '#22c55e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
const isSharing = currentVisibility === 'local' && newVisibility === 'shared'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 100010,
|
||||||
|
}}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={modalRef}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.bg,
|
||||||
|
borderRadius: '12px',
|
||||||
|
width: '90%',
|
||||||
|
maxWidth: '420px',
|
||||||
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||||
|
border: `1px solid ${colors.cardBorder}`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '20px 24px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '24px' }}>
|
||||||
|
{isSharing ? '⚠️' : '🔒'}
|
||||||
|
</span>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.textHeading,
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSharing ? 'Change Visibility?' : 'Make Private?'}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ padding: '0 24px 20px' }}>
|
||||||
|
<p style={{ fontSize: '14px', color: colors.text, margin: '0 0 16px 0', lineHeight: '1.5' }}>
|
||||||
|
{isSharing
|
||||||
|
? "You're about to make this item visible to others:"
|
||||||
|
: "You're about to make this item private:"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Item preview */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
padding: '12px 14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: `1px solid ${colors.cardBorder}`,
|
||||||
|
marginBottom: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '18px' }}>📄</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: colors.text,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{itemTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current vs New state */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
fontSize: '13px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '6px 10px',
|
||||||
|
backgroundColor: isSharing ? `${colors.localColor}20` : `${colors.sharedColor}20`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
color: isSharing ? colors.localColor : colors.sharedColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{isSharing ? '🔒' : '🌐'}</span>
|
||||||
|
<span>{isSharing ? 'Private' : 'Shared'}</span>
|
||||||
|
</div>
|
||||||
|
<span style={{ color: colors.textMuted }}>→</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '6px 10px',
|
||||||
|
backgroundColor: isSharing ? `${colors.sharedColor}20` : `${colors.localColor}20`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
color: isSharing ? colors.sharedColor : colors.localColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{isSharing ? '🌐' : '🔒'}</span>
|
||||||
|
<span>{isSharing ? 'Shared' : 'Private'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning for sharing */}
|
||||||
|
{isSharing && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.warningBg,
|
||||||
|
border: `1px solid ${colors.warningBorder}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '12px 14px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: colors.warningText,
|
||||||
|
margin: 0,
|
||||||
|
lineHeight: '1.5',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>Note:</strong> Shared items will be visible to all collaborators
|
||||||
|
on this board and may be uploaded to cloud storage.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info for making private */}
|
||||||
|
{!isSharing && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: isDarkMode ? 'rgba(99, 102, 241, 0.15)' : 'rgba(99, 102, 241, 0.1)',
|
||||||
|
border: `1px solid ${isDarkMode ? 'rgba(99, 102, 241, 0.3)' : 'rgba(99, 102, 241, 0.3)'}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '12px 14px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: isDarkMode ? '#a5b4fc' : '#4f46e5',
|
||||||
|
margin: 0,
|
||||||
|
lineHeight: '1.5',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>Note:</strong> Private items are only visible to you and remain
|
||||||
|
encrypted in your browser. Other collaborators won't be able to see this item.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Don't ask again checkbox */}
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: colors.textMuted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={dontAskAgain}
|
||||||
|
onChange={(e) => setDontAskAgain(e.target.checked)}
|
||||||
|
style={{
|
||||||
|
width: '16px',
|
||||||
|
height: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Don't ask again for this session
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: '10px',
|
||||||
|
padding: '16px 24px',
|
||||||
|
borderTop: `1px solid ${colors.cardBorder}`,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{
|
||||||
|
padding: '10px 18px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: `1px solid ${colors.cardBorder}`,
|
||||||
|
backgroundColor: colors.btnSecondaryBg,
|
||||||
|
color: colors.btnSecondaryText,
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '500',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onConfirm(dontAskAgain)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 18px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: isSharing ? colors.sharedColor : colors.localColor,
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '500',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{isSharing ? '🌐' : '🔒'}</span>
|
||||||
|
{isSharing ? 'Make Shared' : 'Make Private'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session storage key for "don't ask again" preference
|
||||||
|
const DONT_ASK_KEY = 'visibility-change-dont-ask'
|
||||||
|
|
||||||
|
export function shouldSkipVisibilityPrompt(): boolean {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(DONT_ASK_KEY) === 'true'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSkipVisibilityPrompt(skip: boolean): void {
|
||||||
|
try {
|
||||||
|
if (skip) {
|
||||||
|
sessionStorage.setItem(DONT_ASK_KEY, 'true')
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem(DONT_ASK_KEY)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore storage errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,7 @@ import { MycelialIntelligenceShape } from "@/shapes/MycelialIntelligenceShapeUti
|
||||||
import { PrivateWorkspaceShape } from "@/shapes/PrivateWorkspaceShapeUtil"
|
import { PrivateWorkspaceShape } from "@/shapes/PrivateWorkspaceShapeUtil"
|
||||||
import { PrivateWorkspaceTool } from "@/tools/PrivateWorkspaceTool"
|
import { PrivateWorkspaceTool } from "@/tools/PrivateWorkspaceTool"
|
||||||
import { PrivateWorkspaceManager } from "@/components/PrivateWorkspaceManager"
|
import { PrivateWorkspaceManager } from "@/components/PrivateWorkspaceManager"
|
||||||
|
import { VisibilityChangeManager } from "@/components/VisibilityChangeManager"
|
||||||
import { GoogleItemShape } from "@/shapes/GoogleItemShapeUtil"
|
import { GoogleItemShape } from "@/shapes/GoogleItemShapeUtil"
|
||||||
import { GoogleItemTool } from "@/tools/GoogleItemTool"
|
import { GoogleItemTool } from "@/tools/GoogleItemTool"
|
||||||
import {
|
import {
|
||||||
|
|
@ -1111,6 +1112,7 @@ export function Board() {
|
||||||
>
|
>
|
||||||
<CmdK />
|
<CmdK />
|
||||||
<PrivateWorkspaceManager />
|
<PrivateWorkspaceManager />
|
||||||
|
<VisibilityChangeManager />
|
||||||
</Tldraw>
|
</Tldraw>
|
||||||
</div>
|
</div>
|
||||||
</AutomergeHandleProvider>
|
</AutomergeHandleProvider>
|
||||||
|
|
|
||||||
|
|
@ -107,13 +107,14 @@ export class GoogleItemShape extends BaseBoxShapeUtil<IGoogleItemShape> {
|
||||||
return date.toLocaleDateString()
|
return date.toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMakeShared = () => {
|
const handleToggleVisibility = () => {
|
||||||
|
const newVisibility = isLocal ? 'shared' : 'local'
|
||||||
// Dispatch event for Phase 5 permission flow
|
// Dispatch event for Phase 5 permission flow
|
||||||
window.dispatchEvent(new CustomEvent('request-visibility-change', {
|
window.dispatchEvent(new CustomEvent('request-visibility-change', {
|
||||||
detail: {
|
detail: {
|
||||||
shapeId: shape.id,
|
shapeId: shape.id,
|
||||||
currentVisibility: shape.props.visibility,
|
currentVisibility: shape.props.visibility,
|
||||||
newVisibility: 'shared',
|
newVisibility,
|
||||||
title: shape.props.title,
|
title: shape.props.title,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
@ -182,11 +183,11 @@ export class GoogleItemShape extends BaseBoxShapeUtil<IGoogleItemShape> {
|
||||||
}}
|
}}
|
||||||
title={isLocal
|
title={isLocal
|
||||||
? 'Private - Only you can see (click to share)'
|
? 'Private - Only you can see (click to share)'
|
||||||
: 'Shared - Visible to collaborators'
|
: 'Shared - Visible to collaborators (click to make private)'
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (isLocal) handleMakeShared()
|
handleToggleVisibility()
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue