AIDA / docs /CHAT_API_DOCUMENTATION.md
destinyebuka's picture
fyp
699ead6

A newer version of the Gradio SDK is available: 6.6.0

Upgrade

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:

{
  "listing_id": "675abc123...",
  "message": "Hi, I'm interested in this property"  // optional
}

Response:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "success": true,
  "conversation_id": "674xyz789...",
  "cleared_count": 25,
  "cleared_at": "2024-12-26T00:22:00.000000"
}

Frontend Implementation:

// 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
{
  "user_ids": ["user_123", "user_456"]
}

WebSocket API

Connection

final wsUrl = 'wss://api.lojiz.com/ws/chat?token=$jwtToken';
final channel = WebSocketChannel.connect(Uri.parse(wsUrl));

Connection Confirmed

Server sends after successful connection:

{
  "action": "connected",
  "user_id": "user_123",
  "timestamp": "2024-12-24T10:00:00.000000"
}

WebSocket Actions (Client β†’ Server)

Send Message

{
  "action": "send_message",
  "conversation_id": "674xyz789...",
  "message_type": "text",
  "content": "Hello!",
  "media": null,
  "property_card": null
}

Typing Indicator

{
  "action": "typing",
  "conversation_id": "674xyz789...",
  "is_typing": true
}

Mark Read

{
  "action": "mark_read",
  "conversation_id": "674xyz789..."
}

Heartbeat (Keep Alive)

Send every 30 seconds:

{
  "action": "heartbeat"
}

Edit Message

{
  "action": "edit_message",
  "conversation_id": "674xyz789...",
  "message_id": "msg_123",
  "new_content": "Updated text"
}

Delete Message

{
  "action": "delete_message",
  "conversation_id": "674xyz789...",
  "message_id": "msg_123",
  "delete_for": "everyone"
}

delete_for: "everyone" or "me"

Add Reaction

{
  "action": "add_reaction",
  "conversation_id": "674xyz789...",
  "message_id": "msg_123",
  "emoji": "πŸ‘"
}

Remove Reaction

{
  "action": "remove_reaction",
  "conversation_id": "674xyz789...",
  "message_id": "msg_123",
  "emoji": "πŸ‘"
}

Clear Chat

{
  "action": "clear_chat",
  "conversation_id": "674xyz789..."
}

WebSocket Events (Server β†’ Client)

New Message

{
  "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

{
  "action": "user_typing",
  "conversation_id": "674xyz789...",
  "user_id": "user_456",
  "user_name": "Jane Smith",
  "is_typing": true
}

Message Read

{
  "action": "message_read",
  "conversation_id": "674xyz789...",
  "read_by": "user_456",
  "read_at": "2024-12-24T11:36:00.000000"
}

User Status Changed

{
  "action": "user_status_changed",
  "user_id": "user_456",
  "is_online": true,
  "last_seen": null
}

Message Edited

{
  "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

{
  "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

{
  "action": "reaction_added",
  "conversation_id": "674xyz789...",
  "message_id": "msg_123",
  "emoji": "πŸ‘",
  "user_id": "user_456"
}

Reaction Removed

{
  "action": "reaction_removed",
  "conversation_id": "674xyz789...",
  "message_id": "msg_123",
  "emoji": "πŸ‘",
  "user_id": "user_456"
}

Chat Cleared (Persistent)

Received after successfully clearing a chat:

{
  "action": "chat_cleared",
  "conversation_id": "674xyz789...",
  "cleared_count": 25,
  "cleared_at": "2024-12-26T00:22:00.000000"
}

Frontend Handling:

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

{
  "action": "heartbeat_ack",
  "timestamp": "2024-12-24T11:45:00.000000"
}

Message Object Schema

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

if (message.canEdit) {
  // Show edit button in message options
}

Show/Hide Delete Options

// "Delete for everyone" - only when allowed
if (message.canDeleteForEveryone) {
  showDeleteForEveryoneOption();
}

// "Delete for me" - always available
showDeleteForMeOption();

Display Edited Label

if (message.isEdited) {
  Text("(edited)", style: TextStyle(fontSize: 12, color: Colors.grey));
}

Display Deleted Message

if (message.isDeleted) {
  Text("🚫 This message was deleted", 
    style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey));
}

Display Reactions

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

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