AIDA / docs /CHAT_API_DOCUMENTATION.md
destinyebuka's picture
fyp
699ead6
# 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