# DMs / Private Messaging Dev Planning
## Overview
The goal of this document is to detail a little bit of a plan to handle the new Direct message feature. The goal is to have a system robust enough to handle both Direct messages and group messages. Think of group messages as Threads on discord, where even after joining you have the full context of the chat.
## Database Schema
This schema is built under the assumption that we want both DM & group chats. As such, we'll have a "Chats" table that serves as a hub to know which users are following a thread of messages of sorts.
### ChatMemberRole
```typescript=
enum ChatMode {
Direct
Group
}
```
### Chats
```typescript=
model Chat {
id Int @id @default(autoincrement())
mode ChatMode @default("Direct")
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastMessageAt DateTime?
hash String // Hash of all members. To be updated when members join/leave
messages ChatMessage[]
members ChatMember[] // Can exit the chat but not add/remove members.
@@index([mode], type: Hash)
@@index([hash], type: Hash)
}
```
### ChatMessage
```typescript=
model ChatMessage {
id Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
content String
userId Int?
user User?
chatId Int
chat Chat
referenceMessageId Int? // Used for replies.
referenceMessage ChatMessage?
isSystemMessage Boolean @default(false) // This is useful to create messages like "User X left the chat" / general in chat notifs
// reactions?
// this relation will not be done via Prisma, should use the entityId/entityType of the File table.
attachments File[]
}
```
### ChatMemberStatus
```typescript=
enum ChatMemberStatus {
Active
Hidden
Blocked
}
```
### ChatMemberRole
```typescript=
enum ChatMemberRole {
Admin
User
}
```
### ChatMember
```typescript=
model ChatMember {
userId Int
user User
chatId Int
chat Chat
role ChatMemberRole @default("Admin")
status ChatMemberStatus @default("Active") // Array to support active+muted
muted Boolean
blockedAt DateTime?
unblockedAt DateTime?
joinedAt DateTime @default(now())
lastViewedAt DateTime @default(now()) // To be updated every time they open the chat.
unreadMessageCount Int
}
```
### Users
```typescript=
model User {
...userModel
chats ChatMember[]
}
```
## Planning / Parts / Specs - v1.0
### Chats
#### Direct chats
Refers to a chat between 2 users. These are considered DMs. A direct chat can later be turned into a Group chat and that'll make all old messages available to the newly added user.
Important values for a direct chat within a ChatMember record are:
* **Role**: Determines that this user can add other participants to the chat. Admins are the only ones to do that. This is to be decided, if we decide a direct convo can never become a group one, this value will be **false** for all users in the direct convo.
* **Muted**: Determines that the user will **not** receive notifications from this chat. Messages still go through, but the user won't be alerted at any point.
* **Hidden**: Determines whether or not to show/hide a chat from the list of open/available chats.
* **BlockedAt/UnblockedAt**: Stops you from getting bothered by another user. They can still send messages but during the blocking period you will not see messages they sent over.
### Starting a "direct" chat
A direct chat refers to a DM as generally known. The goal here is to keep up the same API for group and direct messages, which is the chat API.

#### Who can start a chat
Any user that has not been banned/muted can start a chat. If you have hiddena user, blockedAt will be marked automatically and you will not receive messages from that person even when they try to.
#### General process:
1. The user will click on "Chat with this user" button on the user's profile card or click "Start a new chat" in the chat page prompting them to look for a user.
2. We will land the user in the following URL: `/chat?userId[]=1`. At this point **a chat has not been created**. The route should
* Look into the DB for a chat with the `direct` mode where user A (session user) and B (query user) are the only members. This can be done using the **hash**
* If a chat is found, then the user gets redirected to the appropiate url `/chat/:id`.
3. User can send messages to the chat/other user. If a chat exists, then the message is added to it. If it does not, then it is created, members are created and the message is added to the chat. **Note:**
* If user A has hidden B a chat cannot be initiated. If the chat was already active, the "hiding" member will be marked as "blockedAt" that date. when unhidding / unblocking mark the relevant value.
4. By default, chat members are created with the muted = false & hidden = false flags, meaning all chats will appear into the open chat list.
5. When a chat is created a hash should be created that ensures this chat is not replicated. The hash is to be created using both user IDs. Sort by ID of the users => `{userA}-{userB}` (or a more advanced hash maybe, but that's enough). The same principle applies for group chats. Sorting is important, it makes it so that you never get `{userB}-{userA}`
:::info
When starting a chat from the "new chat button" at the top, a modal should pop to search for users. Same URL redirection should happen (`/chat?userId[]=1`)
:::
### Selecting one of your existing chats
Users will have a list of open chats on their sidebar. This list will only include chats in which they're members of and **are not** hidden. Users can still search among their hidden chats via a search button / start a new chat button. Blocked chats should **not** appear within hidden chats.

### Closing a chat
At least for the time being, all closing a chat will do is **mark it as hidden**, meaning, no messages will be deleted. If you open it up again, all of your previous messages will be restored.
## Endpoints
### chat.getAll
Returns all chats for a user.
#### Schema
```typescript=
export const chatGetAllSchema = z.object({
userId: z.number(),
status: z.nativeEnum(ChatMemberStatus)
});
```
#### Response
```typescript=
type res = { items: ChatMember[], cursor: int };
```
### chat.getUnreadMessages
Returns the count of unread messages based off of `ChatMember.unreadMessages` sum
#### Schema
```typescript=
export const chatGetUnreadMessagesSchema = z.object({
userId: z.number(),
});
```
#### Response
```typescript=
type res = { unreadMessages: int };
```
### chat.sendMessage
Sends a new message to a chat. If chat ID is provided, the message is appended to that one. Otherwise, a new chat is created with the relevant message as the 1st message and the targetUserIds being the members (along with the sender).
#### Schema
```typescript=
export const chatSendMessageSchema = z.object({
userId: z.number(),
content: z.string(),
chatId: z.number().optional(),
targetUserIds: z.array(z.number()).optional(),
})
.refine(data => data.chatId !== undefined || data.targetuserIds?.length > 0,
{ message: "Please provide a target chat or users." }
);
```
#### Response
```typescript=
type res = {
message: { id: number, ...messageProps },
chat: { id: number, ...chatProps }
};
```
### chat.getMessages
Returns a list of paginated messages within a chat
#### Schema
```typescript=
export const chatGetMessageSchema = z.object({
chatId: z.number(),
before: z.date().optional,
after: z.date().optional,
})
```
#### Response
```typescript=
type res = { items: Messages[], cursor: Date };
```
## v2.0 (The future)
### DM File sharing
Unkown at the time being if this is a priority or a nice to have at best. Leaving it for v2 assuming the latter.
### Chat reporting
Users should be able to report other users' chats / messages.
### Group Chats
Following the same principle as before, group chats should be possible. The main difference is that when creating a new chat, we should allow the user **to select multiple people**. If 2+ people are selected, we should automatically make the new chat a "group chat" and create all the relevant members.
Note that the rules of blocked users should apply just the same. A membership to the chat is crated with the blocked status and the user is **not** bothered by it. This should happen if even 1 of the participants is hidden by you.