Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 105 additions & 2 deletions components/compose/compose-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
TrashIcon,
EyeIcon,
EyeSlashIcon,
GlobeAltIcon,
ChevronDownIcon,
} from '@heroicons/react/24/outline'
import { useAppStore, ThreadPost } from '@/lib/store'
import { Button } from '@/components/ui/button'
Expand Down Expand Up @@ -38,6 +40,99 @@ import {

const CHARACTER_LIMIT = 500

// Supported languages for post creation (ISO 639-1 codes)
const SUPPORTED_LANGUAGES = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Spanish' },
{ code: 'pt', name: 'Portuguese' },
{ code: 'fr', name: 'French' },
{ code: 'de', name: 'German' },
{ code: 'it', name: 'Italian' },
{ code: 'nl', name: 'Dutch' },
{ code: 'ru', name: 'Russian' },
{ code: 'zh', name: 'Chinese' },
{ code: 'ja', name: 'Japanese' },
{ code: 'ko', name: 'Korean' },
{ code: 'ar', name: 'Arabic' },
{ code: 'hi', name: 'Hindi' },
{ code: 'tr', name: 'Turkish' },
{ code: 'pl', name: 'Polish' },
{ code: 'uk', name: 'Ukrainian' },
{ code: 'vi', name: 'Vietnamese' },
{ code: 'th', name: 'Thai' },
{ code: 'id', name: 'Indonesian' },
{ code: 'sv', name: 'Swedish' },
] as const

// Language selector component
function LanguageSelector({
value,
onChange,
disabled = false,
}: {
value: string
onChange: (code: string) => void
disabled?: boolean
}) {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)

// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])

const selectedLanguage = SUPPORTED_LANGUAGES.find(l => l.code === value) || SUPPORTED_LANGUAGES[0]

return (
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-colors ${
disabled
? 'text-gray-400 cursor-not-allowed'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
title="Select post language"
>
<GlobeAltIcon className="w-4 h-4" />
<span>{selectedLanguage.name}</span>
<ChevronDownIcon className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>

{isOpen && (
<div className="absolute bottom-full left-0 mb-1 w-40 max-h-60 overflow-y-auto bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50">
{SUPPORTED_LANGUAGES.map((lang) => (
<button
key={lang.code}
type="button"
onClick={() => {
onChange(lang.code)
setIsOpen(false)
}}
className={`w-full px-3 py-2 text-left text-sm transition-colors ${
lang.code === value
? 'bg-yappr-50 dark:bg-yappr-900/30 text-yappr-600 dark:text-yappr-400 font-medium'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
{lang.name}
</button>
))}
</div>
Comment on lines +111 to +130

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Escape from the language menu discards the entire draft

The custom popup has no Escape handling or nested dismissable/menu primitive. Radix Dialog.Content uses a DismissableLayer whose capture-phase Escape handler calls onDismiss and then onOpenChange(false). Because the root passes setComposeOpen directly, the store replaces threadPosts when this happens. Pressing Escape while the language list is open therefore closes the entire composer and clears the draft instead of dismissing only the popup. Use an accessible Select or DropdownMenu primitive, or coordinate the popup state with Dialog.Content's onEscapeKeyDown so the first Escape closes only the language list.

source: ['codex']

)}
</div>
)
}

// Formatting button component
function FormatButton({
onClick,
Expand Down Expand Up @@ -438,6 +533,7 @@ export function ComposeModal() {
const [isPosting, setIsPosting] = useState(false)
const [postingProgress, setPostingProgress] = useState<PostingProgress | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [postLanguage, setPostLanguage] = useState('en')
const firstTextareaRef = useRef<HTMLTextAreaElement>(null)

// Focus first textarea when modal opens
Expand Down Expand Up @@ -513,6 +609,7 @@ export function ComposeModal() {
return await dashClient.createPost(postContent, {
replyToPostId: previousPostId || undefined,
quotedPostId: i === 0 ? quotingPost?.id : undefined,
language: postLanguage,
})
})

Expand Down Expand Up @@ -740,6 +837,7 @@ export function ComposeModal() {
resetThreadPosts()
setShowPreview(false)
setPostingProgress(null)
setPostLanguage('en')
}

const handleKeyDown = (e: React.KeyboardEvent) => {
Expand Down Expand Up @@ -894,9 +992,14 @@ export function ComposeModal() {
</div>
</div>

{/* Footer - minimal with keyboard hint */}
{/* Footer - language selector and keyboard hint */}
<div className="px-4 py-2 border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-neutral-950">
<div className="flex items-center justify-end">
<div className="flex items-center justify-between">
<LanguageSelector
value={postLanguage}
onChange={setPostLanguage}
disabled={isPosting}
/>
<span className="text-xs text-gray-400">
{threadPosts.length > 1
? `${totalCharacters} total chars · ${isMac ? '⌘' : 'Ctrl'}+Enter to post`
Expand Down
5 changes: 3 additions & 2 deletions lib/dash-platform-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class DashPlatformClient {
quotedPostId?: string
mediaUrl?: string
primaryHashtag?: string
language?: string
}) {
// Get identity ID from instance or auth context
let identityId = this.identityId
Expand Down Expand Up @@ -150,8 +151,8 @@ export class DashPlatformClient {
postData.primaryHashtag = options.primaryHashtag.replace('#', '')
}

// Add language (defaults to 'en' in the contract, but let's be explicit)
postData.language = 'en'
// Add language (defaults to 'en' if not specified)
postData.language = options?.language || 'en'

console.log('Creating post with data:', postData)

Expand Down