---
# System prepended metadata

title: New flattened chat model

---

# 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: ![](https://i.imgur.com/NK7oyPY.png)
- `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.