---
# System prepended metadata

title: DMs / Private Messaging Dev Plan

---

# 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. 
![](https://hackmd.io/_uploads/BJ85VCLfT.png)


#### 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. 
![](https://hackmd.io/_uploads/ryViVRLMa.png)

### 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.