~aleteoryx/muditaos

196c02686ae5b344e6814800b7cb8aa2473f15ea — Przemyslaw Brudny 5 years ago 2cff347
[EGD-2395] Added BottomTop orientation support for listView. Created SMSThreadViewWindow and SMSOutputWidget. MessagesStyle moved from global Style.hpp. Fixes in Text. ListView fixes, BoxLayout callback for requestedSize. Added smsInput into list. Drafts handling added.
38 files changed, 1190 insertions(+), 786 deletions(-)

M changelog.md
M module-apps/DatabaseModel.hpp
M module-apps/application-messages/CMakeLists.txt
M module-apps/application-messages/data/MessagesStyle.hpp
A module-apps/application-messages/models/SMSThreadModel.cpp
A module-apps/application-messages/models/SMSThreadModel.hpp
M module-apps/application-messages/models/ThreadsModel.cpp
M module-apps/application-messages/models/ThreadsSearchResultsModel.cpp
M module-apps/application-messages/widgets/BaseThreadItem.cpp
M module-apps/application-messages/widgets/SMSInputWidget.cpp
M module-apps/application-messages/widgets/SMSInputWidget.hpp
A module-apps/application-messages/widgets/SMSOutputWidget.cpp
A module-apps/application-messages/widgets/SMSOutputWidget.hpp
M module-apps/application-messages/windows/NewMessage.cpp
M module-apps/application-messages/windows/SMSThreadViewWindow.cpp
M module-apps/application-messages/windows/SMSThreadViewWindow.hpp
M module-apps/application-phonebook/widgets/InputBoxWithLabelAndIconWidget.cpp
M module-apps/application-phonebook/widgets/PhonebookItem.cpp
M module-apps/application-phonebook/windows/PhonebookIceContacts.cpp
M module-apps/application-phonebook/windows/PhonebookIceContacts.hpp
M module-db/CMakeLists.txt
M module-db/Common/Common.hpp
M module-db/Interface/SMSRecord.cpp
M module-db/Interface/SMSRecord.hpp
M module-db/Tables/SMSTable.cpp
M module-db/Tables/SMSTable.hpp
A module-db/queries/messages/sms/QuerySMSGetForList.cpp
A module-db/queries/messages/sms/QuerySMSGetForList.hpp
M module-db/tests/SMSRecord_tests.cpp
M module-db/tests/SMSTable_tests.cpp
M module-gui/gui/widgets/BoxLayout.cpp
M module-gui/gui/widgets/BoxLayout.hpp
M module-gui/gui/widgets/ListView.cpp
M module-gui/gui/widgets/ListView.hpp
M module-gui/gui/widgets/Rect.hpp
M module-gui/gui/widgets/Style.hpp
M module-gui/gui/widgets/Text.cpp
M module-gui/test/test-google/test-gui-listview.cpp
M changelog.md => changelog.md +1 -0
@@ 19,6 19,7 @@

* `[system]` Timer API - linked to timer, same for Services and Applications. Updated docs
* `[system]` Removed `using std` and `using cpp_freertos` from commonly used headers
* `[messages]` Refactored messages SMS thread window to use ListView. 

### Fixed


M module-apps/DatabaseModel.hpp => module-apps/DatabaseModel.hpp +1 -1
@@ 16,7 16,7 @@ namespace app
      protected:
        /// Pointer to application that owns the model
        Application *application = nullptr;
        uint32_t recordsCount;
        uint32_t recordsCount    = 0;
        std::vector<std::shared_ptr<T>> records;
        uint32_t modelIndex = 0;


M module-apps/application-messages/CMakeLists.txt => module-apps/application-messages/CMakeLists.txt +4 -0
@@ 18,12 18,14 @@ target_sources( ${PROJECT_NAME}
        "widgets/SMSTemplateModel.cpp"
        "widgets/SMSTemplateItem.cpp"
        "widgets/SMSInputWidget.cpp"
        "widgets/SMSOutputWidget.cpp"
        "widgets/SearchResultsItem.cpp"
        "widgets/BaseThreadItem.cpp"

        "models/BaseThreadsRecordModel.cpp"
        "models/ThreadsModel.cpp"
        "models/ThreadsSearchResultsModel.cpp"
        "models/SMSThreadModel.cpp"
        
        "windows/MessagesMainWindow.cpp"
        "windows/SMSThreadViewWindow.cpp"


@@ 40,7 42,9 @@ target_sources( ${PROJECT_NAME}
        "ApplicationMessages.hpp"
        "data/MessagesStyle.hpp"
        "models/ThreadsModel.hpp"
        "models/SMSThreadModel.hpp"
        "widgets/ThreadItem.hpp"
        "widgets/SMSInputWidget.hpp"
        "widgets/SMSOutputWidget.hpp"
)


M module-apps/application-messages/data/MessagesStyle.hpp => module-apps/application-messages/data/MessagesStyle.hpp +38 -6
@@ 17,6 17,8 @@ namespace style

        namespace threadItem
        {
            constexpr uint32_t sms_thread_item_h = 100;

            constexpr uint32_t topMargin    = 16;
            constexpr uint32_t bottomMargin = 13;



@@ 52,14 54,44 @@ namespace style

        namespace smsInput
        {
            constexpr uint32_t min_h               = 40;
            constexpr uint32_t default_input_w     = 405;
            constexpr uint32_t default_input_h     = 30;
            constexpr uint32_t bottom_padding      = 5;
            constexpr uint32_t max_input_h         = default_input_h * 4 + bottom_padding;
            constexpr uint32_t reply_bottom_margin = 5;
            constexpr gui::Length min_h                   = 40;
            constexpr gui::Length default_input_w         = 395;
            constexpr gui::Length default_input_h         = 30;
            constexpr gui::Length bottom_padding          = 5;
            constexpr gui::Length max_input_h             = default_input_h * 4 + bottom_padding;
            constexpr gui::Length reply_bottom_margin     = 5;
            constexpr gui::Length new_sms_vertical_spacer = 25;
        } // namespace smsInput

        namespace smsOutput
        {
            constexpr gui::Length sms_radius                   = 7;
            constexpr gui::Length default_h                    = 30;
            constexpr gui::Length sms_max_width                = 320;
            constexpr gui::Length sms_h_padding                = 15;
            constexpr gui::Length sms_h_big_padding            = 25;
            constexpr gui::Length sms_v_padding                = 10;
            constexpr gui::Length sms_vertical_spacer          = 10;
            constexpr gui::Length sms_error_icon_left_margin   = 5;
            constexpr gui::Length sms_error_icon_right_margin  = 2;
            const inline gui::Padding sms_left_bubble_padding  = gui::Padding(smsOutput::sms_h_big_padding,
                                                                             smsOutput::sms_v_padding,
                                                                             smsOutput::sms_h_padding,
                                                                             smsOutput::sms_v_padding);
            const inline gui::Padding sms_right_bubble_padding = gui::Padding(smsOutput::sms_h_padding,
                                                                              smsOutput::sms_v_padding,
                                                                              smsOutput::sms_h_big_padding,
                                                                              smsOutput::sms_v_padding);
        } // namespace smsOutput

        namespace smsList
        {
            constexpr uint32_t x = style::window::default_left_margin;
            constexpr uint32_t y = style::header::height;
            constexpr uint32_t h = style::window::default_body_height;
            constexpr uint32_t w = style::listview::body_width_with_scroll;
        } // namespace smsList

        namespace templates
        {
            namespace list

A module-apps/application-messages/models/SMSThreadModel.cpp => module-apps/application-messages/models/SMSThreadModel.cpp +123 -0
@@ 0,0 1,123 @@
#include <module-services/service-db/messages/QueryMessage.hpp>
#include <module-services/service-db/api/DBServiceAPI.hpp>
#include <module-db/queries/messages/sms/QuerySMSGetCountByThreadID.hpp>
#include <module-db/queries/messages/sms/QuerySMSGetForList.hpp>

#include <application-messages/widgets/SMSOutputWidget.hpp>
#include <module-apps/application-messages/ApplicationMessages.hpp>
#include "application-messages/data/MessagesStyle.hpp"
#include "SMSThreadModel.hpp"
#include "ListView.hpp"

SMSThreadModel::SMSThreadModel(app::Application *app) : DatabaseModel(app)
{
    smsInput = new gui::SMSInputWidget(application);
}

SMSThreadModel::~SMSThreadModel()
{
    delete smsInput;
}

unsigned int SMSThreadModel::getMinimalItemHeight() const
{
    return style::messages::smsOutput::default_h;
}

gui::ListItem *SMSThreadModel::getItem(gui::Order order)
{
    std::shared_ptr<SMSRecord> sms = getRecord(order);

    if (sms == nullptr) {
        return nullptr;
    }

    // Small hack to trick current model logic -> adding empty row into query result for Input Widget
    if (sms->type == SMSType::INPUT) {
        addReturnNumber();
        return smsInput;
    }

    return new gui::SMSOutputWidget(application, sms);
}

unsigned int SMSThreadModel::requestRecordsCount()
{
    return recordsCount;
}

void SMSThreadModel::requestRecords(uint32_t offset, uint32_t limit)
{
    auto query = std::make_unique<db::query::SMSGetForList>(smsThreadID, offset, limit);
    query->setQueryListener(
        db::QueryCallback::fromFunction([this](auto response) { return handleQueryResponse(response); }));
    DBServiceAPI::GetQuery(application, db::Interface::Name::SMS, std::move(query));
}

bool SMSThreadModel::updateRecords(std::unique_ptr<std::vector<SMSRecord>> records)
{
    DatabaseModel::updateRecords(std::move(records));
    list->onProviderDataUpdate();
    return true;
}

auto SMSThreadModel::handleQueryResponse(db::QueryResult *queryResult) -> bool
{
    auto msgResponse = dynamic_cast<db::query::SMSGetForListResult *>(queryResult);
    assert(msgResponse != nullptr);

    auto records_data = msgResponse->getResults();

    // If list record count has changed we need to rebuild list.
    if (recordsCount != (msgResponse->getCount() + 1)) {
        // Additional one element for SMSInputWidget.
        recordsCount = msgResponse->getCount() + 1;
        list->rebuildList(style::listview::RebuildType::Full, 0, true);
        return false;
    }

    resetInputWidget();

    if (msgResponse->getDraft().isValid()) {
        smsInput->draft = msgResponse->getDraft().type == SMSType::DRAFT
                              ? std::optional<SMSRecord>{msgResponse->getDraft()}
                              : std::nullopt;
        smsInput->displayDraftMessage();
    }

    auto records = std::make_unique<std::vector<SMSRecord>>(records_data.begin(), records_data.end());

    return this->updateRecords(std::move(records));
}

void SMSThreadModel::addReturnNumber()
{
    if (number != nullptr) {
        smsInput->number = std::move(number);
    }

    smsInput->activatedCallback = [this]([[maybe_unused]] gui::Item &item) {
        auto app = dynamic_cast<app::ApplicationMessages *>(application);
        assert(app != nullptr);
        assert(smsInput->number != nullptr);
        if (app->handleSendSmsFromThread(*smsInput->number, smsInput->inputText->getText())) {
            LOG_ERROR("handleSendSmsFromThread failed");
        }
        smsInput->inputText->clear();
        smsInput->clearDraftMessage();
        return true;
    };
}

void SMSThreadModel::handleDraftMessage()
{
    smsInput->handleDraftMessage();
}

void SMSThreadModel::resetInputWidget()
{
    smsInput->setFocus(false);
    smsInput->setVisible(true);
    smsInput->clearNavigationItem(gui::NavigationDirection::UP);
    smsInput->clearNavigationItem(gui::NavigationDirection::DOWN);
}

A module-apps/application-messages/models/SMSThreadModel.hpp => module-apps/application-messages/models/SMSThreadModel.hpp +30 -0
@@ 0,0 1,30 @@
#pragma once

#include "DatabaseModel.hpp"
#include "Application.hpp"
#include "ListItemProvider.hpp"
#include "Interface/SMSRecord.hpp"
#include <application-messages/widgets/SMSInputWidget.hpp>

class SMSThreadModel : public app::DatabaseModel<SMSRecord>, public gui::ListItemProvider
{
  public:
    unsigned int smsThreadID      = 0;
    gui::SMSInputWidget *smsInput = nullptr;
    std::unique_ptr<utils::PhoneNumber::View> number;

    SMSThreadModel(app::Application *app);
    ~SMSThreadModel() override;

    void addReturnNumber();
    void handleDraftMessage();
    void resetInputWidget();

    auto handleQueryResponse(db::QueryResult *) -> bool;

    unsigned int requestRecordsCount() override;
    bool updateRecords(std::unique_ptr<std::vector<SMSRecord>> records) override;
    void requestRecords(uint32_t offset, uint32_t limit) override;
    unsigned int getMinimalItemHeight() const override;
    gui::ListItem *getItem(gui::Order order) override;
};

M module-apps/application-messages/models/ThreadsModel.cpp => module-apps/application-messages/models/ThreadsModel.cpp +2 -2
@@ 2,17 2,17 @@
#include "InputEvent.hpp"
#include "OptionWindow.hpp"
#include "application-messages/data/SMSdata.hpp"
#include "application-messages/data/MessagesStyle.hpp"
#include "application-messages/widgets/ThreadItem.hpp"
#include "application-messages/windows/ThreadWindowOptions.hpp"
#include <module-services/service-db/api/DBServiceAPI.hpp>
#include <module-services/service-db/messages/DBThreadMessage.hpp>

ThreadsModel::ThreadsModel(app::Application *app) : BaseThreadsRecordModel(app)
{}

auto ThreadsModel::getMinimalItemHeight() const -> unsigned int
{
    return style::window::messages::sms_thread_item_h;
    return style::messages::threadItem::sms_thread_item_h;
}

auto ThreadsModel::getItem(gui::Order order) -> gui::ListItem *

M module-apps/application-messages/models/ThreadsSearchResultsModel.cpp => module-apps/application-messages/models/ThreadsSearchResultsModel.cpp +2 -1
@@ 6,6 6,7 @@
#include "service-db/api/DBServiceAPI.hpp"
#include <module-db/queries/messages/threads/QueryThreadsSearch.hpp>
#include <module-apps/application-messages/ApplicationMessages.hpp>
#include "application-messages/data/MessagesStyle.hpp"

namespace gui::model
{


@@ 15,7 16,7 @@ namespace gui::model

    auto ThreadsSearchResultsModel::getMinimalItemHeight() const -> unsigned int
    {
        return style::window::messages::sms_thread_item_h;
        return style::messages::threadItem::sms_thread_item_h;
    }

    auto ThreadsSearchResultsModel::getItem(gui::Order order) -> gui::ListItem *

M module-apps/application-messages/widgets/BaseThreadItem.cpp => module-apps/application-messages/widgets/BaseThreadItem.cpp +2 -2
@@ 8,8 8,8 @@ namespace gui
    {
        using namespace style;
        setMargins(Margins(0, style::margins::small, 0, style::margins::small));
        setMinimumSize(window::default_body_width, style::window::messages::sms_thread_item_h);
        setMaximumSize(window::default_body_width, style::window::messages::sms_thread_item_h);
        setMinimumSize(window::default_body_width, style::messages::threadItem::sms_thread_item_h);
        setMaximumSize(window::default_body_width, style::messages::threadItem::sms_thread_item_h);

        setRadius(0);
        setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM | RectangleEdgeFlags::GUI_RECT_EDGE_TOP);

M module-apps/application-messages/widgets/SMSInputWidget.cpp => module-apps/application-messages/widgets/SMSInputWidget.cpp +104 -14
@@ 7,36 7,48 @@
#include <i18/i18.hpp>
#include <Font.hpp>
#include <utility>
#include <algorithm>

#include "TextParse.hpp"

namespace gui
{

    SMSInputWidget::SMSInputWidget(Item *parent, app::Application *application) : HBox(parent, 0, 0, 0, 0)
    SMSInputWidget::SMSInputWidget(app::Application *application) : application(application)
    {

        setMinimumSize(style::window::default_body_width, style::messages::smsInput::min_h);
        setMaximumHeight(style::messages::smsInput::max_input_h);
        setMargins(Margins(0, style::window::messages::new_sms_vertical_spacer, 0, 0));
        setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM);
        setMargins(Margins(0, style::messages::smsInput::new_sms_vertical_spacer, 0, 0));
        setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);

        body = new HBox(this, 0, 0, 0, 0);
        body->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM);
        body->setMaximumSize(style::window::default_body_width, style::messages::smsInput::max_input_h);

        inputText = new gui::Text(this, 0, 0, 0, 0, "", ExpandMode::EXPAND_UP);
        deleteByList = false;

        inputText = new gui::Text(body, 0, 0, 0, 0, "", ExpandMode::EXPAND_UP);
        inputText->setMaximumSize(style::messages::smsInput::default_input_w, style::messages::smsInput::max_input_h);
        inputText->setMinimumSize(style::messages::smsInput::default_input_w,
                                  style::messages::smsInput::default_input_h);
        inputText->setMaximumHeight(style::messages::smsInput::max_input_h);
        inputText->setFont(style::window::font::medium);
        inputText->setPadding(Padding(0, 0, 0, style::messages::smsInput::bottom_padding));
        inputText->setPenFocusWidth(style::window::default_border_focus_w);
        inputText->setPenWidth(style::window::default_border_focus_w);
        inputText->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);

        replyImage = new Image(this, 0, 0, "messages_reply");
        replyImage = new Image(body, 0, 0, "messages_reply");
        replyImage->setAlignment(Alignment(gui::Alignment::Vertical::Bottom));
        replyImage->setMargins(Margins(0, 0, 0, style::messages::smsInput::reply_bottom_margin));
        replyImage->activeItem = false;

        inputText->inputCallback = [=](Item &, const InputEvent &event) {
        inputCallback = [&]([[maybe_unused]] Item &item, const InputEvent &event) { return inputText->onInput(event); };

        focusChangedCallback = [this]([[maybe_unused]] Item &item) {
            setFocusItem(focus ? body : nullptr);
            return true;
        };

        inputText->inputCallback = [this, application]([[maybe_unused]] Item &, const InputEvent &event) {
            if (event.state == InputEvent::State::keyReleasedShort && event.keyCode == KeyCode::KEY_LF) {
                auto app = dynamic_cast<app::ApplicationMessages *>(application);
                assert(app != nullptr);


@@ 45,11 57,14 @@ namespace gui
            return false;
        };

        inputText->focusChangedCallback = [=](Item &) -> bool {
        inputText->focusChangedCallback = [this, application]([[maybe_unused]] Item &) -> bool {
            assert(body != nullptr);
            assert(application != nullptr);

            if (inputText->focus) {

                application->getCurrentWindow()->setBottomBarText(utils::localize.get("sms_reply"),
                                                                  BottomBar::Side::CENTER);
                application->getWindow(gui::name::window::thread_view)
                    ->setBottomBarText(utils::localize.get("sms_reply"), BottomBar::Side::CENTER);

                inputText->setInputMode(new InputMode(
                    {InputMode::ABC, InputMode::abc, InputMode::digit},


@@ 57,7 72,7 @@ namespace gui
                    [=]() { application->getCurrentWindow()->bottomBarRestoreFromTemporaryMode(); },
                    [=]() { application->getCurrentWindow()->selectSpecialCharacter(); }));

                if (inputText->getText().getLine() == utils::localize.get("sms_temp_reply")) {
                if (inputText->getText() == utils::localize.get("sms_temp_reply")) {
                    inputText->clear();
                }
            }


@@ 72,11 87,86 @@ namespace gui
                    }
                }

                application->getCurrentWindow()->clearBottomBarText(BottomBar::Side::CENTER);
                application->getWindow(gui::name::window::thread_view)->clearBottomBarText(BottomBar::Side::CENTER);
            }

            return true;
        };
    }

    void SMSInputWidget::handleDraftMessage()
    {
        if (const auto &text = inputText->getText(); text.empty() || (text == utils::localize.get("sms_temp_reply"))) {
            clearDraftMessage();
        }
        else {
            updateDraftMessage(text);
        }
    }

    void SMSInputWidget::clearDraftMessage()
    {
        if (!draft.has_value()) {
            displayDraftMessage();
            return;
        }

        auto app = dynamic_cast<app::ApplicationMessages *>(application);
        assert(app != nullptr);
        if (const auto removed = app->removeDraft(draft.value()); removed) {
            draft = std::nullopt;
            displayDraftMessage();
        }
    }

    void SMSInputWidget::displayDraftMessage() const
    {
        if (draft.has_value()) {
            inputText->setText(draft->body);
        }
        else {
            inputText->clear();
        }
    }

    void SMSInputWidget::updateDraftMessage(const UTF8 &inputText)
    {
        auto app = dynamic_cast<app::ApplicationMessages *>(application);
        assert(app != nullptr);
        assert(number != nullptr);

        if (draft.has_value()) {
            app->updateDraft(draft.value(), inputText);
        }
        else {
            const auto &[draft, success] = app->createDraft(*number, inputText);
            if (success) {
                this->draft = draft;
            }
        }
    }

    auto SMSInputWidget::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool
    {
        body->setPosition(0, 0);
        body->setSize(newDim.w, newDim.h);

        return true;
    }

    auto SMSInputWidget::handleRequestResize([[maybe_unused]] const Item *child,
                                             unsigned short request_w,
                                             unsigned short request_h) -> Size
    {
        request_h =
            std::clamp((Length)request_h, style::messages::smsInput::min_h, style::messages::smsInput::max_input_h);

        setMinimumHeight(request_h);
        if (parent != nullptr) {
            requestSize(request_w, request_h);
        }

        return Size(request_w, request_h);
    }

} /* namespace gui */

M module-apps/application-messages/widgets/SMSInputWidget.hpp => module-apps/application-messages/widgets/SMSInputWidget.hpp +18 -4
@@ 4,20 4,34 @@

#include "Text.hpp"
#include "Image.hpp"
#include "ListItem.hpp"
#include <BoxLayout.hpp>
#include "Interface/SMSRecord.hpp"

namespace gui
{

    class SMSInputWidget : public HBox
    class SMSInputWidget : public ListItem
    {
        gui::Image *replyImage = nullptr;
        app::Application *application = nullptr;
        HBox *body                    = nullptr;
        gui::Image *replyImage        = nullptr;

      public:
        gui::Text *inputText = nullptr;
        std::optional<SMSRecord> draft; // draft message of the thread we are showing, if exists.
        std::unique_ptr<utils::PhoneNumber::View> number = nullptr;

        SMSInputWidget(Item *parent, app::Application *application);
        virtual ~SMSInputWidget() = default;
        SMSInputWidget(app::Application *application);
        ~SMSInputWidget() override = default;

        void handleDraftMessage();
        void clearDraftMessage();
        void updateDraftMessage(const UTF8 &inputText);
        void displayDraftMessage() const;

        auto onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool override;
        auto handleRequestResize(const Item *, unsigned short request_w, unsigned short request_h) -> Size override;
    };

} /* namespace gui */

A module-apps/application-messages/widgets/SMSOutputWidget.cpp => module-apps/application-messages/widgets/SMSOutputWidget.cpp +157 -0
@@ 0,0 1,157 @@
#include <application-messages/ApplicationMessages.hpp>
#include "application-messages/windows/OptionsMessages.hpp"
#include <OptionWindow.hpp>
#include "SMSOutputWidget.hpp"
#include "application-messages/data/MessagesStyle.hpp"

#include <Style.hpp>
#include <time/time_conversion.hpp>

namespace gui
{

    SMSOutputWidget::SMSOutputWidget(app::Application *application, const std::shared_ptr<SMSRecord> &record)
    {
        setMinimumSize(style::window::default_body_width, style::messages::smsOutput::default_h);
        setMargins(Margins(0, style::messages::smsOutput::sms_vertical_spacer, 0, 0));
        setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);

        body = new HBox(this, 0, 0, 0, 0);
        body->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
        body->setMaximumSize(style::window::default_body_width, style::window::default_body_height);

        smsBubble = new TextBubble(nullptr, 0, 0, 0, 0);
        smsBubble->setMaximumSize(style::messages::smsOutput::sms_max_width, style::window::default_body_height);
        smsBubble->setAlignment(Alignment(Alignment::Vertical::Center));
        smsBubble->setTextType(TextType::MULTI_LINE);
        smsBubble->setRadius(style::messages::smsOutput::sms_radius);
        smsBubble->setFont(style::window::font::medium);
        smsBubble->setPenFocusWidth(style::window::default_border_focus_w);
        smsBubble->setPenWidth(style::window::default_border_rect_no_focus);
        smsBubble->setPadding(style::messages::smsOutput::sms_right_bubble_padding);

        LOG_DEBUG("ADD SMS TYPE: %d", static_cast<int>(record->type));
        switch (record->type) {
        case SMSType::QUEUED:
            // Handle in the same way as case below. (pending sending display as already sent)
            [[fallthrough]];
        case SMSType::OUTBOX:
            smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
            body->setReverseOrder(true);
            body->addWidget(smsBubble);
            timeLabelBuild(record->date);
            break;
        case SMSType::INBOX:
            smsBubble->setPadding(style::messages::smsOutput::sms_left_bubble_padding);
            smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_LEFT);
            body->setReverseOrder(false);
            body->addWidget(smsBubble);
            timeLabelBuild(record->date);
            break;
        case SMSType::FAILED:
            smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
            body->setReverseOrder(true);
            errorIconBuild();
            body->addWidget(smsBubble);
            break;
        case SMSType::DRAFT:
            LOG_ERROR("Can't handle Draft type message in smsBubble");
            break;
        default:
            break;
        }

        smsBubble->setText(record->body);

        focusChangedCallback = [this]([[maybe_unused]] Item &item) {
            setFocusItem(focus ? body : nullptr);
            return false;
        };

        body->focusChangedCallback = [this]([[maybe_unused]] Item &item) {
            if (timeLabel != nullptr) {
                timeLabel->setVisible(focus);
                body->resizeItems();
            }
            return true;
        };

        inputCallback = [&]([[maybe_unused]] Item &item, const InputEvent &event) { return smsBubble->onInput(event); };

        smsBubble->inputCallback = [application, record](Item &, const InputEvent &event) {
            if (event.state == InputEvent::State::keyReleasedShort && event.keyCode == KeyCode::KEY_LF) {
                LOG_INFO("Message activated!");
                auto app = dynamic_cast<app::ApplicationMessages *>(application);
                assert(app != nullptr);
                if (app->windowOptions != nullptr) {
                    app->windowOptions->clearOptions();
                    app->windowOptions->addOptions(smsWindowOptions(app, *record));
                    app->switchWindow(app->windowOptions->getName(), nullptr);
                }
                return true;
            }
            return false;
        };
    }

    void SMSOutputWidget::positionTimeLabel() const
    {
        if (timeLabel != nullptr) {
            timeLabel->setMinimumWidth(timeLabel->getTextNeedSpace());
            timeLabel->setMinimumHeight(style::messages::smsOutput::default_h);
            uint16_t timeLabelMargin = body->getWidth() - (smsBubble->getWidth() + timeLabel->getTextNeedSpace());

            if (body->getReverseOrder()) {
                timeLabel->setMargins(Margins(0, 0, timeLabelMargin, 0));
            }
            else {
                timeLabel->setMargins(Margins(timeLabelMargin, 0, 0, 0));
            }

            body->resizeItems();
        }
    }

    void SMSOutputWidget::timeLabelBuild(time_t timestamp)
    {
        timeLabel             = new gui::Label(body, 0, 0, 0, 0);
        timeLabel->activeItem = false;
        timeLabel->setFont(style::window::font::verysmall);
        timeLabel->setText(utils::time::Time(timestamp));
        timeLabel->setVisible(false);
        timeLabel->setAlignment(gui::Alignment(gui::Alignment::Horizontal::Center, gui::Alignment::Vertical::Center));
        timeLabel->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
    }

    void SMSOutputWidget::errorIconBuild()
    {
        errorIcon = new gui::Image("messages_error_W_M");
        errorIcon->setAlignment(Alignment(Alignment::Vertical::Center));
        errorIcon->activeItem = false;
        errorIcon->setMargins(Margins(style::messages::smsOutput::sms_error_icon_left_margin,
                                      0,
                                      style::messages::smsOutput::sms_error_icon_right_margin,
                                      0));
        body->addWidget(errorIcon);
    }

    auto SMSOutputWidget::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool
    {
        body->setPosition(0, 0);
        body->setSize(newDim.w, newDim.h);

        // We need to calculate margin between sms and timeLabel and we can do it only after sizes are set.
        positionTimeLabel();

        return true;
    }

    auto SMSOutputWidget::handleRequestResize([[maybe_unused]] const Item *child,
                                              unsigned short request_w,
                                              unsigned short request_h) -> Size
    {
        setMinimumHeight(request_h);
        return Size(request_w, request_h);
    }

} /* namespace gui */

A module-apps/application-messages/widgets/SMSOutputWidget.hpp => module-apps/application-messages/widgets/SMSOutputWidget.hpp +35 -0
@@ 0,0 1,35 @@
#pragma once

#include "Application.hpp"

#include "Text.hpp"
#include "TextBubble.hpp"
#include "Image.hpp"
#include "ListItem.hpp"
#include <BoxLayout.hpp>

namespace gui
{

    class SMSOutputWidget : public ListItem
    {
        HBox *body            = nullptr;
        TextBubble *smsBubble = nullptr;
        Label *timeLabel      = nullptr;
        Image *errorIcon      = nullptr;

        void timeLabelBuild(time_t timestamp);
        void errorIconBuild();
        void positionTimeLabel() const;

      public:
        gui::Text *inputText = nullptr;

        SMSOutputWidget(app::Application *application, const std::shared_ptr<SMSRecord> &record);
        virtual ~SMSOutputWidget() = default;

        auto onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) -> bool override;
        auto handleRequestResize(const Item *, unsigned short request_w, unsigned short request_h) -> Size override;
    };

} /* namespace gui */

M module-apps/application-messages/windows/NewMessage.cpp => module-apps/application-messages/windows/NewMessage.cpp +2 -2
@@ 177,7 177,7 @@ namespace gui
        reciepientHbox->setAlignment(gui::Alignment::Vertical::Center);
        reciepientHbox->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_BOTTOM);
        reciepientHbox->setPenFocusWidth(style::window::default_border_focus_w);
        reciepientHbox->setPenWidth(style::window::messages::sms_border_no_focus);
        reciepientHbox->setPenWidth(style::window::default_border_rect_no_focus);

        recipient = new gui::Text(reciepientHbox,
                                  0,


@@ 221,7 221,7 @@ namespace gui
            [=]() { bottomBarRestoreFromTemporaryMode(); },
            [=]() { selectSpecialCharacter(); }));
        message->setPenFocusWidth(style::window::default_border_focus_w);
        message->setPenWidth(style::window::messages::sms_border_no_focus);
        message->setPenWidth(style::window::default_border_rect_no_focus);
        message->setFont(style::window::font::medium);
        message->setAlignment(Alignment(gui::Alignment::Horizontal::Left, gui::Alignment::Vertical::Center));
        message->activatedCallback = [=](Item &) -> bool {

M module-apps/application-messages/windows/SMSThreadViewWindow.cpp => module-apps/application-messages/windows/SMSThreadViewWindow.cpp +34 -405
@@ 2,411 2,46 @@

#include "application-messages/ApplicationMessages.hpp"
#include "application-messages/data/SMSdata.hpp"
#include "application-messages/data/MessagesStyle.hpp"

#include "OptionsMessages.hpp"
#include <OptionWindow.hpp>
#include "Service/Message.hpp"

#include <Text.hpp>
#include <TextBubble.hpp>
#include <Label.hpp>
#include <Margins.hpp>
#include <service-db/api/DBServiceAPI.hpp>
#include <service-appmgr/ApplicationManager.hpp>
#include <service-db/messages/DBNotificationMessage.hpp>
#include <i18/i18.hpp>
#include <time/time_conversion.hpp>
#include <log/log.hpp>
#include <Style.hpp>
#include <OptionWindow.hpp>

#include <memory>
#include <cassert>

namespace gui
{

    SMSThreadViewWindow::SMSThreadViewWindow(app::Application *app) : AppWindow(app, name::window::thread_view)
    SMSThreadViewWindow::SMSThreadViewWindow(app::Application *app)
        : AppWindow(app, name::window::thread_view), smsModel{std::make_shared<SMSThreadModel>(this->application)}
    {
        AppWindow::buildInterface();
        setTitle(utils::localize.get("app_messages_title_main"));
        topBar->setActive(TopBar::Elements::TIME, true);
        bottomBar->setText(BottomBar::Side::LEFT, utils::localize.get(style::strings::common::options));
        bottomBar->setText(BottomBar::Side::RIGHT, utils::localize.get(style::strings::common::back));
        body = new gui::VBox(this,
                             style::window::default_left_margin,
                             title->offset_h(),
                             elements_width,
                             bottomBar->getY() - title->offset_h());
        body->setPenWidth(style::window::default_border_no_focus_w);
        body->setPenFocusWidth(style::window::default_border_no_focus_w);
        body->borderCallback = [this](const InputEvent &inputEvent) -> bool {
            if (inputEvent.state != InputEvent::State::keyReleasedShort) {
                return false;
            }
            if (inputEvent.keyCode == KeyCode::KEY_UP) {
                return this->showMessages(Action::NextPage);
            }
            else if (inputEvent.keyCode == KeyCode::KEY_DOWN) {
                return this->showMessages(Action::PreviousPage);
            }
            else {
                return false;
            }
        };

        refreshTextItem();
        /// setup
        body->setReverseOrder(true);
        body->setVisible(true);
        setFocusItem(body);
    }

    void SMSThreadViewWindow::handleDraftMessage()
    {
        if (const auto &text = inputMessage->inputText->getText();
            text.empty() || (text == utils::localize.get("sms_temp_reply"))) {
            clearDraftMessage();
        }
        else {
            updateDraftMessage(text);
        }
    }

    void SMSThreadViewWindow::clearDraftMessage()
    {
        if (!SMS.draft.has_value()) {
            displayDraftMessage();
            return;
        }

        auto app = dynamic_cast<app::ApplicationMessages *>(application);
        assert(app != nullptr);
        if (const auto removed = app->removeDraft(SMS.draft.value()); removed) {
            SMS.draft = std::nullopt;
            displayDraftMessage();
        }
    }

    void SMSThreadViewWindow::displayDraftMessage() const
    {
        if (SMS.draft.has_value()) {
            inputMessage->inputText->setText(SMS.draft->body);
        }
        else {
            inputMessage->inputText->clear();
        }
    }

    void SMSThreadViewWindow::updateDraftMessage(const UTF8 &inputText)
    {
        auto app = dynamic_cast<app::ApplicationMessages *>(application);
        assert(app != nullptr);

        if (SMS.draft.has_value()) {
            app->updateDraft(SMS.draft.value(), inputText);
        }
        else {
            const auto &[draft, success] = app->createDraft(*number, inputText);
            if (success) {
                SMS.draft = draft;
            }
        }
    }

    void SMSThreadViewWindow::refreshTextItem()
    {
        if (inputMessage != nullptr) {
            return;
        }
        smsList = new gui::ListView(this,
                                    style::messages::smsList::x,
                                    style::messages::smsList::y,
                                    style::messages::smsList::w,
                                    style::messages::smsList::h,
                                    smsModel);
        smsList->setOrientation(style::listview::Orientation::BottomTop);

        inputMessage                    = new SMSInputWidget(body, application);
        inputMessage->activatedCallback = [this]([[maybe_unused]] gui::Item &item) {
            auto app = dynamic_cast<app::ApplicationMessages *>(application);
            assert(app != nullptr);
            if (app->handleSendSmsFromThread(*number, inputMessage->inputText->getText())) {
                LOG_ERROR("handleSendSmsFromThread failed");
            }
            clearDraftMessage();
            return true;
        };
    }

    void SMSThreadViewWindow::destroyTextItem()
    {
        body->erase(inputMessage);
        if (inputMessage->parent == nullptr) {
            delete (inputMessage);
        }
        inputMessage = nullptr;
    }

    void SMSThreadViewWindow::cleanView()
    {
        body->removeWidget(inputMessage);
        body->erase();
    }

    bool SMSThreadViewWindow::showMessages(SMSThreadViewWindow::Action what)
    {
        if (SMS.thread <= 0) {
            LOG_ERROR("threadID not set!");
            return false;
        }
        addSMS(what);
        return true;
    }

    void SMSThreadViewWindow::addSMS(SMSThreadViewWindow::Action what)
    {
        LOG_DEBUG("--- %d ---", static_cast<int>(what));
        // if there was text - then remove it temp
        // 1. load elements to tmp vector
        std::unique_ptr<ThreadRecord> threadDetails = DBServiceAPI::ThreadGet(this->application, SMS.thread);
        if (threadDetails == nullptr) {
            LOG_ERROR("cannot fetch details of selected thread (id: %d)", SMS.thread);
            return;
        }
        SMS.dbsize = threadDetails->msgCount;

        if (threadDetails->isUnread()) {
            auto app = dynamic_cast<app::ApplicationMessages *>(application);
            assert(app != nullptr);
            if (application->getCurrentWindow() == this) {
                app->markSmsThreadAsRead(threadDetails->ID);
            }
        }

        LOG_DEBUG("start: %d end: %d db: %d", SMS.start, SMS.end, SMS.dbsize);
        if (what == Action::Init || what == Action::NewestPage) {
            SMS.start = 0;

            // Refactor
            SMS.end = maxsmsinwindow;
            if (what == Action::Init) {
                destroyTextItem();
            }
            refreshTextItem();
        }

        // 2. check how many of these will fit in box
        //         update begin / end in `SMS`
        if (what == Action::NextPage) {
            if (SMS.end != SMS.dbsize) {

                // Refactor
                for (auto sms : body->children) {
                    if (sms->visible)
                        SMS.start++;
                }

                if (inputMessage->visible)
                    SMS.start -= 1;

                LOG_INFO("SMS start %d", SMS.start);
            }
            else {
                LOG_INFO("All sms shown");
                return;
            }
        }
        else if (what == Action::PreviousPage) {
            if (SMS.start == 0) {
                return;
            }
            else if (SMS.start - maxsmsinwindow < 0) {
                SMS.start = 0;
            }
            else {
                SMS.start -= maxsmsinwindow;
            }
            LOG_DEBUG("in progress %d", SMS.start);
        }
        SMS.sms = DBServiceAPI::SMSGetLimitOffsetByThreadID(this->application, SMS.start, maxsmsinwindow, SMS.thread);
        LOG_DEBUG("=> SMS %d < %d < %d",
                  static_cast<int>(SMS.start),
                  static_cast<int>(SMS.sms->size()),
                  static_cast<int>(maxsmsinwindow));
        if (SMS.sms->size() == 0) {
            LOG_WARN("Malformed thread. Leave it (id: %d)", SMS.thread);
            application->switchWindow(gui::name::window::main_window);
            return;
        }

        if (what == Action::Init) {
            const auto &lastSms = SMS.sms->front();
            SMS.draft           = lastSms.type == SMSType::DRAFT ? std::optional<SMSRecord>{lastSms} : std::nullopt;
            displayDraftMessage();
        }

        // 3. add them to box
        this->cleanView();
        // if we are going from 0 then we want to show text prompt
        if (SMS.start == 0) {
            body->addWidget(inputMessage);
        }

        // rebuild bubbles
        SMS.end = SMS.start;
        for (auto &el : *SMS.sms) {
            if (el.type != SMSType::DRAFT) {
                if (!smsBuild(el)) {
                    break;
                }
                ++SMS.end;
            }
        }

        body->setNavigation();
        setFocusItem(body);
        if (Action::PreviousPage == what) {
            body->setVisible(true, true);
        }
        LOG_DEBUG("sms built");
    }

    HBox *SMSThreadViewWindow::smsSpanBuild(Text *smsBubble, const SMSRecord &el) const
    {
        HBox *labelSpan = new gui::HBox();

        labelSpan->setPenWidth(style::window::default_border_no_focus_w);
        labelSpan->setPenFocusWidth(style::window::default_border_no_focus_w);
        labelSpan->setSize(elements_width, smsBubble->getHeight());
        labelSpan->setMinimumWidth(elements_width);
        labelSpan->setMinimumHeight(smsBubble->getHeight());
        labelSpan->setMaximumHeight(smsBubble->widgetMaximumArea.h);
        labelSpan->setFillColor(gui::Color(11, 0));

        LOG_DEBUG("ADD SMS TYPE: %d", static_cast<int>(el.type));
        switch (el.type) {
        case SMSType::QUEUED:
            // Handle in the same way as case below. (pending sending display as already sent)
            [[fallthrough]];
        case SMSType::OUTBOX:
            smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
            smsBubble->setX(body->getWidth() - smsBubble->getWidth());
            labelSpan->setReverseOrder(true);
            labelSpan->addWidget(smsBubble);
            addTimeLabel(
                labelSpan, timeLabelBuild(el.date), elements_width - (smsBubble->getWidth() + smsBubble->yapSize));
            break;
        case SMSType::INBOX:
            smsBubble->setPadding(style::window::messages::sms_left_bubble_padding);
            smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_LEFT);
            labelSpan->setReverseOrder(false);
            labelSpan->addWidget(smsBubble);
            addTimeLabel(
                labelSpan, timeLabelBuild(el.date), elements_width - (smsBubble->getWidth() + smsBubble->yapSize));
            break;
        case SMSType::FAILED:
            smsBubble->setYaps(RectangleYapFlags::GUI_RECT_YAP_TOP_RIGHT);
            smsBubble->setX(body->getWidth() - smsBubble->getWidth());
            labelSpan->setReverseOrder(true);
            addErrorIcon(labelSpan);
            labelSpan->addWidget(smsBubble);
            break;
        default:
            break;
        }

        if (!smsBubble->visible) {
            delete labelSpan; // total fail
            labelSpan = nullptr;
        }
        return labelSpan;
    }

    void SMSThreadViewWindow::addErrorIcon(HBox *layout) const
    {
        auto errorIcon = new gui::Image("messages_error_W_M");
        errorIcon->setAlignment(Alignment(Alignment::Vertical::Center));
        errorIcon->activeItem = false; // make it non-focusable
        errorIcon->setMargins(Margins(style::window::messages::sms_error_icon_offset,
                                      0,
                                      (style::window::messages::sms_failed_offset -
                                       (errorIcon->getWidth() + style::window::messages::sms_error_icon_offset)),
                                      0));
        layout->addWidget(errorIcon);
    }

    void SMSThreadViewWindow::addTimeLabel(HBox *layout, Label *timeLabel, uint16_t widthAvailable) const
    {
        // add time label activated on focus
        timeLabel->setMinimumWidth(timeLabel->getTextNeedSpace());
        timeLabel->setMinimumHeight(layout->getHeight());
        timeLabel->setSize(timeLabel->getTextNeedSpace(), layout->getHeight());

        uint16_t timeLabelSpacerWidth = widthAvailable - timeLabel->getWidth();

        timeLabel->setMargins(Margins(timeLabelSpacerWidth, 0, timeLabelSpacerWidth, 0));
        layout->addWidget(timeLabel);

        layout->focusChangedCallback = [=](gui::Item &item) {
            timeLabel->setVisible(item.focus);
            // we need to inform parent that it needs to resize itself - easiest way to do so
            if (timeLabel->parent) {
                timeLabel->parent->setSize(timeLabel->parent->getWidth(), timeLabel->parent->getHeight());
            }
            return true;
        };
    }

    Label *SMSThreadViewWindow::timeLabelBuild(time_t timestamp) const
    {
        auto timeLabel        = new gui::Label(nullptr, 0, 0, 0, 0);
        timeLabel->activeItem = false;
        timeLabel->setFont(style::window::font::verysmall);
        timeLabel->setText(utils::time::Time(timestamp));
        timeLabel->setPenWidth(style::window::default_border_no_focus_w);
        timeLabel->setVisible(false);
        timeLabel->setAlignment(gui::Alignment(gui::Alignment::Horizontal::Center, gui::Alignment::Vertical::Center));
        return timeLabel;
    }

    bool SMSThreadViewWindow::smsBuild(const SMSRecord &smsRecord)
    {
        auto max_available_h = body->area().h;
        auto max_available_w = style::window::messages::sms_max_width;
        /// dummy sms thread - TODO load from db - on switchData
        auto smsBubble = new TextBubble(nullptr, 0, 0, style::window::messages::sms_max_width, 0);
        smsBubble->setMaximumSize(max_available_w, max_available_h);
        smsBubble->setTextType(TextType::MULTI_LINE);
        smsBubble->setRadius(style::window::messages::sms_radius);
        smsBubble->setFont(style::window::font::medium);
        smsBubble->setPenFocusWidth(style::window::default_border_focus_w);
        smsBubble->setPenWidth(style::window::messages::sms_border_no_focus);
        smsBubble->setPadding(style::window::messages::sms_right_bubble_padding);
        smsBubble->setText(smsRecord.body);

        smsBubble->inputCallback = [=, &smsRecord](Item &, const InputEvent &event) {
            if (event.state == InputEvent::State::keyReleasedShort && event.keyCode == KeyCode::KEY_LF) {
                LOG_INFO("Message activated!");
                auto app = dynamic_cast<app::ApplicationMessages *>(application);
                assert(app != nullptr);
                if (app->windowOptions != nullptr) {
                    app->windowOptions->clearOptions();
                    app->windowOptions->addOptions(smsWindowOptions(app, smsRecord));
                    app->switchWindow(app->windowOptions->getName(), nullptr);
                }
                return true;
            }
            return false;
        };

        // wrap label in H box, to make fit datetime in it
        HBox *labelSpan = smsSpanBuild(smsBubble, smsRecord);
        labelSpan->setMargins(Margins(0, style::window::messages::sms_vertical_spacer, 0, 0));

        if (labelSpan == nullptr) {
            return false;
        }

        LOG_INFO("Add sms: %s %s", smsRecord.body.c_str(), smsRecord.number.getFormatted().c_str());
        body->addWidget(labelSpan);
        return labelSpan->visible;
        setFocusItem(smsList);
    }

    void SMSThreadViewWindow::rebuild()
    {
        addSMS(SMSThreadViewWindow::Action::Init);
        smsList->rebuildList();
    }

    void SMSThreadViewWindow::buildInterface()


@@ 428,25 63,33 @@ namespace gui
            auto pdata = dynamic_cast<SMSThreadData *>(data);
            if (pdata) {
                LOG_INFO("We have it! %" PRIu32, pdata->thread->ID);
                cleanView();
                SMS.thread = pdata->thread->ID;
                showMessages(Action::Init);
                auto ret = DBServiceAPI::ContactGetByIDWithTemporary(application, pdata->thread->contactID);
                contact  = std::make_shared<ContactRecord>(ret->front());
                // should be name number for now - easier to handle
                setTitle(ret->front().getFormattedName());
                auto retNumber = DBServiceAPI::GetNumberById(application, pdata->thread->numberID, numberIdTimeout);
                assert(retNumber != nullptr);
                number = std::move(retNumber);
                LOG_INFO("Phone number for thread: %s", number->getFormatted().c_str());
                smsModel->number = std::move(retNumber);
                LOG_INFO("Phonenumber for thread: %s", smsModel->number->getFormatted().c_str());

                // Mark thread as Read
                if (pdata->thread->isUnread()) {
                    auto app = dynamic_cast<app::ApplicationMessages *>(application);
                    assert(app != nullptr);
                    if (application->getCurrentWindow() == this) {
                        app->markSmsThreadAsRead(pdata->thread->ID);
                    }
                }

                smsModel->smsThreadID = pdata->thread->ID;
                smsList->rebuildList();
            }
        }
        if (auto pdata = dynamic_cast<SMSTextData *>(data)) {
            auto txt = pdata->text;
            LOG_INFO("received sms templates data \"%s\"", txt.c_str());
            pdata->concatenate == SMSTextData::Concatenate::True ? inputMessage->inputText->addText(txt)
                                                                 : inputMessage->inputText->setText(txt);
            body->resizeItems();
            pdata->concatenate == SMSTextData::Concatenate::True ? smsModel->smsInput->inputText->addText(txt)
                                                                 : smsModel->smsInput->inputText->setText(txt);
        }
    }



@@ 457,7 100,7 @@ namespace gui

    void SMSThreadViewWindow::onClose()
    {
        handleDraftMessage();
        smsModel->handleDraftMessage();
    }

    bool SMSThreadViewWindow::onDatabaseMessage(sys::Message *msgl)


@@ 465,24 108,10 @@ namespace gui
        auto msg = dynamic_cast<db::NotificationMessage *>(msgl);
        if (msg != nullptr) {
            if (msg->interface == db::Interface::Name::SMS) {
                std::unique_ptr<ThreadRecord> threadDetails;
                switch (msg->type) {
                case db::Query::Type::Create:
                    // jump to the latest SMS
                    addSMS(SMSThreadViewWindow::Action::NewestPage);
                    break;
                case db::Query::Type::Update:
                case db::Query::Type::Delete:
                    addSMS(SMSThreadViewWindow::Action::Refresh);
                    break;
                case db::Query::Type::Read:
                    // do not update view, as we don't have visual representation for read status
                    break;
                }
                if (this == application->getCurrentWindow()) {
                    application->refreshWindow(gui::RefreshModes::GUI_REFRESH_FAST);
                if (msg->dataModified()) {
                    rebuild();
                    return true;
                }
                return true;
            }
            if (msg->interface == db::Interface::Name::SMSThread) {
                if (msg->type == db::Query::Type::Delete) {

M module-apps/application-messages/windows/SMSThreadViewWindow.hpp => module-apps/application-messages/windows/SMSThreadViewWindow.hpp +4 -45
@@ 1,15 1,11 @@
#pragma once

#include <AppWindow.hpp>
#include <application-messages/widgets/SMSInputWidget.hpp>
#include <gui/widgets/BoxLayout.hpp>
#include <gui/widgets/Image.hpp>
#include <gui/widgets/Label.hpp>
#include <gui/widgets/Window.hpp>
#include <module-apps/application-messages/models/SMSThreadModel.hpp>

#include <ListView.hpp>
#include <PhoneNumber.hpp>
#include <service-db/api/DBServiceAPI.hpp>
#include <Text.hpp>

#include <functional>
#include <string>


@@ 19,39 15,11 @@ namespace gui
    class SMSThreadViewWindow : public AppWindow
    {
      private:
        gui::VBox *body         = nullptr;
        uint16_t elements_width = this->getWidth() - style::window::default_left_margin * 2;
        void cleanView();
        enum class Action
        {
            Init,        /// first load of sms thread view
            NewestPage,  /// show a sms thread page from the latest sms
            Refresh,     /// just refresh current view
            NextPage,    /// load previous page
            PreviousPage /// load next page
        };
        /// return if request was handled
        bool showMessages(Action what);
        void addSMS(Action what);
        bool smsBuild(const SMSRecord &smsRecord);
        Label *timeLabelBuild(time_t timestamp) const;
        HBox *smsSpanBuild(Text *smsBubble, const SMSRecord &el) const;
        const ssize_t maxsmsinwindow = 7;
        std::shared_ptr<SMSThreadModel> smsModel = nullptr;
        gui::ListView *smsList                   = nullptr;

        std::shared_ptr<ContactRecord> contact;
        std::unique_ptr<utils::PhoneNumber::View> number;

        struct
        {
            int start  = 0;                              // actual shown position start
            int end    = 7;                              // actual shown position end
            int thread = 0;                              // thread we are showing
            int dbsize = 0;                              // size of elements in db
            std::unique_ptr<std::vector<SMSRecord>> sms; // loaded sms from db
            std::optional<SMSRecord> draft;              // draft message of the thread we are showing, if exists.
        } SMS;

        gui::SMSInputWidget *inputMessage                 = nullptr;
        inline static const std::uint32_t numberIdTimeout = 1000;

      public:


@@ 69,15 37,6 @@ namespace gui
        void buildInterface() override;

        void destroyInterface() override;
        void destroyTextItem();
        void refreshTextItem();
        void addTimeLabel(HBox *layout, Label *timeLabel, uint16_t widthAvailable) const;
        void addErrorIcon(HBox *layout) const;

        void handleDraftMessage();
        void clearDraftMessage();
        void updateDraftMessage(const UTF8 &inputText);
        void displayDraftMessage() const;
    };

} /* namespace gui */

M module-apps/application-phonebook/widgets/InputBoxWithLabelAndIconWidget.cpp => module-apps/application-phonebook/widgets/InputBoxWithLabelAndIconWidget.cpp +1 -1
@@ 24,7 24,7 @@ namespace gui
        hBox = new gui::HBox(this, 0, 0, phonebookStyle::inputBoxWithLabelAndIconIWidget::w, 0);
        hBox->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
        hBox->setPenFocusWidth(style::window::default_border_focus_w);
        hBox->setPenWidth(style::window::messages::sms_border_no_focus);
        hBox->setPenWidth(style::window::default_border_rect_no_focus);

        inputBoxLabel = new gui::Label(hBox, 0, 0, 0, 0);
        inputBoxLabel->setMinimumSize(phonebookStyle::inputBoxWithLabelAndIconIWidget::input_box_w,

M module-apps/application-phonebook/widgets/PhonebookItem.cpp => module-apps/application-phonebook/widgets/PhonebookItem.cpp +1 -1
@@ 16,7 16,7 @@ namespace gui
        hBox = new gui::HBox(this, 0, 0, 0, 0);
        hBox->setEdges(gui::RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);
        hBox->setPenFocusWidth(style::window::default_border_focus_w);
        hBox->setPenWidth(style::window::messages::sms_border_no_focus);
        hBox->setPenWidth(style::window::default_border_rect_no_focus);

        contactName = new gui::Label(hBox, 0, 0, 0, 0);
        contactName->setPenFocusWidth(0);

M module-apps/application-phonebook/windows/PhonebookIceContacts.cpp => module-apps/application-phonebook/windows/PhonebookIceContacts.cpp +5 -0
@@ 73,4 73,9 @@ namespace gui

        return false;
    }

    void PhonebookIceContacts::onBeforeShow(ShowMode mode, SwitchData *data)
    {
        rebuild();
    }
} /* namespace gui */

M module-apps/application-phonebook/windows/PhonebookIceContacts.hpp => module-apps/application-phonebook/windows/PhonebookIceContacts.hpp +1 -0
@@ 17,6 17,7 @@ namespace gui

        bool onInput(const InputEvent &inputEvent) override;
        bool onDatabaseMessage(sys::Message *msgl) override;
        void onBeforeShow(ShowMode mode, SwitchData *data) override;

        void rebuild() override;
        void buildInterface() override;

M module-db/CMakeLists.txt => module-db/CMakeLists.txt +1 -0
@@ 67,6 67,7 @@ set(SOURCES
        queries/messages/sms/QuerySMSGetByID.cpp
        queries/messages/sms/QuerySMSGetByContactID.cpp
        queries/messages/sms/QuerySMSGetByThreadID.cpp
        queries/messages/sms/QuerySMSGetForList.cpp
        queries/messages/sms/QuerySMSGetByText.cpp
        queries/messages/sms/QuerySMSAdd.cpp
        queries/messages/sms/QuerySMSRemove.cpp

M module-db/Common/Common.hpp => module-db/Common/Common.hpp +1 -0
@@ 20,6 20,7 @@ enum class SMSType : uint32_t
    INBOX   = 0x04,
    OUTBOX  = 0x08,
    QUEUED  = 0x10,
    INPUT   = 0x12,
    UNKNOWN = 0xFF
};


M module-db/Interface/SMSRecord.cpp => module-db/Interface/SMSRecord.cpp +41 -0
@@ 18,6 18,7 @@

#include <PhoneNumber.hpp>
#include <optional>
#include <module-db/queries/messages/sms/QuerySMSGetForList.hpp>

SMSRecord::SMSRecord(const SMSTableRow &w, const utils::PhoneNumber::View &num)
    : date(w.date), dateSent(w.dateSent), errorCode(w.errorCode), body(w.body), type(w.type), threadID(w.threadID),


@@ 310,6 311,9 @@ std::unique_ptr<db::QueryResult> SMSRecordInterface::runQuery(std::shared_ptr<db
    else if (typeid(*query) == typeid(db::query::SMSGetByThreadID)) {
        return getByThreadIDQuery(query);
    }
    else if (typeid(*query) == typeid(db::query::SMSGetForList)) {
        return getForListQuery(query);
    }
    else if (typeid(*query) == typeid(db::query::SMSGetCountByThreadID)) {
        return getCountByThreadIDQuery(query);
    }


@@ 509,6 513,43 @@ std::unique_ptr<db::QueryResult> SMSRecordInterface::getByThreadIDQuery(std::sha
    return response;
}

std::unique_ptr<db::QueryResult> SMSRecordInterface::getForListQuery(std::shared_ptr<db::Query> query)
{
    const auto localQuery = static_cast<const db::query::SMSGetForList *>(query.get());
    auto smsVector =
        smsDB->sms.getByThreadIdWithoutDraftWithEmptyInput(localQuery->threadId, localQuery->offset, localQuery->limit);
    std::vector<SMSRecord> recordVector;
    for (auto sms : smsVector) {
        SMSRecord record;
        record.body      = sms.body;
        record.contactID = sms.contactID;
        record.date      = sms.date;
        record.dateSent  = sms.dateSent;
        record.errorCode = sms.errorCode;
        record.threadID  = sms.threadID;
        record.type      = sms.type;
        record.ID        = sms.ID;
        recordVector.emplace_back(record);
    }

    auto count = smsDB->sms.countWithoutDraftsByThreadId(localQuery->threadId);
    auto draft = smsDB->sms.getDraftByThreadId(localQuery->threadId);

    SMSRecord record;
    record.body      = draft.body;
    record.contactID = draft.contactID;
    record.date      = draft.date;
    record.dateSent  = draft.dateSent;
    record.errorCode = draft.errorCode;
    record.threadID  = draft.threadID;
    record.type      = draft.type;
    record.ID        = draft.ID;

    auto response = std::make_unique<db::query::SMSGetForListResult>(recordVector, count, record);
    response->setRequestQuery(query);
    return response;
}

std::unique_ptr<db::QueryResult> SMSRecordInterface::getLastByThreadIDQuery(std::shared_ptr<db::Query> query)
{
    const auto localQuery = static_cast<const db::query::SMSGetLastByThreadID *>(query.get());

M module-db/Interface/SMSRecord.hpp => module-db/Interface/SMSRecord.hpp +1 -0
@@ 79,6 79,7 @@ class SMSRecordInterface : public RecordInterface<SMSRecord, SMSRecordField>
    std::unique_ptr<db::QueryResult> updateQuery(std::shared_ptr<db::Query> query);
    std::unique_ptr<db::QueryResult> getQuery(std::shared_ptr<db::Query> query);
    std::unique_ptr<db::QueryResult> getByThreadIDQuery(std::shared_ptr<db::Query> query);
    std::unique_ptr<db::QueryResult> getForListQuery(std::shared_ptr<db::Query> query);
    std::unique_ptr<db::QueryResult> getCountByThreadIDQuery(std::shared_ptr<db::Query> query);
    std::unique_ptr<db::QueryResult> getLastByThreadIDQuery(std::shared_ptr<db::Query> query);
};

M module-db/Tables/SMSTable.cpp => module-db/Tables/SMSTable.cpp +67 -0
@@ 137,6 137,73 @@ std::vector<SMSTableRow> SMSTable::getByThreadId(uint32_t threadId, uint32_t off
    return ret;
}

std::vector<SMSTableRow> SMSTable::getByThreadIdWithoutDraftWithEmptyInput(uint32_t threadId,
                                                                           uint32_t offset,
                                                                           uint32_t limit)
{
    auto retQuery = db->query("SELECT * FROM sms WHERE thread_id= %u AND type != %u UNION ALL SELECT 0 as _id, 0 as "
                              "thread_id, 0 as contact_id, 0 as "
                              "date, 0 as date_send, 0 as error_code, 0 as body, %u as type LIMIT %u OFFSET %u",
                              threadId,
                              SMSType::DRAFT,
                              SMSType::INPUT,
                              limit,
                              offset);

    if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
        return std::vector<SMSTableRow>();
    }

    std::vector<SMSTableRow> ret;

    do {
        ret.push_back(SMSTableRow{
            (*retQuery)[0].getUInt32(),                       // ID
            (*retQuery)[1].getUInt32(),                       // threadID
            (*retQuery)[2].getUInt32(),                       // contactID
            (*retQuery)[3].getUInt32(),                       // date
            (*retQuery)[4].getUInt32(),                       // dateSent
            (*retQuery)[5].getUInt32(),                       // errorCode
            (*retQuery)[6].getString(),                       // body
            static_cast<SMSType>((*retQuery)[7].getUInt32()), // type
        });
    } while (retQuery->nextRow());

    return ret;
}

uint32_t SMSTable::countWithoutDraftsByThreadId(uint32_t threadId)
{
    auto queryRet = db->query("SELECT COUNT(*) FROM sms WHERE thread_id= %u AND type != %u;", threadId, SMSType::DRAFT);

    if (queryRet == nullptr || queryRet->getRowCount() == 0) {
        return 0;
    }

    return uint32_t{(*queryRet)[0].getUInt32()};
}

SMSTableRow SMSTable::getDraftByThreadId(uint32_t threadId)
{
    auto retQuery = db->query(
        "SELECT * FROM sms WHERE thread_id= %u AND type = %u ORDER BY date DESC LIMIT 1;", threadId, SMSType::DRAFT);

    if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
        return SMSTableRow();
    }

    return SMSTableRow{
        (*retQuery)[0].getUInt32(),                       // ID
        (*retQuery)[1].getUInt32(),                       // threadID
        (*retQuery)[2].getUInt32(),                       // contactID
        (*retQuery)[3].getUInt32(),                       // date
        (*retQuery)[4].getUInt32(),                       // dateSent
        (*retQuery)[5].getUInt32(),                       // errorCode
        (*retQuery)[6].getString(),                       // body
        static_cast<SMSType>((*retQuery)[7].getUInt32()), // type
    };
}

std::vector<SMSTableRow> SMSTable::getByText(std::string text)
{


M module-db/Tables/SMSTable.hpp => module-db/Tables/SMSTable.hpp +5 -0
@@ 47,6 47,11 @@ class SMSTable : public Table<SMSTableRow, SMSTableFields>
    std::vector<SMSTableRow> getByContactId(uint32_t contactId);
    std::vector<SMSTableRow> getByText(std::string text);
    std::vector<SMSTableRow> getByThreadId(uint32_t threadId, uint32_t offset, uint32_t limit);
    std::vector<SMSTableRow> getByThreadIdWithoutDraftWithEmptyInput(uint32_t threadId,
                                                                     uint32_t offset,
                                                                     uint32_t limit);
    uint32_t countWithoutDraftsByThreadId(uint32_t threadId);
    SMSTableRow getDraftByThreadId(uint32_t threadId);

    std::pair<uint32_t, std::vector<SMSTableRow>> getManyByType(SMSType type, uint32_t offset, uint32_t limit);


A module-db/queries/messages/sms/QuerySMSGetForList.cpp => module-db/queries/messages/sms/QuerySMSGetForList.cpp +35 -0
@@ 0,0 1,35 @@
#include "QuerySMSGetForList.hpp"

#include <utility>

namespace db::query
{
    SMSGetForList::SMSGetForList(unsigned int threadId, unsigned int offset, unsigned int limit)
        : Query(Query::Type::Read), threadId(threadId), offset(offset), limit(limit)
    {}

    auto SMSGetForList::debugInfo() const -> std::string
    {
        return "SMSGetForList";
    }

    SMSGetForListResult::SMSGetForListResult(std::vector<SMSRecord> result, unsigned int count, SMSRecord draft)
        : result(std::move(result)), count(count), draft(std::move(draft))
    {}
    auto SMSGetForListResult::getResults() const -> std::vector<SMSRecord>
    {
        return result;
    }
    auto SMSGetForListResult::getCount() const -> unsigned int
    {
        return count;
    }
    auto SMSGetForListResult::getDraft() const -> SMSRecord
    {
        return draft;
    }
    auto SMSGetForListResult::debugInfo() const -> std::string
    {
        return "SMSGetForListResult";
    }
} // namespace db::query

A module-db/queries/messages/sms/QuerySMSGetForList.hpp => module-db/queries/messages/sms/QuerySMSGetForList.hpp +36 -0
@@ 0,0 1,36 @@
#pragma once

#include <Tables/ThreadsTable.hpp>
#include <Common/Query.hpp>
#include <string>
#include "Interface/SMSRecord.hpp"

namespace db::query
{

    class SMSGetForList : public Query
    {
      public:
        unsigned int threadId = DB_ID_NONE;
        unsigned int offset   = 0;
        unsigned int limit    = 0;

        SMSGetForList(unsigned int id, unsigned int offset = 0, unsigned int limit = 0);
        [[nodiscard]] auto debugInfo() const -> std::string override;
    };

    class SMSGetForListResult : public QueryResult
    {
        std::vector<SMSRecord> result;
        unsigned int count = 0;
        SMSRecord draft;

      public:
        SMSGetForListResult(std::vector<SMSRecord> result, unsigned int count, SMSRecord draft);
        [[nodiscard]] auto getResults() const -> std::vector<SMSRecord>;
        [[nodiscard]] auto getCount() const -> unsigned int;
        [[nodiscard]] auto getDraft() const -> SMSRecord;
        [[nodiscard]] auto debugInfo() const -> std::string override;
    };

} // namespace db::query

M module-db/tests/SMSRecord_tests.cpp => module-db/tests/SMSRecord_tests.cpp +195 -165
@@ 16,6 16,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <module-db/queries/messages/sms/QuerySMSGetForList.hpp>

struct test
{


@@ 52,183 53,212 @@ TEST_CASE("SMS Record tests")
    recordIN.body      = bodyTest;
    recordIN.type      = typeTest;

    // Add 2 records
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));
    SECTION("SMS Record Test")
    {
        // Add 2 records
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));

    // Get all available records
    auto records = smsRecInterface.GetLimitOffset(0, 100);
    REQUIRE(records->size() == 2);
        // Get all available records
        auto records = smsRecInterface.GetLimitOffset(0, 100);
        REQUIRE(records->size() == 2);

    // Check if fetched records contain valid data
    for (const auto &w : *records) {
        REQUIRE(w.body == bodyTest);
        REQUIRE(w.number == numberTest);
    }
        // Check if fetched records contain valid data
        for (const auto &w : *records) {
            REQUIRE(w.body == bodyTest);
            REQUIRE(w.number == numberTest);
        }

    // Get all available records by specified thread ID and check for invalid data
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
    REQUIRE((*records).size() == 2);
    for (const auto &w : *records) {
        REQUIRE(w.body == bodyTest);
        REQUIRE(w.number == numberTest);
    }
        // Get all available records by specified thread ID and check for invalid data
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
        REQUIRE((*records).size() == 2);
        for (const auto &w : *records) {
            REQUIRE(w.body == bodyTest);
            REQUIRE(w.number == numberTest);
        }

    // Get all available records by specified contact ID and check for invalid data
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
    REQUIRE((*records).size() == 2);
    for (const auto &w : *records) {
        REQUIRE(w.body == bodyTest);
        REQUIRE(w.number == numberTest);
    }
        // Get all available records by specified contact ID and check for invalid data
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
        REQUIRE((*records).size() == 2);
        for (const auto &w : *records) {
            REQUIRE(w.body == bodyTest);
            REQUIRE(w.number == numberTest);
        }

    // Remove records one by one
    REQUIRE(smsRecInterface.RemoveByID(1));
    REQUIRE(smsRecInterface.RemoveByID(2));

    // SMS database should not contain any records
    REQUIRE(smsRecInterface.GetCount() == 0);

    // Test updating record
    REQUIRE(smsRecInterface.Add(recordIN));
    recordIN.ID   = 1;
    recordIN.body = bodyTest2;
    REQUIRE(smsRecInterface.Update(recordIN));

    auto record = smsRecInterface.GetByID(1);
    REQUIRE(record.ID != 0);
    REQUIRE(record.body == bodyTest2);

    // SMS database should contain 1 record
    REQUIRE(smsRecInterface.GetCount() == 1);

    // Remove existing record
    REQUIRE(smsRecInterface.RemoveByID(1));
    // SMS database should be empty
    REQUIRE(smsRecInterface.GetCount() == 0);

    // Test fetching record by using Thread ID
    // Add records with different numbers, they should be placed in separate threads&contacts dbs
    recordIN.body   = bodyTest;
    recordIN.number = numberTest;
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));

    recordIN.number = numberTest2;
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));

    // Get all available records by specified thread ID and check for invalid data
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
    REQUIRE((*records).size() == 2);
    for (const auto &w : *records) {
        REQUIRE(w.body == bodyTest);
        REQUIRE(w.number == numberTest);
    }
        // Remove records one by one
        REQUIRE(smsRecInterface.RemoveByID(1));
        REQUIRE(smsRecInterface.RemoveByID(2));

        // SMS database should not contain any records
        REQUIRE(smsRecInterface.GetCount() == 0);

        // Test updating record
        REQUIRE(smsRecInterface.Add(recordIN));
        recordIN.ID   = 1;
        recordIN.body = bodyTest2;
        REQUIRE(smsRecInterface.Update(recordIN));

        auto record = smsRecInterface.GetByID(1);
        REQUIRE(record.ID != 0);
        REQUIRE(record.body == bodyTest2);

        // SMS database should contain 1 record
        REQUIRE(smsRecInterface.GetCount() == 1);

        // Remove existing record
        REQUIRE(smsRecInterface.RemoveByID(1));
        // SMS database should be empty
        REQUIRE(smsRecInterface.GetCount() == 0);

        // Test fetching record by using Thread ID
        // Add records with different numbers, they should be placed in separate threads&contacts dbs
        recordIN.body   = bodyTest;
        recordIN.number = numberTest;
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));

        recordIN.number = numberTest2;
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));

        // Get all available records by specified thread ID and check for invalid data
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1");
        REQUIRE((*records).size() == 2);
        for (const auto &w : *records) {
            REQUIRE(w.body == bodyTest);
            REQUIRE(w.number == numberTest);
        }

    // Get all available records by specified thread ID and check for invalid data
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "2");
    REQUIRE((*records).size() == 2);
    for (const auto &w : *records) {
        REQUIRE(w.body == bodyTest);
        REQUIRE(w.number == numberTest2);
    }
        // Get all available records by specified thread ID and check for invalid data
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "2");
        REQUIRE((*records).size() == 2);
        for (const auto &w : *records) {
            REQUIRE(w.body == bodyTest);
            REQUIRE(w.number == numberTest2);
        }

    // Get all available records by specified contact ID and check for invalid data
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
    REQUIRE((*records).size() == 2);
    for (const auto &w : *records) {
        REQUIRE(w.body == bodyTest);
        REQUIRE(w.number == numberTest);
    }
        // Get all available records by specified contact ID and check for invalid data
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
        REQUIRE((*records).size() == 2);
        for (const auto &w : *records) {
            REQUIRE(w.body == bodyTest);
            REQUIRE(w.number == numberTest);
        }

    // Get all available records by specified contact ID and check for invalid data
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "2");
    REQUIRE((*records).size() == 2);
    for (const auto &w : *records) {
        REQUIRE(w.body == bodyTest);
        REQUIRE(w.number == numberTest2);
    }
        // Get all available records by specified contact ID and check for invalid data
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "2");
        REQUIRE((*records).size() == 2);
        for (const auto &w : *records) {
            REQUIRE(w.body == bodyTest);
            REQUIRE(w.number == numberTest2);
        }

    // Remove sms records in order to check automatic management of threads and contact databases
    ThreadRecordInterface threadRecordInterface(smsDB.get(), contactsDB.get());
    REQUIRE(smsRecInterface.RemoveByID(1));
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
    REQUIRE((*records).size() == 1);

    REQUIRE(threadRecordInterface.GetCount() == 2);

    REQUIRE(smsRecInterface.RemoveByID(2));
    records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
    REQUIRE((*records).size() == 0);
    REQUIRE(threadRecordInterface.GetCount() == 1);

    REQUIRE(smsRecInterface.RemoveByID(3));
    REQUIRE(smsRecInterface.RemoveByID(4));
    REQUIRE(threadRecordInterface.GetCount() == 0);

    // Test removing a message which belongs to non-existent thread
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsDB->threads.removeById(1)); // stealthy thread remove
    REQUIRE(smsRecInterface.RemoveByID(1));

    // Test handling of missmatch in sms vs. thread tables
    auto trueCount = 10;
    // prepare
    for (auto added = 0; added < trueCount; added++) {
        recordIN.date = added;                     // for proper order
        recordIN.body = std::to_string(added + 1); // == ID
        REQUIRE(smsRecInterface.Add(recordIN));    // threadID = 1
    }
    ThreadRecord threadRec = threadRecordInterface.GetByID(1);
    REQUIRE(threadRec.isValid());
    ThreadsTableRow threadRaw{{.ID = threadRec.ID},
                              .date           = threadRec.date,
                              .msgCount       = threadRec.msgCount,
                              .unreadMsgCount = threadRec.unreadMsgCount,
                              .contactID      = threadRec.contactID,
                              .snippet        = threadRec.snippet,
                              .type           = threadRec.type};
    threadRaw.msgCount = trueCount + 1; // break the DB
    REQUIRE(smsDB->threads.update(threadRaw));

    REQUIRE(static_cast<int>(smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1")->size()) ==
            trueCount);
    // end of preparation, now test
    for (auto latest = trueCount; latest > 0; latest--) {
        REQUIRE(smsRecInterface.RemoveByID(latest)); // remove the latest
        switch (latest) {                            // was just removed
        case 3:                                      // remaining 2 or more
        default:
            REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() == std::to_string(latest - 1)); // next to newest
            break;
        case 2:                                                                             // remaining 1
            REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() == std::to_string(1)); // only one remaining
            break;
        case 1: // no sms remaining
            // make sure there is no thread nor sms
            REQUIRE(threadRecordInterface.GetCount() == 0);
            REQUIRE(smsRecInterface.GetCount() == 0);
            break;
        // Remove sms records in order to check automatic management of threads and contact databases
        ThreadRecordInterface threadRecordInterface(smsDB.get(), contactsDB.get());
        REQUIRE(smsRecInterface.RemoveByID(1));
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
        REQUIRE((*records).size() == 1);

        REQUIRE(threadRecordInterface.GetCount() == 2);

        REQUIRE(smsRecInterface.RemoveByID(2));
        records = smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ContactID, "1");
        REQUIRE((*records).size() == 0);
        REQUIRE(threadRecordInterface.GetCount() == 1);

        REQUIRE(smsRecInterface.RemoveByID(3));
        REQUIRE(smsRecInterface.RemoveByID(4));
        REQUIRE(threadRecordInterface.GetCount() == 0);

        // Test removing a message which belongs to non-existent thread
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsDB->threads.removeById(1)); // stealthy thread remove
        REQUIRE(smsRecInterface.RemoveByID(1));

        // Test handling of missmatch in sms vs. thread tables
        auto trueCount = 10;
        // prepare
        for (auto added = 0; added < trueCount; added++) {
            recordIN.date = added;                     // for proper order
            recordIN.body = std::to_string(added + 1); // == ID
            REQUIRE(smsRecInterface.Add(recordIN));    // threadID = 1
        }
        ThreadRecord threadRec = threadRecordInterface.GetByID(1);
        REQUIRE(threadRec.isValid());
        ThreadsTableRow threadRaw{{.ID = threadRec.ID},
                                  .date           = threadRec.date,
                                  .msgCount       = threadRec.msgCount,
                                  .unreadMsgCount = threadRec.unreadMsgCount,
                                  .contactID      = threadRec.contactID,
                                  .snippet        = threadRec.snippet,
                                  .type           = threadRec.type};
        threadRaw.msgCount = trueCount + 1; // break the DB
        REQUIRE(smsDB->threads.update(threadRaw));

        REQUIRE(static_cast<int>(
                    smsRecInterface.GetLimitOffsetByField(0, 100, SMSRecordField::ThreadID, "1")->size()) == trueCount);
        // end of preparation, now test
        for (auto latest = trueCount; latest > 0; latest--) {
            REQUIRE(smsRecInterface.RemoveByID(latest)); // remove the latest
            switch (latest) {                            // was just removed
            case 3:                                      // remaining 2 or more
            default:
                REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() ==
                        std::to_string(latest - 1)); // next to newest
                break;
            case 2:                                                                             // remaining 1
                REQUIRE(threadRecordInterface.GetByID(1).snippet.c_str() == std::to_string(1)); // only one remaining
                break;
            case 1: // no sms remaining
                // make sure there is no thread nor sms
                REQUIRE(threadRecordInterface.GetCount() == 0);
                REQUIRE(smsRecInterface.GetCount() == 0);
                break;
            }
        }

        // Test removing by field
        recordIN.number = numberTest;
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ThreadID, "1"));
        REQUIRE(smsRecInterface.GetCount() == 0);

        recordIN.number = numberTest;
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ContactID, "1"));
        REQUIRE(smsRecInterface.GetCount() == 0);
    }

    // Test removing by field
    recordIN.number = numberTest;
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ThreadID, "1"));
    REQUIRE(smsRecInterface.GetCount() == 0);

    recordIN.number = numberTest;
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.Add(recordIN));
    REQUIRE(smsRecInterface.RemoveByField(SMSRecordField::ContactID, "1"));
    REQUIRE(smsRecInterface.GetCount() == 0);
    SECTION("SMS Record Draft and Input test")
    {
        recordIN.type = SMSType ::INBOX;

        // Add 3 INBOX Records
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));
        REQUIRE(smsRecInterface.Add(recordIN));

        // Add 1 Draft Records
        recordIN.type = SMSType ::DRAFT;
        recordIN.body = "Draft Message";
        REQUIRE(smsRecInterface.Add(recordIN));

        auto query  = std::make_shared<db::query::SMSGetForList>(1, 0, 100);
        auto ret    = smsRecInterface.runQuery(query);
        auto result = dynamic_cast<db::query::SMSGetForListResult *>(ret.get());
        REQUIRE(result != nullptr);

        REQUIRE(result->getCount() == 3);
        REQUIRE(result->getResults().size() == 4);
        REQUIRE(result->getResults().back().type == SMSType::INPUT);
        REQUIRE(result->getDraft().type == SMSType::DRAFT);
    }

    Database::deinitialize();
}

M module-db/tests/SMSTable_tests.cpp => module-db/tests/SMSTable_tests.cpp +74 -56
@@ 1,13 1,3 @@

/*
 * @file smsdb.sms_tests.cpp
 * @author Mateusz Piesta (mateusz.piesta@mudita.com)
 * @date 28.05.19
 * @brief
 * @copyright Copyright (C) 2019 mudita.com
 * @details
 */

#include "vfs.hpp"

#include <catch2/catch.hpp>


@@ 18,9 8,6 @@
#include "Tables/SettingsTable.hpp"

#include <algorithm>

#include <cstdint>
#include <cstdio>
#include <cstring>

TEST_CASE("SMS Table tests")


@@ 43,64 30,95 @@ TEST_CASE("SMS Table tests")

    };

    // add 4 elements into table
    REQUIRE(smsdb.sms.add(testRow1));
    REQUIRE(smsdb.sms.add(testRow1));
    REQUIRE(smsdb.sms.add(testRow1));
    REQUIRE(smsdb.sms.add(testRow1));
    SMSTableRow testRow2 = {{.ID = 0},
                            .threadID  = 0,
                            .contactID = 0,
                            .date      = 0,
                            .dateSent  = 0,
                            .errorCode = 0,
                            .body      = "Test Draft SMS",
                            .type      = SMSType ::DRAFT

    };

    SECTION("SMS Table test")
    {
        // add 4 elements into table
        REQUIRE(smsdb.sms.add(testRow1));
        REQUIRE(smsdb.sms.add(testRow1));
        REQUIRE(smsdb.sms.add(testRow1));
        REQUIRE(smsdb.sms.add(testRow1));

        // Table should have 4 elements
        REQUIRE(smsdb.sms.count() == 4);

        // update existing element in table
        testRow1.ID   = 4;
        testRow1.body = "updated Test SMS message ";
        REQUIRE(smsdb.sms.update(testRow1));

        // Get table row using valid ID & check if it was updated
        auto sms = smsdb.sms.getById(4);
        REQUIRE(sms.body == testRow1.body);

        // Get table row using invalid ID(should return empty smsdb.smsRow)
        auto smsFailed = smsdb.sms.getById(100);
        REQUIRE(smsFailed.body == "");

        // Get table rows using valid offset/limit parameters
        auto retOffsetLimit = smsdb.sms.getLimitOffset(0, 4);
        REQUIRE(retOffsetLimit.size() == 4);

    // Table should have 4 elements
    REQUIRE(smsdb.sms.count() == 4);
        // Get table rows using valid offset/limit parameters and specific field's ID
        REQUIRE(smsdb.sms.getLimitOffsetByField(0, 4, SMSTableFields::Date, "0").size() == 4);

    // update existing element in table
    testRow1.ID   = 4;
    testRow1.body = "updated Test SMS message ";
    REQUIRE(smsdb.sms.update(testRow1));
        // Get table rows using invalid limit parameters(should return 4 elements instead of 100)
        auto retOffsetLimitBigger = smsdb.sms.getLimitOffset(0, 100);
        REQUIRE(retOffsetLimitBigger.size() == 4);

    // Get table row using valid ID & check if it was updated
    auto sms = smsdb.sms.getById(4);
    REQUIRE(sms.body == testRow1.body);
        // Get table rows using invalid offset/limit parameters(should return empty object)
        auto retOffsetLimitFailed = smsdb.sms.getLimitOffset(5, 4);
        REQUIRE(retOffsetLimitFailed.size() == 0);

    // Get table row using invalid ID(should return empty smsdb.smsRow)
    auto smsFailed = smsdb.sms.getById(100);
    REQUIRE(smsFailed.body == "");
        // Get count of elements by field's ID
        REQUIRE(smsdb.sms.countByFieldId("thread_id", 0) == 4);

    // Get table rows using valid offset/limit parameters
    auto retOffsetLimit = smsdb.sms.getLimitOffset(0, 4);
    REQUIRE(retOffsetLimit.size() == 4);
        // Get count of elements by invalid field's ID
        REQUIRE(smsdb.sms.countByFieldId("invalid_field", 0) == 0);

    // Get table rows using valid offset/limit parameters and specific field's ID
    REQUIRE(smsdb.sms.getLimitOffsetByField(0, 4, SMSTableFields::Date, "0").size() == 4);
        REQUIRE(smsdb.sms.removeById(2));

    // Get table rows using invalid limit parameters(should return 4 elements instead of 100)
    auto retOffsetLimitBigger = smsdb.sms.getLimitOffset(0, 100);
    REQUIRE(retOffsetLimitBigger.size() == 4);
        // Table should have now 3 elements
        REQUIRE(smsdb.sms.count() == 3);

    // Get table rows using invalid offset/limit parameters(should return empty object)
    auto retOffsetLimitFailed = smsdb.sms.getLimitOffset(5, 4);
    REQUIRE(retOffsetLimitFailed.size() == 0);
        // Remove non existing element
        REQUIRE(smsdb.sms.removeById(100));

    // Get count of elements by field's ID
    REQUIRE(smsdb.sms.countByFieldId("thread_id", 0) == 4);
        // Remove all elements from table
        REQUIRE(smsdb.sms.removeById(1));
        REQUIRE(smsdb.sms.removeById(3));
        REQUIRE(smsdb.sms.removeById(4));

    // Get count of elements by invalid field's ID
    REQUIRE(smsdb.sms.countByFieldId("invalid_field", 0) == 0);
        // Table should be empty now
        REQUIRE(smsdb.sms.count() == 0);
    }

    REQUIRE(smsdb.sms.removeById(2));
    SECTION("SMS Draft and Input Table test")
    {
        REQUIRE(smsdb.sms.add(testRow1));
        REQUIRE(smsdb.sms.add(testRow1));
        REQUIRE(smsdb.sms.add(testRow2));

    // Table should have now 3 elements
    REQUIRE(smsdb.sms.count() == 3);
        REQUIRE(smsdb.sms.countWithoutDraftsByThreadId(0) == 2);
        REQUIRE(smsdb.sms.count() == 3);

    // Remove non existing element
    REQUIRE(smsdb.sms.removeById(100));
        REQUIRE(smsdb.sms.getDraftByThreadId(0).body == "Test Draft SMS");

    // Remove all elements from table
    REQUIRE(smsdb.sms.removeById(1));
    REQUIRE(smsdb.sms.removeById(3));
    REQUIRE(smsdb.sms.removeById(4));
        auto results = smsdb.sms.getByThreadIdWithoutDraftWithEmptyInput(0, 0, 10);

    // Table should be empty now
    REQUIRE(smsdb.sms.count() == 0);
        REQUIRE(results.size() == 3);
        REQUIRE(results.back().type == SMSType ::INPUT);
    }

    Database::deinitialize();
}

M module-gui/gui/widgets/BoxLayout.cpp => module-gui/gui/widgets/BoxLayout.cpp +16 -0
@@ 3,6 3,7 @@
#include <InputEvent.hpp>
#include <Label.hpp>
#include <log/log.hpp>
#include "assert.h"

namespace gui
{


@@ 135,11 136,22 @@ namespace gui
        setVisible(value, false);
    }

    unsigned int BoxLayout::getVisibleChildrenCount()
    {
        assert(children.size() >= outOfDrawAreaItems.size());
        return children.size() - outOfDrawAreaItems.size();
    }

    void BoxLayout::setReverseOrder(bool value)
    {
        reverseOrder = value;
    }

    bool BoxLayout::getReverseOrder()
    {
        return reverseOrder;
    }

    void BoxLayout::addToOutOfDrawAreaList(Item *it)
    {
        if (it->visible) {


@@ 452,6 464,10 @@ namespace gui
        resizeItems(); // vs mark dirty
        setNavigation();

        if (parentOnRequestedResizeCallback != nullptr) {
            parentOnRequestedResizeCallback();
        }

        return granted;
    }


M module-gui/gui/widgets/BoxLayout.hpp => module-gui/gui/widgets/BoxLayout.hpp +5 -1
@@ 101,11 101,15 @@ namespace gui
        /// set navigation from last to fist element in box
        virtual void setNavigation();
        std::list<Item *>::iterator getNavigationFocusedItem();
        unsigned int getFocusItemIndex() const;
        [[nodiscard]] unsigned int getFocusItemIndex() const;
        [[nodiscard]] unsigned int getVisibleChildrenCount();
        /// If requested causes box to change its structure parent may need to do some actions via this callback.
        std::function<void()> parentOnRequestedResizeCallback = nullptr;
        void setVisible(bool value) override;
        /// set visible but from previous scope... (page, element etc)
        void setVisible(bool value, bool previous);
        void setReverseOrder(bool value);
        [[nodiscard]] bool getReverseOrder();
        /// callback for situaton when we reached top/bottom/left/right of box
        /// if we want to do sth special (i.e. request new items)
        std::function<bool(const InputEvent &inputEvent)> borderCallback = nullptr;

M module-gui/gui/widgets/ListView.cpp => module-gui/gui/widgets/ListView.cpp +115 -41
@@ 17,14 17,17 @@ namespace gui
        activeItem = false;
    }

    bool ListViewScroll::shouldShowScroll(int currentPageSize, int elementsCount)
    bool ListViewScroll::shouldShowScroll(unsigned int currentPageSize, unsigned int elementsCount)
    {

        return ((parent->widgetArea.w > style::listview::scroll::min_space) &&
                (parent->widgetArea.h > style::listview::scroll::min_space) && currentPageSize < elementsCount);
    }

    void ListViewScroll::update(int startIndex, int currentPageSize, int elementsCount, int topMargin)
    void ListViewScroll::update(unsigned int startIndex,
                                unsigned int currentPageSize,
                                unsigned int elementsCount,
                                int topMargin)
    {
        if (shouldShowScroll(currentPageSize, elementsCount)) {



@@ 64,8 67,8 @@ namespace gui
        this->setBorderColor(ColorNoColor);

        body = new VBox{this, 0, 0, w, h};
        body->setBorderColor(ColorNoColor);
        body->setAlignment(Alignment::Vertical::Top);
        body->setEdges(RectangleEdgeFlags::GUI_RECT_EDGE_NO_EDGES);

        body->borderCallback = [this](const InputEvent &inputEvent) -> bool {
            if (inputEvent.state != InputEvent::State::keyReleasedShort) {


@@ 82,6 85,11 @@ namespace gui
            }
        };

        body->parentOnRequestedResizeCallback = [this]() {
            if (pageLoaded)
                recalculateOnBoxRequestedResize();
        };

        scroll = new ListViewScroll(this,
                                    style::listview::scroll::x,
                                    style::listview::scroll::y,


@@ 113,36 121,51 @@ namespace gui
        scrollTopMargin = value;
    }

    void ListView::setOrientation(style::listview::Orientation value)
    {
        orientation = value;

        if (orientation == style::listview::Orientation::TopBottom) {
            body->setAlignment(Alignment::Vertical::Top);
        }
        else {
            body->setAlignment(Alignment::Vertical::Bottom);
        }
    }

    void ListView::setProvider(std::shared_ptr<ListItemProvider> prov)
    {
        if (prov != nullptr) {
            provider       = prov;
            provider->list = this;

            rebuildList();
        }
    }

    void ListView::rebuildList(style::listview::RebuildType rebuildType, unsigned int dataOffset)
    void ListView::rebuildList(style::listview::RebuildType rebuildType, unsigned int dataOffset, bool forceRebuild)
    {
        setElementsCount(provider->requestRecordsCount());
        if (pageLoaded || forceRebuild) {
            setElementsCount(provider->requestRecordsCount());

        setup(rebuildType, dataOffset);
        clearItems();
            setup(rebuildType, dataOffset);
            clearItems();

        // If deletion operation caused last page to be removed request previous one.
        if (startIndex != 0 && startIndex == elementsCount) {
            requestPreviousPage();
            // If deletion operation caused last page to be removed request previous one.
            if ((startIndex != 0 && startIndex == elementsCount)) {
                requestPreviousPage();
            }
            else {
                provider->requestRecords(startIndex, calculateLimit());
            }
        }
        else {
            provider->requestRecords(startIndex, calculateLimit());
            rebuildRequests.push_front({rebuildType, dataOffset});
        }
    };

    void ListView::setup(style::listview::RebuildType rebuildType, unsigned int dataOffset)
    {
        if (rebuildType == style::listview::RebuildType::Full) {
            startIndex       = 0;
            setStartIndex();
            storedFocusIndex = 0;
        }
        else if (rebuildType == style::listview::RebuildType::OnOffset) {


@@ 155,6 178,7 @@ namespace gui
            }
        }
        else if (rebuildType == style::listview::RebuildType::InPlace) {

            storedFocusIndex = body->getFocusItemIndex();

            if (direction == style::listview::Direction::Top) {


@@ 181,9 205,9 @@ namespace gui
    void ListView::clear()
    {
        clearItems();
        setStartIndex();
        body->setReverseOrder(false);
        startIndex = 0;
        direction  = style::listview::Direction::Bottom;
        direction = style::listview::Direction::Bottom;
    }

    void ListView::clearItems()


@@ 222,6 246,12 @@ namespace gui
        resizeWithScroll();
        checkFirstPage();
        pageLoaded = true;

        // Check if there were queued rebuild Requests - if so rebuild list again.
        if (!rebuildRequests.empty()) {
            rebuildList(rebuildRequests.back().first, rebuildRequests.back().second);
            rebuildRequests.pop_back();
        }
    }

    void ListView::onProviderDataUpdate()


@@ 237,24 267,41 @@ namespace gui
        return Order::Previous;
    }

    void ListView::setStartIndex()
    {
        if (orientation == style::listview::Orientation::TopBottom) {
            startIndex = 0;
        }
        else {
            startIndex = elementsCount;
        }
    }

    void ListView::recalculateStartIndex()
    {
        if (direction == style::listview::Direction::Top) {
            startIndex = startIndex - currentPageSize > 0 ? startIndex - currentPageSize : 0;

            startIndex = startIndex < currentPageSize ? 0 : startIndex - currentPageSize;
        }
    }

    void ListView::checkFirstPage()
    {
        // On direction top check if first page is filled with items. If not reload page with direction bottom so it
        // will be filled.
        if (direction == style::listview::Direction::Top && startIndex == 0) {
        // Check if first page is filled with items. If not reload page to be filed with items. Check for both
        // Orientations.
        if (orientation == style::listview::Orientation::TopBottom && direction == style::listview::Direction::Top &&
            startIndex == 0) {
            if (body->getSizeLeft() > provider->getMinimalItemHeight()) {
                clearItems();
                body->setReverseOrder(false);
                direction       = style::listview::Direction::Bottom;
                focusOnLastItem = true;
                provider->requestRecords(startIndex, calculateLimit());
                rebuildList();
            }
        }

        if (orientation == style::listview::Orientation::BottomTop && direction == style::listview::Direction::Bottom &&
            startIndex + currentPageSize == elementsCount) {
            if (body->getSizeLeft() > provider->getMinimalItemHeight()) {
                focusOnLastItem = true;
                rebuildList();
            }
        }
    }


@@ 298,8 345,6 @@ namespace gui
            if (!body->setFocusOnElement(storedFocusIndex)) {
                body->setFocusOnLastElement();
            }

            storedFocusIndex = 0;
        }

        if (focusOnLastItem) {


@@ 321,11 366,40 @@ namespace gui
    bool ListView::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim)
    {
        Rect::onDimensionChanged(oldDim, newDim);
        body->setSize(newDim.w, newDim.h);
        refresh();
        body->setSize(body->getWidth(), newDim.h);
        scroll->update(startIndex, currentPageSize, elementsCount, scrollTopMargin);

        return true;
    }

    auto ListView::handleRequestResize([[maybe_unused]] const Item *child,
                                       unsigned short request_w,
                                       unsigned short request_h) -> Size
    {

        return Size(request_w, request_h);
    }

    void ListView::recalculateOnBoxRequestedResize()
    {
        if (currentPageSize != body->getVisibleChildrenCount()) {

            unsigned int diff = currentPageSize < body->getVisibleChildrenCount()
                                    ? 0
                                    : currentPageSize - body->getVisibleChildrenCount();
            currentPageSize = body->getVisibleChildrenCount();

            if (direction == style::listview::Direction::Top) {
                startIndex += diff;
            }
            else {
                startIndex = startIndex < diff ? 0 : startIndex - diff;
            }

            checkFirstPage();
        }
    }

    bool ListView::onInput(const InputEvent &inputEvent)
    {
        return body->onInput(inputEvent);


@@ 339,11 413,11 @@ namespace gui
        return count;
    }

    unsigned int ListView::calculateLimit()
    unsigned int ListView::calculateLimit(style::listview::Direction value)
    {
        auto minLimit =
            (2 * currentPageSize > calculateMaxItemsOnPage() ? 2 * currentPageSize : calculateMaxItemsOnPage());
        if (direction == style::listview::Direction::Bottom)
        if (value == style::listview::Direction::Bottom)
            return (minLimit + startIndex <= elementsCount ? minLimit : elementsCount - startIndex);
        else
            return minLimit < startIndex ? minLimit : startIndex;


@@ 351,16 425,13 @@ namespace gui

    bool ListView::requestNextPage()
    {
        direction = style::listview::Direction::Bottom;
        body->setReverseOrder(false);

        if (startIndex + currentPageSize >= elementsCount && boundaries == style::listview::Boundaries::Continuous) {

            startIndex = 0;
        }
        else if (startIndex + currentPageSize >= elementsCount && boundaries == style::listview::Boundaries::Fixed) {

            return true;
            return false;
        }
        else {



@@ 368,6 439,8 @@ namespace gui
                                                                       : elementsCount - (elementsCount - startIndex);
        }

        direction = style::listview::Direction::Bottom;
        body->setReverseOrder(false);
        pageLoaded = false;
        provider->requestRecords(startIndex, calculateLimit());



@@ 376,8 449,6 @@ namespace gui

    bool ListView::requestPreviousPage()
    {
        direction = style::listview::Direction::Top;
        body->setReverseOrder(true);
        auto topFetchIndex = 0;
        auto limit         = 0;



@@ 385,22 456,25 @@ namespace gui

            topFetchIndex = elementsCount - (elementsCount % currentPageSize);
            startIndex    = elementsCount;
            limit         = calculateLimit() - topFetchIndex;
            limit         = calculateLimit(style::listview::Direction::Top) - topFetchIndex;
        }
        else if (startIndex == 0 && boundaries == style::listview::Boundaries::Fixed) {

            return true;
            return false;
        }
        else {

            limit         = calculateLimit();
            topFetchIndex = startIndex - calculateLimit() > 0 ? startIndex - calculateLimit() : 0;
            limit         = calculateLimit(style::listview::Direction::Top);
            topFetchIndex = startIndex < calculateLimit(style::listview::Direction::Top)
                                ? 0
                                : startIndex - calculateLimit(style::listview::Direction::Top);
        }

        direction = style::listview::Direction::Top;
        body->setReverseOrder(true);
        pageLoaded = false;
        provider->requestRecords(topFetchIndex, limit);

        return true;
    }

} /* namespace gui */

M module-gui/gui/widgets/ListView.hpp => module-gui/gui/widgets/ListView.hpp +14 -7
@@ 16,8 16,8 @@ namespace gui
      public:
        ListViewScroll(Item *parent, uint32_t x, uint32_t y, uint32_t w, uint32_t h);

        bool shouldShowScroll(int listPageSize, int elementsCount);
        void update(int startIndex, int listPageSize, int elementsCount, int topMargin);
        bool shouldShowScroll(unsigned int listPageSize, unsigned int elementsCount);
        void update(unsigned int startIndex, unsigned int listPageSize, unsigned int elementsCount, int topMargin);
    };

    class ListView : public Rect


@@ 29,14 29,16 @@ namespace gui
        std::shared_ptr<ListItemProvider> provider = nullptr;
        VBox *body                                 = nullptr;
        ListViewScroll *scroll                     = nullptr;
        std::list<std::pair<style::listview::RebuildType, unsigned int>> rebuildRequests;

        unsigned int currentPageSize = 0;
        bool pageLoaded              = false;
        bool pageLoaded              = true;
        bool focusOnLastItem         = false;
        int scrollTopMargin          = style::margins::big;

        style::listview::Boundaries boundaries = style::listview::Boundaries::Fixed;
        style::listview::Direction direction   = style::listview::Direction::Bottom;
        style::listview::Boundaries boundaries   = style::listview::Boundaries::Fixed;
        style::listview::Direction direction     = style::listview::Direction::Bottom;
        style::listview::Orientation orientation = style::listview::Orientation::TopBottom;

        void clearItems();
        virtual void addItemsOnPage();


@@ 45,8 47,10 @@ namespace gui
        void resizeWithScroll();
        void recalculateStartIndex();
        void checkFirstPage();
        void setStartIndex();
        void recalculateOnBoxRequestedResize();
        unsigned int calculateMaxItemsOnPage();
        unsigned int calculateLimit();
        unsigned int calculateLimit(style::listview::Direction value = style::listview::Direction::Bottom);
        Order getOrderFromDirection();
        virtual bool requestNextPage();
        virtual bool requestPreviousPage();


@@ 60,9 64,11 @@ namespace gui
        void setElementsCount(unsigned int count);
        void setProvider(std::shared_ptr<ListItemProvider> provider);
        void rebuildList(style::listview::RebuildType rebuildType = style::listview::RebuildType::Full,
                         unsigned int dataOffset                  = 0);
                         unsigned int dataOffset                  = 0,
                         bool forceRebuild                        = false);
        void clear();
        std::shared_ptr<ListItemProvider> getProvider();
        void setOrientation(style::listview::Orientation value);
        void setBoundaries(style::listview::Boundaries value);
        void setScrollTopMargin(int value);
        void setAlignment(const Alignment &value) override;


@@ 72,6 78,7 @@ namespace gui
        std::list<DrawCommand *> buildDrawList() override;
        bool onInput(const InputEvent &inputEvent) override;
        bool onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim) override;
        auto handleRequestResize(const Item *, unsigned short request_w, unsigned short request_h) -> Size override;
    };

} /* namespace gui */

M module-gui/gui/widgets/Rect.hpp => module-gui/gui/widgets/Rect.hpp +1 -1
@@ 31,7 31,7 @@ namespace gui
        /// mark if we japs should be painted. small protrusions indicating a speech bubble
        RectangleYapFlags yaps = {RectangleYapFlags::GUI_RECT_YAP_NO_YAPS};
        /// yap size in horizontal width.
        unsigned short yapSize = {style::window::messages::yaps_size_default};
        unsigned short yapSize = style::window::default_rect_yaps;

      public:
        Rect();

M module-gui/gui/widgets/Style.hpp => module-gui/gui/widgets/Style.hpp +9 -25
@@ 37,6 37,7 @@ namespace style
            const inline std::string medium = "gt_pressura_regular_24";
        }; // namespace font
    };     // namespace footer

    namespace window
    {
        const inline uint32_t default_left_margin  = 20;


@@ 48,6 49,7 @@ namespace style
        const inline uint32_t default_border_focus_w       = 2;
        const inline uint32_t default_border_rect_no_focus = 1;
        const inline uint32_t default_border_no_focus_w    = 0;
        const inline uint32_t default_rect_yaps            = 10;
        namespace font
        {
            const inline std::string supersizemelight = "gt_pressura_light_90";


@@ 79,32 81,8 @@ namespace style
        /// minimal label decoration for Option
        void decorateOption(gui::Label *el);

        namespace messages
        {
            inline const uint32_t sms_radius                    = 7;
            inline const uint32_t sms_border_no_focus           = 1;
            inline const uint32_t sms_thread_item_h             = 100;
            const inline unsigned short yaps_size_default       = 10;
            const inline gui::Length sms_max_width              = 320;
            const inline unsigned short sms_h_padding           = 15;
            const inline unsigned short sms_h_left_padding      = 25;
            const inline unsigned short sms_v_padding           = 10;
            const inline unsigned short sms_vertical_spacer     = 10;
            const inline unsigned short new_sms_vertical_spacer = 25;
            const inline unsigned short sms_failed_offset       = 39;
            const inline unsigned short sms_error_icon_offset   = 2;
            const inline gui::Padding sms_left_bubble_padding =
                gui::Padding(style::window::messages::sms_h_left_padding,
                             style::window::messages::sms_v_padding,
                             style::window::messages::sms_h_padding,
                             style::window::messages::sms_v_padding);
            const inline gui::Padding sms_right_bubble_padding = gui::Padding(style::window::messages::sms_h_padding,
                                                                              style::window::messages::sms_v_padding,
                                                                              style::window::messages::sms_h_padding,
                                                                              style::window::messages::sms_v_padding);
        } // namespace messages

    }; // namespace window

    namespace settings
    {
        namespace date


@@ 216,6 194,12 @@ namespace style
                     ///< offset.
        };

        enum class Orientation
        {
            TopBottom,
            BottomTop
        };

        namespace scroll
        {
            const inline uint32_t x           = 0;

M module-gui/gui/widgets/Text.cpp => module-gui/gui/widgets/Text.cpp +8 -6
@@ 333,8 333,9 @@ namespace gui
                                                               : area(Area::Max).h;
        }

        Length w             = sizeMinusPadding(Axis::X, Area::Max);
        Length h             = area(Area::Normal).size(Axis::Y);
        Length w = sizeMinusPadding(Axis::X, Area::Max);
        Length h = sizeMinusPadding(Axis::Y, Area::Max);

        auto line_y_position = padding.top;
        auto cursor          = 0;



@@ 393,12 394,13 @@ namespace gui
        // should be done on each loop
        {
            uint16_t h_used = line_y_position + padding.bottom;
            uint16_t w_used = lines.maxWidth();
            uint16_t w_used = lines.maxWidth() + padding.getSumInAxis(Axis::X);

            if (lines.size() == 0) {
                debug_text("No lines to show, try to at least fit in cursor");
                if (format.getFont() != nullptr) {
                    h_used += format.getFont()->info.line_height;
                    w_used += TextCursor::default_width;
                if (format.getFont() != nullptr && line_y_position < format.getFont()->info.line_height) {
                    h_used = format.getFont()->info.line_height;
                    w_used = TextCursor::default_width;
                    debug_text("empty line height: %d", h_used);
                }
            }

M module-gui/test/test-google/test-gui-listview.cpp => module-gui/test/test-google/test-gui-listview.cpp +1 -0
@@ 51,6 51,7 @@ class ListViewTesting : public ::testing::Test
        ASSERT_EQ(0, testListView->currentPageSize) << "List should be empty";

        testListView->setProvider(testProvider);
        testListView->rebuildList();
    }

    void TearDown() override