File size: 9,361 Bytes
0a645ca
 
 
 
4e15311
 
 
 
 
 
0a645ca
 
 
 
 
 
 
 
 
 
 
 
 
 
f7e886e
 
 
0a645ca
 
 
 
 
 
 
 
 
 
 
 
f7e886e
 
 
 
0a645ca
 
2043cb2
310bac1
4e15311
 
 
 
 
 
 
 
 
0a645ca
 
 
 
4e15311
 
 
0a645ca
4e15311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a645ca
 
4e15311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a645ca
 
 
 
 
 
4e15311
 
 
6f7fd5d
 
f7e886e
 
 
 
0a645ca
4e15311
 
 
 
 
 
 
 
 
 
2043cb2
 
4e15311
 
 
0a645ca
4e15311
 
2043cb2
 
 
 
 
 
701f9eb
 
 
 
 
 
 
2043cb2
 
 
310bac1
2043cb2
701f9eb
 
 
 
2043cb2
701f9eb
2043cb2
 
 
701f9eb
310bac1
 
 
 
 
 
 
2043cb2
 
 
 
4e15311
 
701f9eb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# ============================================================
# app/models/message.py - Message Model
# ============================================================
from datetime import datetime
from typing import Optional, List, Dict

# Time windows for message actions (in minutes)
EDIT_WINDOW_MINUTES = 15           # Can edit within 15 minutes
DELETE_FOR_EVERYONE_WINDOW_MINUTES = 60  # Can delete for everyone within 1 hour


class Message:
    """Message model for MongoDB"""
    
    @staticmethod
    def create_document(
        conversation_id: str,
        sender_id: str,
        sender_name: str,
        sender_avatar: Optional[str],
        message_type: str,
        content: Optional[str] = None,
        media: Optional[dict] = None,
        property_card: Optional[dict] = None,
        replied_to_message_id: Optional[str] = None,
        replied_to_content: Optional[str] = None,
        replied_to_sender: Optional[str] = None,
    ) -> dict:
        """Create message document for insertion"""
        now = datetime.utcnow()
        return {
            "conversation_id": conversation_id,
            "sender_id": sender_id,
            "sender_name": sender_name,
            "sender_avatar": sender_avatar,
            "message_type": message_type,  # "text", "image", "document", "voice", "property_inquiry", "system"
            "content": content,
            "media": media,  # {url, filename, size, mime_type, duration, thumbnail_url}
            "property_card": property_card,  # {listing_id, title, price, currency, bedrooms, bathrooms, location, image_url, listing_type}
            # Reply-to fields
            "replied_to_message_id": replied_to_message_id,
            "replied_to_content": replied_to_content,  # Preview of replied message (truncated)
            "replied_to_sender": replied_to_sender,  # Name of original sender
            "is_read": False,
            "read_at": None,
            "read_by": [],  # List of user IDs who have read this message
            "delivered_to": [],  # List of user IDs who have received this message (online)
            # Edit tracking
            "is_edited": False,
            "edited_at": None,
            # Deletion tracking
            "is_deleted": False,           # Deleted for everyone
            "deleted_at": None,
            "deleted_for": [],             # List of user_ids who deleted this message for themselves
            # Reactions: {emoji: [user_id1, user_id2, ...]}
            "reactions": {},
            "created_at": now,
        }
    
    @staticmethod
    def _serialize_datetime(dt) -> Optional[str]:
        """Convert datetime to ISO string, handling None values"""
        if dt is None:
            return None
        if isinstance(dt, datetime):
            return dt.isoformat()
        return dt  # Already a string or other serializable type
    
    @staticmethod
    def _calculate_action_availability(
        message_doc: dict,
        for_user_id: Optional[str] = None
    ) -> dict:
        """
        Calculate whether edit and delete-for-everyone actions are available.
        
        Returns:
            dict with can_edit and can_delete_for_everyone flags
        """
        is_deleted = message_doc.get("is_deleted", False)
        sender_id = message_doc.get("sender_id")
        message_type = message_doc.get("message_type")
        created_at = message_doc.get("created_at")
        
        # Default: no actions available
        can_edit = False
        can_delete_for_everyone = False
        
        # If message is deleted, no actions available
        if is_deleted:
            return {"can_edit": False, "can_delete_for_everyone": False}
        
        # Only sender can edit/delete for everyone
        is_sender = for_user_id and sender_id == for_user_id
        
        if is_sender and created_at:
            now = datetime.utcnow()
            minutes_since = (now - created_at).total_seconds() / 60
            
            # Edit: only text messages, within 15 minutes
            if message_type == "text" and minutes_since <= EDIT_WINDOW_MINUTES:
                can_edit = True
            
            # Delete for everyone: within 1 hour
            if minutes_since <= DELETE_FOR_EVERYONE_WINDOW_MINUTES:
                can_delete_for_everyone = True
        
        return {
            "can_edit": can_edit,
            "can_delete_for_everyone": can_delete_for_everyone,
        }
    
    @staticmethod
    def format_response(message_doc: dict, for_user_id: Optional[str] = None) -> Optional[dict]:
        """
        Format message document for API response.
        
        Args:
            message_doc: The message document from MongoDB
            for_user_id: If provided, checks if message is deleted for this user
                         and calculates action availability
            
        Returns:
            Formatted message dict, or None if deleted for the requesting user
        """
        if not message_doc:
            return None
        
        # If message is deleted for everyone, show placeholder
        is_deleted = message_doc.get("is_deleted", False)
        
        # If deleted specifically for this user, don't return the message
        deleted_for = message_doc.get("deleted_for", [])
        if for_user_id and for_user_id in deleted_for:
            return None  # Message was "deleted for me" by this user
        
        # Calculate action availability for frontend
        action_availability = Message._calculate_action_availability(message_doc, for_user_id)
        
        # Build response
        response = {
            "id": str(message_doc.get("_id", "")),
            "conversation_id": message_doc.get("conversation_id"),
            "sender_id": message_doc.get("sender_id"),
            "sender_name": message_doc.get("sender_name"),
            "sender_avatar": message_doc.get("sender_avatar"),
            "message_type": message_doc.get("message_type"),
            "content": message_doc.get("content") if not is_deleted else None,
            "media": message_doc.get("media") if not is_deleted else None,
            "property_card": message_doc.get("property_card") if not is_deleted else None,
            # CRITICAL: Include metadata for alert_results, search_results, etc.
            "metadata": message_doc.get("metadata") if not is_deleted else None,
            # Reply-to fields
            "replied_to_message_id": message_doc.get("replied_to_message_id"),
            "replied_to_content": message_doc.get("replied_to_content"),
            "replied_to_sender": message_doc.get("replied_to_sender"),
            "is_read": message_doc.get("is_read", False),
            "read_at": Message._serialize_datetime(message_doc.get("read_at")),
            # Edit fields
            "is_edited": message_doc.get("is_edited", False),
            "edited_at": Message._serialize_datetime(message_doc.get("edited_at")),
            # Deletion status
            "is_deleted": is_deleted,
            "deleted_at": Message._serialize_datetime(message_doc.get("deleted_at")) if is_deleted else None,
            # Reactions
            "reactions": message_doc.get("reactions", {}),
            "created_at": Message._serialize_datetime(message_doc.get("created_at")),
            # Status field for read receipts - calculated from sender's perspective
            "status": Message._calculate_status(message_doc, for_user_id),
            # Action availability flags for frontend UI
            "can_edit": action_availability["can_edit"],
            "can_delete_for_everyone": action_availability["can_delete_for_everyone"],
        }
        
        return response
    
    @staticmethod
    def _calculate_status(message_doc: dict, for_user_id: Optional[str] = None) -> str:
        """
        Calculate message status from user's perspective.
        
        Status indicators:
        - sent: Message saved to database (single gray check)
        - delivered: Message received by recipient (double gray check)
        - read: Recipient has read the message (double blue check)
        
        For sender: Show read if recipient has read, otherwise delivered/sent
        For recipient: Always delivered (they have it)
        """
        sender_id = message_doc.get("sender_id")
        read_by = message_doc.get("read_by", [])
        delivered_to = message_doc.get("delivered_to", [])
        
        # If no for_user_id provided, default to "sent" (can't determine perspective)
        if not for_user_id:
            return "sent"
        
        # If viewing as sender
        if for_user_id == sender_id:
            # Check if anyone else has read it
            other_readers = [uid for uid in read_by if uid != sender_id]
            if other_readers:
                return "read"  # Recipient has read it - double blue check
            
            # Check if anyone else has received it (delivered)
            other_recipients = [uid for uid in delivered_to if uid != sender_id]
            if other_recipients:
                return "delivered"  # Recipient received it - double gray check
            
            return "sent"  # Not delivered yet - single gray check
        else:
            # Viewing as recipient - they have the message
            return "delivered"