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