# New flattened chat model
The chat model was flattened as part of this refactor: https://github.com/status-im/status-desktop/issues/5286
The goal was to make the model more straightforward (no model inside another model) and to enable drag and drop functionality on the QML side.
The new model is way simpler, but it might be confusing at first glance, because there are no longer Category Items in the model. Instead, every Item is a Chat Item.
So how do we even show or get data for Categories on the QML side?
Read on to find out.
### Item properties
Here are the properties of those Chat Items (from `src/app/modules/main/chat_section/item.nim`):
```nim=
type
Item* = ref object
id: string
name: string
`type`: int
amIChatAdmin: bool
icon: string
color: string
colorId: int # only for oneToOne sections
emoji: string
colorHash: color_hash_model.Model
description: string
lastMessageTimestamp: int
hasUnreadMessages: bool
notificationsCount: int
muted: bool
blocked: bool
active: bool
position: int
categoryId: string
categoryName: string
categoryPosition: int
categoryOpened: bool
highlight: bool
trustStatus: TrustStatus
onlineStatus: OnlineStatus
```
As you can see, it contains mainly what a Chat needs.
This is the interesting part:
```nim=
categoryId: string
categoryName: string
categoryPosition: int
categoryOpened: bool
```
The Category data is **inside** the Chat Item.
This is how we sort and show the Category in the UI.
The one downside is that we duplicate the Category data in each Item, but it's a small price to pay, since it's all small strings, ints or bools.
### Category displaying in QML
Most of the heavy lifting is done in `ui/StatusQ/src/StatusQ/Components/StatusChatList.qml`:
```qml=
StatusListView {
id: statusChatListItems
[...]
model: root.model
section.property: "categoryId"
section.criteria: ViewSection.FullString
...
```
- `root.model` is the Model of Items seen above (file `src/app/modules/main/chat_section/model.nim`)
- `section.property` is the deciding factor whether to show a Category header (StatusChatListCategoryItem.qml)
- this: 
- `section.criteria` is just to let QML know it's a string comparison and to use the full string
The `ListView` is smart enough to **not** repeat the section header (`StatusChatListCategoryItem`) **if** the Items of a same category are in order.
If they are not, it would repeat it.
This is where the `SortFilterProxyModel` comes in.
### Item ordering
The ordering is all handled easily by `SortFilterProxyModel` in the file `ui/StatusQ/src/StatusQ/Components/StatusChatListAndCategories.qml`:
```qml=
model: SortFilterProxyModel {
sourceModel: root.model
sorters: [
RoleSorter {
roleName: "categoryPosition"
priority: 2 // Higher number === higher priority
},
RoleSorter {
roleName: "position"
priority: 1
}
]
}
```
The code is pretty self explanatory. We sort first by `categoryPosition`, then we break ties for Items in the same category using the Chat `position`.
That `position` is both for channels in a category, but also for channels without a Category.
Those orphan channels have a `categoryPosition` of `-1` to be at the top of the channel list, and are also ordered by `position` between each other.
### Getting the Category data in the section delegate
Back in `StatusChatList`, the `delegate` responsible for showing the `StatusChatListCategoryItem` needs to get the Category's data.
Sadly, ListView only gives us the `section` property, that contains `categoryId` in this case.
Here is how we get the remainder of it:
```qml=
readonly property string categoryId: {
// Update category data from here because `dataChanged` signals on the Name do not affect the section
// The section is only affected by CategoryId changes, but it never actually changes, so `categoryName` doesn't update automatically
updateCategoryData(section)
return section
}
property string categoryName: ""
property bool categoryOpened: true
function updateCategoryData(catId) {
categoryName = root.model.sourceModel.getCategoryNameForCategoryId(catId)
categoryOpened = root.model.sourceModel.getCategoryOpenedForCategoryId(catId)
}
```
The code should be self-explanatory, but basically, when the `section` (`categoryId`) changes, which could be when removing a channel from a category or just on start when it gets populated, we call `updateCategoryData`, which fecthes the data from the Model.
The reason we call `root.model.sourceModel` and not just `root.model` is because the `model` itself is the SortProxyModel.
The model function calls do something very simple, for eg:
```nim=
proc getCategoryNameForCategoryId*(self: Model, categoryId: string): string {.slot.} =
let index = self.getItemIdxByCategory(categoryId)
if index == -1:
return
return self.items[index].categoryName
```
`getItemIdxByCategory` gives us the first Item that fits a `categoryId`, then we use that Item to get the `categoryName`.
### Handling Empty Categories
The most keen among you might have thought of a little edge case with using Chat Item data to display the Category: What if the a Category is empty (has no Chat associated)?
Indeed, it would **not** appear with the current model shown above.
That's why we introduce "fake empty chats".
In the module (`src/app/modules/main/chat_section/module.nim`), when we detect that a category has no Chats associated, we add an empty Item like so:
```nim=
proc addEmptyChatItemForCategory(self: Module, category: Category) =
# Add an empty chat item that has the category info
let emptyChatItem = chat_item.initItem(
id = "cat-" & category.id,
name = "",
icon = "",
color = "",
emoji = "",
description = "",
`type` = chat_item.CATEGORY_TYPE,
amIChatAdmin = false,
lastMessageTimestamp = 0,
hasUnreadMessages = false,
notificationsCount = 0,
muted = false,
blocked = false,
active = false,
position = 99,
category.id,
category.name,
category.position,
)
self.view.chatsModel().appendItem(emptyChatItem)
```
It has no Chat info, apart from the Category data. This is enough to show the Category header.
However, the empty Chat Item must **not** be shown in the QML, that's why we set its `type` to `chat_item.CATEGORY_TYPE`, which is `-1`.
Then, in QML, we check that type to set the Loader to `active: false`:
```qml=
Loader {
id: chatLoader
active: model.type !== d.chatTypeCategory && model.categoryOpened
```
The last thing to note about empty Items is that we need to clean them up when a Category does have new actual Chat Items added. We do it in the Model in `updateItemsWithCategoryDetailById`, which is the method called when a Category is edited:
```nim=
if hasCategory and not found:
# This item was removed from the category
if (item.`type` == CATEGORY_TYPE):
# It was an empty item to show the category and it isn't needed anymore
indexesToDelete.add(i)
continue
```
This code is part of the loop over all the Items in the Model.
If the Item is part of the Category being edited, is **not** part of the channels now belonging to the category and is an empty Item, we add it to the array of Items to delete.