diff --git a/.cursor/commands/clean-up.md b/.cursor/commands/clean-up.md index b18b60b..5c020eb 100644 --- a/.cursor/commands/clean-up.md +++ b/.cursor/commands/clean-up.md @@ -1,4 +1,40 @@ -View git diff between the branch i specified and HEAD. If no branch is specified, -default to main. Identify code that needs to be cleaned up, like debug logs, -unused variables etc. Think twice before removing or adding code, because you -mustn't alter the behavior. \ No newline at end of file +# Code Cleanup Command + +## Overview +Analyze git diff between the specified branch and HEAD (defaults to main if no branch specified) and clean up code quality issues without altering functionality. + +## Process +1. **Get diff**: Run `git diff ..HEAD` to see changes +2. **Identify issues**: Look for code quality problems in the diff +3. **Clean up**: Remove only the identified issues +4. **Verify**: Ensure no behavioral changes + +## What to Clean Up +- **Debug artifacts**: `console.log()`, `debugger`, `print()` statements +- **Unused code**: Variables, imports, functions, parameters +- **Commented code**: Dead code blocks, TODO comments (unless active) +- **Formatting**: Inconsistent spacing, trailing whitespace +- **Temporary code**: Test values, hardcoded strings meant to be dynamic +- **Redundant code**: Duplicate logic, unnecessary intermediate variables + +## What NOT to Touch +- **Functional logic**: Don't change how features work +- **API interfaces**: Keep method signatures intact +- **Configuration**: Don't modify settings or constants +- **Comments**: Keep documentation and explanatory comments +- **Error handling**: Don't remove try-catch blocks or validation + +## Safety Rules +- ✅ Only modify code that appears in the git diff +- ✅ Preserve all existing functionality +- ✅ Maintain code readability and structure +- ❌ Don't refactor or optimize beyond cleanup +- ❌ Don't add new features or improvements +- ❌ Don't change variable names or function signatures + +## Example +```bash +# If user specifies: "/clean-up main" +git diff main +# Clean only the issues found in this diff +``` \ No newline at end of file diff --git a/.cursor/rules/docs.mdc b/.cursor/rules/docs.mdc index 4b13faf..a3125d3 100644 --- a/.cursor/rules/docs.mdc +++ b/.cursor/rules/docs.mdc @@ -1,6 +1,5 @@ --- -description: Documentation rules -alwaysApply: false +alwaysApply: true --- When documenting this project, follow these rules: diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc index 0eefb7f..8504a0a 100644 --- a/.cursor/rules/general.mdc +++ b/.cursor/rules/general.mdc @@ -16,6 +16,7 @@ When working with this project, follow these rules: - Use double quotes ("") for strings consistently. - Prefer functional components over class components in React. - Use TypeScript strictly - avoid `any` types unless absolutely necessary. +- DO NOT leave placeholders - ask me when it would be better or implement it fully. ## File Operations - If possible, try to update files in a single edit when making multiple changes. diff --git a/backend/routes/account.py b/backend/routes/account.py index 4f0e0af..0448417 100644 --- a/backend/routes/account.py +++ b/backend/routes/account.py @@ -222,4 +222,20 @@ def list_users(current_user: User = Depends(get_current_user), db: Session = Dep @router.get("/crypto/public-key/of/{user_id}") def get_public_key_of(user_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): row = db.query(CryptoPublicKey).filter(CryptoPublicKey.user_id == user_id).first() - return {"publicKey": row.public_key_b64 if row else None} \ No newline at end of file + return {"publicKey": row.public_key_b64 if row else None} + + +@router.get("/users/search") +def search_users(q: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + if len(q.strip()) < 2: + return {"users": []} + + # Case-insensitive partial match on username + users = db.query(User).filter( + User.username.ilike(f"%{q.strip()}%"), + User.id != current_user.id # Exclude current user + ).order_by(User.username.asc()).limit(20).all() + + return { + "users": [convert_user(u) for u in users] + } \ No newline at end of file diff --git a/backend/routes/messaging.py b/backend/routes/messaging.py index d76a28d..d24dacc 100644 --- a/backend/routes/messaging.py +++ b/backend/routes/messaging.py @@ -9,6 +9,7 @@ from fastapi.responses import FileResponse from fastapi.security import HTTPAuthorizationCredentials from sqlalchemy.orm import Session from dependencies import get_current_user, get_db +from .account import convert_user from constants import OWNER_USERNAME from models import Message, SendMessageRequest, EditMessageRequest, User, DMEnvelope, MessageFile, DMFile, Reaction, ReactionRequest, ReactionResponse, DMReaction, DMReactionRequest, DMReactionResponse from push_service import push_service @@ -448,6 +449,48 @@ async def dm_history(other_user_id: int, current_user: User = Depends(get_curren ) +@router.get("/dm/conversations") +async def get_dm_conversations(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + # Get all DM conversations where current user is involved + conversations_query = db.query(DMEnvelope).filter( + (DMEnvelope.sender_id == current_user.id) | (DMEnvelope.recipient_id == current_user.id) + ).order_by(DMEnvelope.timestamp.desc()) + + # Group by the "other user" (not current user) and get latest message + conversations = {} + for envelope in conversations_query: + other_user_id = envelope.recipient_id if envelope.sender_id == current_user.id else envelope.sender_id + + if other_user_id not in conversations: + conversations[other_user_id] = envelope + + # Get user info for each conversation + result = [] + for other_user_id, latest_message in conversations.items(): + other_user = db.query(User).filter(User.id == other_user_id).first() + if other_user: + # Calculate unread count for this conversation + unread_count = db.query(DMEnvelope).filter( + DMEnvelope.sender_id == other_user_id, + DMEnvelope.recipient_id == current_user.id, + DMEnvelope.id > getattr(latest_message, 'last_read_id', 0) # This would need to be stored somewhere + ).count() + + result.append({ + "user": convert_user(other_user), + "lastMessage": convert_dm_envelope(latest_message), + "unreadCount": unread_count + }) + + # Sort by latest message timestamp + result.sort(key=lambda x: x["lastMessage"]["timestamp"], reverse=True) + + return { + "status": "success", + "conversations": result + } + + @router.put("/edit_message/{message_id}") async def edit_message( message_id: int, @@ -792,6 +835,8 @@ class MessaggingSocketManager: "type": "dmEdited", "data": { "id": env.id, + "senderId": env.sender_id, + "recipientId": env.recipient_id, "iv": env.iv_b64, "ciphertext": env.ciphertext_b64, "iv2": env.iv2_b64, diff --git a/frontend/src/core/api/dmApi.ts b/frontend/src/core/api/dmApi.ts index 629285a..e2003d8 100644 --- a/frontend/src/core/api/dmApi.ts +++ b/frontend/src/core/api/dmApi.ts @@ -169,3 +169,29 @@ export async function deleteDmEnvelope(id: number, recipientId: number, authToke data: { id, recipientId } }); } + +export interface DMConversationResponse { + user: User; + lastMessage: DmEnvelope; + unreadCount: number; +} + +export async function fetchDMConversations(token: string): Promise { + const res = await fetch(`${API_BASE_URL}/dm/conversations`, { + headers: getAuthHeaders(token, true) + }); + if (!res.ok) return []; + const data = await res.json(); + return data.conversations || []; +} + +export async function searchUsers(query: string, token: string): Promise { + if (query.length < 2) return []; + + const res = await fetch(`${API_BASE_URL}/users/search?q=${encodeURIComponent(query)}`, { + headers: getAuthHeaders(token, true) + }); + if (!res.ok) return []; + const data = await res.json(); + return data.users || []; +} diff --git a/frontend/src/core/components/RichTextArea.tsx b/frontend/src/core/components/RichTextArea.tsx index 8fb44e0..c2854a6 100644 --- a/frontend/src/core/components/RichTextArea.tsx +++ b/frontend/src/core/components/RichTextArea.tsx @@ -10,6 +10,7 @@ interface RichTextAreaProps { className?: string; rows?: number; autoComplete?: string; + readOnly?: boolean; } export function RichTextArea({ @@ -21,6 +22,7 @@ export function RichTextArea({ className, rows = 1, autoComplete = "off", + readOnly = false }: RichTextAreaProps) { const textareaRef = useRef(null); const hiddenTextareaRef = useRef(null); @@ -183,10 +185,10 @@ export function RichTextArea({ value={text} placeholder={placeholder} rows={rows} - autoComplete={autoComplete} - onChange={handleChange} - onKeyDown={handleKeyDown} - /> + autoComplete={readOnly ? "off" : autoComplete} + onChange={readOnly ? undefined : handleChange} + onKeyDown={readOnly ? undefined : handleKeyDown} + readOnly={readOnly} />