Remove E2EE and switch to plaintext messaging

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
jjbrunton committed 2026-01-08 18:04:48 +00:00
1 parent 43fa7033dc
commit c48936686e
46 files changed
+738 -6874

No files matched your search

+5 -3
View File
@@ -30,9 +30,11 @@
"emoji-mart": "^5.6.0",
"lucide-react": "^0.309.0",
"openai": "^4.24.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-router-dom": "^6.21.0",
"recharts": "^3.6.0",
"sonner": "^1.3.1",
"tailwind-merge": "^2.2.0"
@@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { EmojiPicker } from '@/components/ui/emoji-picker';
import { IconPicker } from '@/components/ui/icon-picker';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
@@ -98,9 +98,9 @@ export function PackFormDialog({
{/* Icon */}
<div className="space-y-2">
<Label>Icon</Label>
<EmojiPicker
<IconPicker
value={formData.icon}
onChange={(emoji) => setField('icon', emoji)}
onChange={(iconName) => setField('icon', iconName)}
/>
</div>
@@ -0,0 +1,231 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type { IconType } from 'react-icons';
import {
IoHeartOutline,
IoHeartHalfOutline,
IoHeartCircleOutline,
IoChatbubblesOutline,
IoChatboxOutline,
IoMailOutline,
IoFlowerOutline,
IoWineOutline,
IoRestaurantOutline,
IoCafeOutline,
IoAirplaneOutline,
IoCarOutline,
IoCompassOutline,
IoMapOutline,
IoHomeOutline,
IoPeopleOutline,
IoPersonOutline,
IoEyeOffOutline,
IoKeyOutline,
IoLockClosedOutline,
IoDiceOutline,
IoGiftOutline,
IoSparklesOutline,
IoStarOutline,
IoFlameOutline,
IoFlashOutline,
IoMoonOutline,
IoSunnyOutline,
IoFlagOutline,
IoCalendarOutline,
IoCheckboxOutline,
IoTrophyOutline,
IoLayersOutline,
IoCubeOutline,
IoFolderOutline,
IoBookmarkOutline,
IoBulbOutline,
IoColorWandOutline,
IoSyncOutline,
IoRefreshOutline,
IoBriefcaseOutline,
} from 'react-icons/io5';
import { Button } from './button';
import { Input } from './input';
interface IconOption {
name: string;
Icon: IconType;
}
const ICON_OPTIONS: IconOption[] = [
// Relationships & Love
{ name: 'heart-outline', Icon: IoHeartOutline },
{ name: 'heart-half-outline', Icon: IoHeartHalfOutline },
{ name: 'heart-circle-outline', Icon: IoHeartCircleOutline },
// Communication
{ name: 'chatbubbles-outline', Icon: IoChatbubblesOutline },
{ name: 'chatbox-outline', Icon: IoChatboxOutline },
{ name: 'mail-outline', Icon: IoMailOutline },
// Romance & Dates
{ name: 'flower-outline', Icon: IoFlowerOutline },
{ name: 'wine-outline', Icon: IoWineOutline },
{ name: 'restaurant-outline', Icon: IoRestaurantOutline },
{ name: 'cafe-outline', Icon: IoCafeOutline },
// Adventure & Travel
{ name: 'airplane-outline', Icon: IoAirplaneOutline },
{ name: 'car-outline', Icon: IoCarOutline },
{ name: 'compass-outline', Icon: IoCompassOutline },
{ name: 'map-outline', Icon: IoMapOutline },
// Home & Family
{ name: 'home-outline', Icon: IoHomeOutline },
{ name: 'people-outline', Icon: IoPeopleOutline },
{ name: 'person-outline', Icon: IoPersonOutline },
// Mystery & Secrets
{ name: 'eye-off-outline', Icon: IoEyeOffOutline },
{ name: 'key-outline', Icon: IoKeyOutline },
{ name: 'lock-closed-outline', Icon: IoLockClosedOutline },
// Fun & Games
{ name: 'dice-outline', Icon: IoDiceOutline },
{ name: 'gift-outline', Icon: IoGiftOutline },
{ name: 'sparkles-outline', Icon: IoSparklesOutline },
{ name: 'star-outline', Icon: IoStarOutline },
// Intimacy
{ name: 'flame-outline', Icon: IoFlameOutline },
{ name: 'flash-outline', Icon: IoFlashOutline },
{ name: 'moon-outline', Icon: IoMoonOutline },
{ name: 'sunny-outline', Icon: IoSunnyOutline },
// Goals & Planning
{ name: 'flag-outline', Icon: IoFlagOutline },
{ name: 'calendar-outline', Icon: IoCalendarOutline },
{ name: 'checkbox-outline', Icon: IoCheckboxOutline },
{ name: 'trophy-outline', Icon: IoTrophyOutline },
// General
{ name: 'layers-outline', Icon: IoLayersOutline },
{ name: 'cube-outline', Icon: IoCubeOutline },
{ name: 'folder-outline', Icon: IoFolderOutline },
{ name: 'bookmark-outline', Icon: IoBookmarkOutline },
{ name: 'bulb-outline', Icon: IoBulbOutline },
{ name: 'color-wand-outline', Icon: IoColorWandOutline },
{ name: 'sync-outline', Icon: IoSyncOutline },
{ name: 'refresh-outline', Icon: IoRefreshOutline },
{ name: 'briefcase-outline', Icon: IoBriefcaseOutline },
];
const ICONS_BY_NAME = ICON_OPTIONS.reduce<Record<string, IconOption>>((acc, option) => {
acc[option.name] = option;
return acc;
}, {});
interface IconPreviewProps {
value?: string | null;
fallback?: string;
className?: string;
}
export function IconPreview({ value, fallback = '?', className }: IconPreviewProps) {
const resolved = value ?? fallback;
const Icon = resolved ? ICONS_BY_NAME[resolved]?.Icon : undefined;
if (Icon) {
return <Icon className={className} />;
}
return <span className={className}>{resolved}</span>;
}
interface IconPickerProps {
value: string;
onChange: (iconName: string) => void;
}
export function IconPicker({ value, onChange }: IconPickerProps) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
if (open) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [open]);
const filteredIcons = useMemo(() => {
const normalized = query.trim().toLowerCase();
if (!normalized) return ICON_OPTIONS;
return ICON_OPTIONS.filter((option) => option.name.includes(normalized));
}, [query]);
const SelectedIcon = ICONS_BY_NAME[value]?.Icon;
const buttonLabel = SelectedIcon ? value : value ? 'Legacy icon' : 'Select an icon';
return (
<div className="relative" ref={containerRef}>
<Button
type="button"
variant="outline"
className="h-14 w-full justify-start gap-3 px-3"
onClick={() => setOpen(!open)}
>
{SelectedIcon ? (
<SelectedIcon className="h-6 w-6" />
) : (
<span className="text-2xl">{value || '?'}</span>
)}
<span className="text-sm text-muted-foreground">{buttonLabel}</span>
</Button>
{open && (
<div className="absolute top-full left-0 mt-2 w-[22rem] rounded-md border bg-background p-3 shadow-lg z-50">
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search icons..."
className="mb-3"
/>
<div className="grid grid-cols-4 gap-2 max-h-64 overflow-y-auto pr-1">
{filteredIcons.map(({ name, Icon }) => {
const isSelected = name === value;
return (
<button
key={name}
type="button"
onClick={() => {
onChange(name);
setOpen(false);
}}
className={`flex flex-col items-center gap-1 rounded-md border p-2 text-xs transition-colors hover:bg-muted ${
isSelected
? 'border-primary ring-1 ring-primary/40'
: 'border-border'
}`}
>
<Icon className="h-5 w-5" />
<span className="text-[10px] leading-tight text-muted-foreground">
{name}
</span>
</button>
);
})}
{filteredIcons.length === 0 && (
<div className="col-span-4 text-xs text-muted-foreground">
No icons found
</div>
)}
</div>
</div>
)}
</div>
);
}
+2 -1
View File
@@ -144,7 +144,8 @@ ${explicitInstruction}${crudeLangInstruction}${inspirationInstruction}${avoidIns
Return a JSON object with an "ideas" array containing 5 objects, where each object has:
- name: Pack name (catchy, 3-6 words) - should evoke activities/experiences, not questions
- description: Brief description (1-2 sentences) focusing on the activities/experiences in the pack
- icon: A single descriptive emoji
- icon: An Ionicon name from this list: heart-outline, flame-outline, sparkles-outline, gift-outline, wine-outline, airplane-outline, home-outline, key-outline, flash-outline, sunny-outline, flower-outline, star-outline, dice-outline, compass-outline, bulb-outline
Make them engaging and specific to "${categoryName}".`;
@@ -8,7 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { EmojiPicker } from '@/components/ui/emoji-picker';
import { IconPicker, IconPreview } from '@/components/ui/icon-picker';
import { Skeleton } from '@/components/ui/skeleton';
import {
Dialog,
@@ -56,11 +56,11 @@ export function CategoriesPage() {
// Form state using the new hook
const form = useEntityForm<CategoryFormData, Category>(
{ name: '', description: '', icon: '📚' },
{ name: '', description: '', icon: 'bookmark-outline' },
(category) => ({
name: category.name,
description: category.description || '',
icon: category.icon || '📚',
icon: category.icon || 'bookmark-outline',
})
);
@@ -118,7 +118,7 @@ export function CategoriesPage() {
form.openCreateWith({
name: idea.name,
description: idea.description || '',
icon: idea.icon || '📚',
icon: idea.icon || 'bookmark-outline',
});
};
@@ -261,9 +261,9 @@ export function CategoriesPage() {
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Icon</Label>
<EmojiPicker
<IconPicker
value={form.formData.icon}
onChange={(emoji) => form.setField('icon', emoji)}
onChange={(iconName) => form.setField('icon', iconName)}
/>
</div>
@@ -363,7 +363,9 @@ export function CategoriesPage() {
<ChevronDown className="h-4 w-4" />
</Button>
</div>
<div className="text-4xl mb-2">{category.icon || '📚'}</div>
<div className="text-4xl mb-2">
<IconPreview value={category.icon} fallback="bookmark-outline" className="text-4xl" />
</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
<Button
+10 -5
View File
@@ -12,6 +12,7 @@ import { toast } from 'sonner';
import { AiGeneratorDialog } from '@/components/ai/AiGeneratorDialog';
import { ExtractTopicsDialog } from '@/components/content/ExtractTopicsDialog';
import { PackFormDialog, type PackFormData } from '@/components/content/PackFormDialog';
import { IconPreview } from '@/components/ui/icon-picker';
// =============================================================================
// Types
@@ -65,7 +66,7 @@ export function PacksPage() {
{
name: '',
description: '',
icon: '💕',
icon: 'heart-outline',
is_premium: false,
is_public: false,
is_explicit: false,
@@ -73,7 +74,7 @@ export function PacksPage() {
(pack) => ({
name: pack.name,
description: pack.description || '',
icon: pack.icon || '💕',
icon: pack.icon || 'heart-outline',
is_premium: pack.is_premium,
is_public: pack.is_public,
is_explicit: pack.is_explicit,
@@ -265,7 +266,7 @@ export function PacksPage() {
form.openCreateWith({
name: result.name || '',
description: result.description,
icon: result.icon || '💕',
icon: result.icon || 'heart-outline',
is_premium: false,
is_public: false,
is_explicit: false,
@@ -331,7 +332,9 @@ export function PacksPage() {
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
{category && (
<>
<span className="text-3xl">{category.icon}</span>
<span className="text-3xl">
<IconPreview value={category.icon} fallback="bookmark-outline" className="text-3xl" />
</span>
{category.name}:
</>
)}
@@ -437,7 +440,9 @@ export function PacksPage() {
<ChevronDown className="h-4 w-4" />
</Button>
</div>
<div className="text-4xl mb-2">{pack.icon || '💕'}</div>
<div className="text-4xl mb-2">
<IconPreview value={pack.icon} fallback="heart-outline" className="text-4xl" />
</div>
</div>
<div className="flex items-center gap-1">
{pack.is_premium && (