Spaces:
Running
Running
| # 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 <token>` header | |
| - **WebSocket**: Pass token as query parameter `?token=<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<void> 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 | |