# Chat API Documentation Complete documentation for implementing real-time chat features in the Lojiz app. --- ## Base URL ``` REST API: https://api.lojiz.com/api/conversations WebSocket: wss://api.lojiz.com/ws/chat?token=JWT_TOKEN ``` --- ## Authentication All endpoints require JWT authentication: - **REST**: Include `Authorization: Bearer ` header - **WebSocket**: Pass token as query parameter `?token=` --- ## REST API Endpoints ### 1. Start/Get Conversation Creates a new conversation or returns existing one between two users. ``` POST /api/conversations/ ``` **Request Body:** ```json { "listing_id": "675abc123...", "message": "Hi, I'm interested in this property" // optional } ``` **Response:** ```json { "success": true, "is_new": true, "conversation": { "id": "674xyz789...", "listing_id": "675abc123...", "participants": ["user_123", "user_456"], "listing_title": "2BR Apartment in Lekki", "listing_image": "https://...", "last_message": null, "unread_count": {"user_123": 0, "user_456": 0}, "status": "active", "created_at": "2024-12-24T10:00:00.000000", "updated_at": "2024-12-24T10:00:00.000000" }, "property_card": { "listing_id": "675abc123...", "title": "2BR Apartment in Lekki", "price": 500000, "currency": "NGN", "bedrooms": 2, "bathrooms": 2, "location": "Lekki Phase 1, Lagos", "image_url": "https://...", "listing_type": "rent" } } ``` --- ### 2. Get All Conversations Returns all conversations for the current user. ``` GET /api/conversations/ ``` **Response:** ```json { "success": true, "data": [ { "id": "674xyz789...", "participants": ["user_123", "user_456"], "listing_title": "2BR Apartment", "listing_image": "https://...", "last_message": { "text": "Yes, it's still available!", "sender_id": "user_456", "timestamp": "2024-12-24T11:30:00.000000" }, "unread_count": {"user_123": 2, "user_456": 0}, "created_at": "...", "updated_at": "..." } ], "total": 1 } ``` --- ### 3. Get Messages Returns messages for a conversation with pagination. ``` GET /api/conversations/{conversation_id}/messages?limit=50&before_id=... ``` **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | int | 50 | Number of messages (max 100) | | `before_id` | string | null | Get messages before this ID (pagination) | **Response:** ```json { "success": true, "data": [ { "id": "msg_123", "conversation_id": "674xyz789...", "sender_id": "user_123", "sender_name": "John Doe", "sender_avatar": "https://...", "message_type": "text", "content": "Hello!", "media": null, "property_card": null, "is_read": true, "read_at": "2024-12-24T11:31:00.000000", "is_edited": false, "edited_at": null, "is_deleted": false, "deleted_at": null, "reactions": {"👍": ["user_456"]}, "created_at": "2024-12-24T11:30:00.000000", "can_edit": false, "can_delete_for_everyone": false } ], "total": 1 } ``` --- ### 4. Send Message (REST Fallback) Use when WebSocket is unavailable. ``` POST /api/conversations/{conversation_id}/messages ``` **Request Body:** ```json { "message_type": "text", "content": "Hello!", "media_url": null, "property_card": null } ``` **Message Types:** | Type | Description | |------|-------------| | `text` | Plain text message | | `image` | Image attachment | | `document` | Document attachment | | `voice` | Voice message | | `property_inquiry` | Property card message | --- ### 5. Mark as Read ``` POST /api/conversations/{conversation_id}/read ``` --- ### 6. Edit Message ``` PUT /api/conversations/{conversation_id}/messages/{message_id} ``` **Request Body:** ```json { "content": "Updated message text" } ``` **Constraints:** - ✅ Sender only - ✅ Text messages only - ✅ Within 15 minutes of sending **Error Responses:** - `403`: "You can only edit your own messages" - `400`: "Only text messages can be edited" - `400`: "Edit window expired (15 minutes)" --- ### 7. Delete Message ``` DELETE /api/conversations/{conversation_id}/messages/{message_id}?for=everyone|me ``` **Query Parameter:** | Value | Description | Constraints | |-------|-------------|-------------| | `everyone` | Delete for all participants | Sender only, within 1 hour | | `me` | Delete for yourself only | Anyone, no time limit | --- ### 8. Add Reaction ``` POST /api/conversations/{conversation_id}/messages/{message_id}/reactions ``` **Request Body:** ```json { "emoji": "👍" } ``` --- ### 9. Remove Reaction ``` DELETE /api/conversations/{conversation_id}/messages/{message_id}/reactions/{emoji} ``` Example: `DELETE /api/conversations/123/messages/456/reactions/👍` --- ### 10. Clear Chat (Persistent) ``` POST /api/conversations/{conversation_id}/clear ``` Clears all messages in a conversation for the current user only. Other participant still sees all messages. > **⚠️ NEW BEHAVIOR (v2.0):** Clear is now **persistent across sessions and devices**. > > When a user clears a chat: > - A `cleared_at` timestamp is stored on the conversation > - Messages with `created_at <= cleared_at` will **never** be shown to that user again > - This persists even after logout/login or using a new device > - New messages sent AFTER the clear will be visible to both participants **Response:** ```json { "success": true, "conversation_id": "674xyz789...", "cleared_count": 25, "cleared_at": "2024-12-26T00:22:00.000000" } ``` **Frontend Implementation:** ```dart // Clear chat and handle response Future clearChat(String conversationId) async { final response = await api.delete('/conversations/$conversationId/clear'); if (response['success']) { // Store cleared_at locally for optimistic UI final clearedAt = response['cleared_at']; // Remove all messages from local state setState(() { messages.clear(); }); // Optionally store cleared_at for reference await prefs.setString('cleared_$conversationId', clearedAt); } } ``` **Important Notes:** - No frontend changes required for basic functionality - the backend handles filtering - The `cleared_at` timestamp is stored server-side on the conversation document - When fetching messages, the server automatically filters out messages before `cleared_at` - New messages sent after clearing the chat will appear for both users --- ### 11. Online Status **Get Single User Status:** ``` GET /api/conversations/users/{user_id}/online-status ``` **Get Bulk Status:** ``` POST /api/conversations/users/online-status ``` ```json { "user_ids": ["user_123", "user_456"] } ``` --- ## WebSocket API ### Connection ```dart final wsUrl = 'wss://api.lojiz.com/ws/chat?token=$jwtToken'; final channel = WebSocketChannel.connect(Uri.parse(wsUrl)); ``` ### Connection Confirmed Server sends after successful connection: ```json { "action": "connected", "user_id": "user_123", "timestamp": "2024-12-24T10:00:00.000000" } ``` --- ## WebSocket Actions (Client → Server) ### Send Message ```json { "action": "send_message", "conversation_id": "674xyz789...", "message_type": "text", "content": "Hello!", "media": null, "property_card": null } ``` ### Typing Indicator ```json { "action": "typing", "conversation_id": "674xyz789...", "is_typing": true } ``` ### Mark Read ```json { "action": "mark_read", "conversation_id": "674xyz789..." } ``` ### Heartbeat (Keep Alive) Send every 30 seconds: ```json { "action": "heartbeat" } ``` ### Edit Message ```json { "action": "edit_message", "conversation_id": "674xyz789...", "message_id": "msg_123", "new_content": "Updated text" } ``` ### Delete Message ```json { "action": "delete_message", "conversation_id": "674xyz789...", "message_id": "msg_123", "delete_for": "everyone" } ``` `delete_for`: `"everyone"` or `"me"` ### Add Reaction ```json { "action": "add_reaction", "conversation_id": "674xyz789...", "message_id": "msg_123", "emoji": "👍" } ``` ### Remove Reaction ```json { "action": "remove_reaction", "conversation_id": "674xyz789...", "message_id": "msg_123", "emoji": "👍" } ``` ### Clear Chat ```json { "action": "clear_chat", "conversation_id": "674xyz789..." } ``` --- ## WebSocket Events (Server → Client) ### New Message ```json { "action": "new_message", "conversation_id": "674xyz789...", "message": { "id": "msg_456", "sender_id": "user_456", "sender_name": "Jane Smith", "content": "Hi there!", "message_type": "text", "is_edited": false, "reactions": {}, "created_at": "2024-12-24T11:35:00.000000", "can_edit": true, "can_delete_for_everyone": true } } ``` ### User Typing ```json { "action": "user_typing", "conversation_id": "674xyz789...", "user_id": "user_456", "user_name": "Jane Smith", "is_typing": true } ``` ### Message Read ```json { "action": "message_read", "conversation_id": "674xyz789...", "read_by": "user_456", "read_at": "2024-12-24T11:36:00.000000" } ``` ### User Status Changed ```json { "action": "user_status_changed", "user_id": "user_456", "is_online": true, "last_seen": null } ``` ### Message Edited ```json { "action": "message_edited", "conversation_id": "674xyz789...", "message_id": "msg_123", "new_content": "Updated message", "edited_at": "2024-12-24T11:40:00.000000", "edited_by": "user_123" } ``` ### Message Deleted ```json { "action": "message_deleted", "conversation_id": "674xyz789...", "message_id": "msg_123", "deleted_for": "everyone", "deleted_at": "2024-12-24T11:41:00.000000", "deleted_by": "user_123" } ``` ### Reaction Added ```json { "action": "reaction_added", "conversation_id": "674xyz789...", "message_id": "msg_123", "emoji": "👍", "user_id": "user_456" } ``` ### Reaction Removed ```json { "action": "reaction_removed", "conversation_id": "674xyz789...", "message_id": "msg_123", "emoji": "👍", "user_id": "user_456" } ``` ### Chat Cleared (Persistent) Received after successfully clearing a chat: ```json { "action": "chat_cleared", "conversation_id": "674xyz789...", "cleared_count": 25, "cleared_at": "2024-12-26T00:22:00.000000" } ``` **Frontend Handling:** ```dart case 'chat_cleared': final conversationId = event['conversation_id']; final clearedAt = event['cleared_at']; // Clear all messages from local state for this conversation setState(() { messages.clear(); }); // Store cleared_at for reference (optional) // The server will filter messages on next fetch automatically break; ``` > **Note:** After clearing, the next time you fetch messages for this conversation, > the server will only return messages created AFTER the `cleared_at` timestamp. > This is automatic - no client-side filtering is needed. ### Heartbeat Acknowledgment ```json { "action": "heartbeat_ack", "timestamp": "2024-12-24T11:45:00.000000" } ``` --- ## Message Object Schema ```typescript interface Message { id: string; conversation_id: string; sender_id: string; sender_name: string; sender_avatar: string | null; message_type: "text" | "image" | "document" | "voice" | "property_inquiry" | "system"; content: string | null; media: { url: string; filename: string; size: number; mime_type: string; duration?: number; // for voice messages thumbnail_url?: string; // for images/videos } | null; property_card: { listing_id: string; title: string; price: number; currency: string; bedrooms: number; bathrooms: number; location: string; image_url: string; listing_type: string; } | null; is_read: boolean; read_at: string | null; // ISO datetime is_edited: boolean; edited_at: string | null; // ISO datetime is_deleted: boolean; deleted_at: string | null; // ISO datetime reactions: { [emoji: string]: string[]; // emoji -> array of user IDs }; created_at: string; // ISO datetime // Action availability (computed by server) can_edit: boolean; // true if sender + text + <15 min can_delete_for_everyone: boolean; // true if sender + <1 hour } ``` --- ## UI Implementation Guide ### Show/Hide Edit Button ```dart if (message.canEdit) { // Show edit button in message options } ``` ### Show/Hide Delete Options ```dart // "Delete for everyone" - only when allowed if (message.canDeleteForEveryone) { showDeleteForEveryoneOption(); } // "Delete for me" - always available showDeleteForMeOption(); ``` ### Display Edited Label ```dart if (message.isEdited) { Text("(edited)", style: TextStyle(fontSize: 12, color: Colors.grey)); } ``` ### Display Deleted Message ```dart if (message.isDeleted) { Text("🚫 This message was deleted", style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey)); } ``` ### Display Reactions ```dart Row( children: message.reactions.entries.map((entry) { final emoji = entry.key; final users = entry.value; return Chip( label: Text("$emoji ${users.length}"), onPressed: () => toggleReaction(message.id, emoji), ); }).toList(), ) ``` ### Handle WebSocket Events ```dart channel.stream.listen((data) { final event = jsonDecode(data); switch (event['action']) { case 'new_message': addMessage(event['message']); break; case 'message_edited': updateMessageContent(event['message_id'], event['new_content']); break; case 'message_deleted': if (event['deleted_for'] == 'everyone') { markMessageAsDeleted(event['message_id']); } else { removeMessageFromList(event['message_id']); } break; case 'reaction_added': addReactionToMessage(event['message_id'], event['emoji'], event['user_id']); break; case 'reaction_removed': removeReactionFromMessage(event['message_id'], event['emoji'], event['user_id']); break; case 'user_typing': showTypingIndicator(event['user_id'], event['is_typing']); break; case 'user_status_changed': updateOnlineStatus(event['user_id'], event['is_online']); break; } }); ``` --- ## Time Windows Summary | Feature | Time Limit | Who Can Do It | Persistent? | |---------|------------|---------------|-------------| | Edit message | 15 minutes | Sender only | N/A | | Delete for everyone | 1 hour | Sender only | Yes | | Delete for me | No limit | Anyone | Yes | | Add reaction | No limit | Any participant | Yes | | Clear chat | No limit | Current user only | **Yes (NEW)** | --- ## Clear Chat - Persistence Details ### How It Works ``` ┌─────────────────────────────────────────────────────────────────┐ │ CLEAR CHAT FLOW │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. User A clears chat │ │ └─→ Backend stores: conversation.cleared_at.userA = now() │ │ └─→ Backend marks existing messages with deleted_for │ │ │ │ 2. User A logs out and logs back in (or uses new device) │ │ └─→ Fetches messages for conversation │ │ └─→ Backend query: WHERE created_at > cleared_at.userA │ │ └─→ Returns: Empty list (all messages were before clear) │ │ │ │ 3. User B (other participant) fetches same conversation │ │ └─→ Backend query: WHERE created_at > cleared_at.userB │ │ └─→ cleared_at.userB is NULL (never cleared) │ │ └─→ Returns: All messages (sees everything) │ │ │ │ 4. User B sends new message AFTER User A cleared │ │ └─→ New message created_at > User A's cleared_at │ │ └─→ Both users can see the new message │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Key Points for Frontend 1. **No client-side filtering needed** - The server handles all filtering based on `cleared_at` 2. **Clear is immediate** - After clearing, fetch messages returns empty or only new messages 3. **Persists across sessions** - User won't see old messages even after logout/login 4. **Persists across devices** - User won't see old messages on any device 5. **Other user unaffected** - The other participant sees all messages as normal 6. **New messages visible** - Messages sent AFTER the clear are visible to both users