Skip to main content

Message Edit & Delete

Adding edit and delete capabilities to messages was a must-have feature for me. We've all sent messages with typos or regretted something we've said, so implementing these features really improved the user experience.

How Message Edit Works

The edit feature allows users to modify their own messages after sending them. Here's how I implemented it:

// In ChatContext.jsx
const editMessage = async (chatId, messageId, newContent) => {
if (!chatId || !messageId || !newContent.trim()) return;

try {
const messageRef = ref(db, `messages/${chatId}/${messageId}`);
const snapshot = await get(messageRef);

if (!snapshot.exists()) {
throw new Error('Message does not exist');
}

const messageData = snapshot.val();

// Check if the user can edit this message
if (messageData.sender !== user.uid && !isCurrentUserAdmin()) {
throw new Error('You cannot edit this message');
}

// Log the edit
await logMessageEdit(chatId, messageId, messageData.content, newContent);

// Update the message
await update(messageRef, {
content: newContent,
edited: true,
editedAt: serverTimestamp()
});

// If this is the last message, update the chat's last message
if (currentChat?.lastMessage?.timestamp === messageData.timestamp) {
await update(ref(db, `chats/${chatId}`), {
lastMessage: {
...currentChat.lastMessage,
content: newContent
}
});
}

return true;
} catch (error) {
console.error('Error editing message:', error);
return false;
}
};

How Message Delete Works

For deletion, I implemented a "soft delete" approach - messages aren't actually removed from the database, but marked as deleted:

// In ChatContext.jsx
const deleteMessage = async (chatId, messageId) => {
if (!chatId || !messageId) return;

try {
const messageRef = ref(db, `messages/${chatId}/${messageId}`);
const snapshot = await get(messageRef);

if (!snapshot.exists()) {
throw new Error('Message does not exist');
}

const messageData = snapshot.val();

// Check if the user can delete this message
if (messageData.sender !== user.uid && !isCurrentUserAdmin()) {
throw new Error('You cannot delete this message');
}

// Log the deletion
await logMessageDelete(chatId, messageId, messageData.content);

// Mark as deleted (soft delete)
await update(messageRef, {
deleted: true,
content: 'This message has been deleted',
deletedAt: serverTimestamp()
});

return true;
} catch (error) {
console.error('Error deleting message:', error);
return false;
}
};

Message UI Components

Users can access edit and delete options through a context menu on their own messages:

// In MessageItem.jsx
function MessageItem({ message, isOwnMessage }) {
const [showOptions, setShowOptions] = useState(false);
const { editMessage, deleteMessage } = useChat();
const [isEditing, setIsEditing] = useState(false);
const [editContent, setEditContent] = useState('');

const handleEdit = () => {
setEditContent(message.content);
setIsEditing(true);
setShowOptions(false);
};

const handleDelete = async () => {
if (window.confirm('Are you sure you want to delete this message?')) {
await deleteMessage(currentChat.id, message.id);
}
setShowOptions(false);
};

const handleSaveEdit = async () => {
if (editContent.trim() && editContent !== message.content) {
await editMessage(currentChat.id, message.id, editContent);
}
setIsEditing(false);
};

return (
<div
className={`${styles.messageItem} ${isOwnMessage ? styles.ownMessage : ''}`}
onMouseEnter={() => setShowOptions(true)}
onMouseLeave={() => setShowOptions(false)}
>
{isEditing ? (
<div className={styles.editContainer}>
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
autoFocus
/>
<div className={styles.editActions}>
<button onClick={() => setIsEditing(false)}>Cancel</button>
<button onClick={handleSaveEdit}>Save</button>
</div>
</div>
) : (
<>
<div className={styles.content}>
{message.deleted ? (
<span className={styles.deletedMessage}>
This message has been deleted
</span>
) : (
message.content
)}
</div>

{message.edited && !message.deleted && (
<span className={styles.editedTag}>
(edited)
</span>
)}

{showOptions && isOwnMessage && !message.deleted && (
<div className={styles.messageOptions}>
<button onClick={handleEdit}>
<EditIcon /> Edit
</button>
<button onClick={handleDelete}>
<DeleteIcon /> Delete
</button>
</div>
)}
</>
)}
</div>
);
}

Admin Capabilities

I also gave admins the ability to edit and delete any message:

// In MessageItem.jsx - Modified version with admin capabilities
function MessageItem({ message }) {
// ... other code

const { user, isCurrentUserAdmin } = useAuth();
const isOwnMessage = message.sender === user.uid;
const canModify = isOwnMessage || isCurrentUserAdmin();

// ... other code

return (
<div className={styles.messageItem}>
{/* ... other elements */}

{showOptions && canModify && !message.deleted && (
<div className={styles.messageOptions}>
<button onClick={handleEdit}>
<EditIcon /> {isOwnMessage ? 'Edit' : 'Admin Edit'}
</button>
<button onClick={handleDelete}>
<DeleteIcon /> {isOwnMessage ? 'Delete' : 'Admin Delete'}
</button>
</div>
)}
</div>
);
}

Message History Tracking

For accountability, I set up a logging system that tracks all edits and deletions:

// In ChatContext.jsx
const logMessageEdit = async (chatId, messageId, originalContent, newContent) => {
try {
const logRef = push(ref(db, `logs/${chatId}`));

await set(logRef, {
messageId,
type: 'EDIT',
performedBy: user.uid,
performedByName: user.displayName,
timestamp: serverTimestamp(),
originalContent,
newContent
});
} catch (error) {
console.error('Error creating edit log:', error);
}
};

const logMessageDelete = async (chatId, messageId, originalContent) => {
try {
const logRef = push(ref(db, `logs/${chatId}`));

await set(logRef, {
messageId,
type: 'DELETE',
performedBy: user.uid,
performedByName: user.displayName,
timestamp: serverTimestamp(),
originalContent
});
} catch (error) {
console.error('Error creating delete log:', error);
}
};

UI Considerations

I wanted to make it clear when messages have been edited or deleted:

/* MessageItem.module.css */
.deletedMessage {
font-style: italic;
color: #999;
}

.editedTag {
font-size: 11px;
color: #999;
margin-left: 5px;
}

.messageOptions {
position: absolute;
right: 10px;
top: 5px;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
overflow: hidden;
z-index: 5;
}

.messageOptions button {
display: flex;
align-items: center;
padding: 8px 12px;
background: none;
border: none;
width: 100%;
text-align: left;
cursor: pointer;
}

.messageOptions button:hover {
background-color: #f5f5f5;
}

Technical Challenges

Handling Edit UI

Getting the edit UI right was a bit tricky - I wanted a smooth transition between viewing and editing:

// In MessageItem.jsx - Edit mode transition
useEffect(() => {
if (isEditing) {
// Auto-resize textarea to match content
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}

// Focus and place cursor at the end
if (textarea) {
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
}
}
}, [isEditing]);

Soft Deletes vs. Hard Deletes

I debated whether to fully remove messages or just mark them as deleted. I went with soft deletes for a few reasons:

  1. It preserves conversation flow - gaps in the chat can be confusing
  2. It provides accountability - we know who deleted what and when
  3. It allows for potential message recovery in the future
  4. It's consistent with other chat platforms users are familiar with

Race Conditions

When multiple users are editing or deleting messages simultaneously, race conditions can occur. I added some safeguards:

// In editMessage - Add transaction to prevent race conditions
const editMessage = async (chatId, messageId, newContent) => {
// ... existing code

try {
const messageRef = ref(db, `messages/${chatId}/${messageId}`);

// Use transaction to handle concurrent edits
const success = await runTransaction(messageRef, (currentData) => {
if (!currentData) return null; // Abort if message doesn't exist

// Check permissions again inside transaction
if (currentData.sender !== user.uid && !isCurrentUserAdmin()) {
return null; // Abort transaction
}

// Only proceed if message hasn't been deleted since we started
if (currentData.deleted) return null;

// Update the message
currentData.content = newContent;
currentData.edited = true;
currentData.editedAt = serverTimestamp();

return currentData;
});

// Log the edit only if transaction succeeded
if (success) {
await logMessageEdit(chatId, messageId, messageData.content, newContent);
}

return success;
} catch (error) {
console.error('Error editing message:', error);
return false;
}
};

Future Improvements

If I had more time to work on these features, I'd add:

  1. Edit History - Allow users (or at least admins) to view the full edit history of a message
  2. Time Limits - Add configurable time limits for editing/deleting (e.g., can only edit within 5 minutes)
  3. Edit Indicators - Show when someone is currently editing a message, similar to typing indicators
  4. Restore Option - Give admins the ability to restore deleted messages
  5. Bulk Delete - Allow admins to delete multiple messages at once
  6. Forward Message - Add ability to forward messages to other chats

Overall, I'm pretty happy with how the edit and delete features turned out. They provide the essential functionality that users expect while maintaining the integrity of conversations.