~aleteoryx/muditaos

5d4c178103403a97dd1e63125f8115ffd3cf5dab — rrandomsky 2 years ago e008509
[MOS-1020] Fix for losing unsaved data

Fix for losing unsaved data when the user click the BACK button while
creating or editing note/contact.
M image/system_a/data/lang/Deutsch.json => image/system_a/data/lang/Deutsch.json +3 -1
@@ 716,5 716,7 @@
    "tethering_enable_question": "<text>Sie sind mit dem Computer verbunden.<br />Tethering einschalten?<br /><text color='4'>(einige Funktionen k\u00f6nnen deaktiviert sein)</text></text>",
    "tethering_menu_access_decline": "<text>Tethering ist eingeschaltet.<br /><br />Schalten Sie Tethering aus,<br />um auf das Men\u00fc zuzugreifen</text>",
    "tethering_phone_mode_change_prohibited": "<text>Tethering ist eingeschaltet.<br /><br />Andere Betriebsarten (Verbunden, DND,<br />Offline) werden von dieser Betriebsart \u00fcberlagert<br />und funktionieren nicht.</text>",
    "tethering_turn_off_question": "Tethering ausschalten?"
    "tethering_turn_off_question": "Tethering ausschalten?",
    "unsaved_changes": "Ungespeicherte \u00c4nderungen",
    "exit_without_saving": "M\u00f6chten Sie den Vorgang ohne Speichern beenden?"
}

M image/system_a/data/lang/English.json => image/system_a/data/lang/English.json +3 -1
@@ 727,5 727,7 @@
    "tethering_enable_question": "<text>You're connected to the computer.<br />Turn tethering on?<br /><text color='4'>(some functions may be disabled)</text></text>",
    "tethering_menu_access_decline": "<text>Tethering is on.<br /><br />To access menu,<br />turn tethering off.</text>",
    "tethering_phone_mode_change_prohibited": "<text>Tethering is on.<br /><br />Other modes (Connected, DND,<br />Offline) are overriden by this mode<br />and are not working.</text>",
    "tethering_turn_off_question": "Turn tethering off?"
    "tethering_turn_off_question": "Turn tethering off?",
    "unsaved_changes": "Unsaved changes",
    "exit_without_saving": "<text>Do you want to exit </text><br></br><text weight='bold'>without saving?</text>"
}

M image/system_a/data/lang/Espanol.json => image/system_a/data/lang/Espanol.json +3 -1
@@ 716,5 716,7 @@
    "tethering_enable_question": "<text>Est\u00e1s conectado al ordenador.<br />\u00bfActivar el anclaje de red?<br /><text color='4'>(algunas funciones podr\u00edan desactivarse)</text></text>",
    "tethering_menu_access_decline": "<text>El anclaje de red est\u00e1 activado.<br /><br />Para acceder al men\u00fa,<br />desactiva el tethering.</text>",
    "tethering_phone_mode_change_prohibited": "<text>El anclaje de red est\u00e1 activado.<br /><br />Este modo anula otros modos (Conectado, No molestar,<br />Desconectado) <br />y hace que dejen de funcionar.</text>",
    "tethering_turn_off_question": "\u00bfDesactivar el anclaje de red?"
    "tethering_turn_off_question": "\u00bfDesactivar el anclaje de red?",
    "unsaved_changes": "Cambios no guardados",
    "exit_without_saving": "\u00bfQuieres salir sin guardar?"
}

M image/system_a/data/lang/Francais.json => image/system_a/data/lang/Francais.json +3 -1
@@ 687,5 687,7 @@
    "tethering_enable_question": "<text>Vous \u00eates connect\u00e9 \u00e0 l'ordinateur.<br />Voulez-vous activer le partage de connexion?<br /><text color='4'>(certaines fonctions peuvent \u00eatre d\u00e9sactiv\u00e9es)</text></text>",
    "tethering_menu_access_decline": "<text>Le partage de connexion est activ\u00e9.<br /><br />Pour acc\u00e9der au menu, veuillez<br />d\u00e9sactiver le partage de connextion.</text>",
    "tethering_phone_mode_change_prohibited": "<text>Le partage de connexion est activ\u00e9.<br /><br />Ce mode-ci remplace et d\u00e9sactive les autres modes<br />(Connect\u00e9, NPD, Hors ligne)</text>",
    "tethering_turn_off_question": "Voulez-vous d\u00e9sactiver le partage de connexion?"
    "tethering_turn_off_question": "Voulez-vous d\u00e9sactiver le partage de connexion?",
    "unsaved_changes": "Modifications non enregistr\u00e9es",
    "exit_without_saving": "Voulez-vous quitter sans sauvegarder ?"
}

M image/system_a/data/lang/Polski.json => image/system_a/data/lang/Polski.json +3 -1
@@ 715,5 715,7 @@
    "tethering_enable_question": "<text>Po\u0142\u0105czono z komputerem.<br />W\u0142\u0105czy\u0107 tethering?<br /><text color='4'>(Niekt\u00f3re funkcje mog\u0105 by\u0107 niedost\u0119pne)</text></text>",
    "tethering_menu_access_decline": "<text>Tethering w\u0142\u0105czony.<br /><br />Aby przej\u015b\u0107 do menu,<br />wy\u0142\u0105cz tethering.</text>",
    "tethering_phone_mode_change_prohibited": "<text>Tethering w\u0142\u0105czony.<br /><br />Ten tryb powoduje, \u017ce inne tryby (Po\u0142\u0105czony, Nie przeszkadza\u0107,<br />Offline) nie dzia\u0142aj\u0105.</text>",
    "tethering_turn_off_question": "Wy\u0142\u0105czy\u0107 tethering?"
    "tethering_turn_off_question": "Wy\u0142\u0105czy\u0107 tethering?",
    "unsaved_changes": "Niezapisane zmiany",
    "exit_without_saving": "<text>Czy chcesz wyj\u015b\u0107 </text><br></br><text weight='bold'>bez zapisywania?</text>"
}

M image/system_a/data/lang/Svenska.json => image/system_a/data/lang/Svenska.json +3 -1
@@ 526,5 526,7 @@
    "tethering_menu_access_decline": "<text>Internetdelning \u00e4r p\u00e5.<br /><br />F\u00f6r att \u00f6ppna menyn, <br />st\u00e4ng av internetdelning.</text>",
    "tethering_phone_mode_change_prohibited": "<text>Internetdelning \u00e4r p\u00e5.<br /><br />Andra l\u00e4gen (ansluten, DND, <br />offline) \u00e5sidos\u00e4tts av detta <br /> l\u00e4ge och fungerar inte.</text>",
    "tethering_turn_off_question": "St\u00e4nga av Internetdelning?",
    "volume_text": "LJUDVOLYM"
    "volume_text": "LJUDVOLYM",
    "unsaved_changes": "Osparade \u00e4ndringar",
    "exit_without_saving": "Vill du g\u00e5 h\u00e4rifr\u00e5n utan att spara?"
}

M module-apps/application-notes/ApplicationNotes.cpp => module-apps/application-notes/ApplicationNotes.cpp +4 -0
@@ 124,6 124,10 @@ namespace app
        windowsFactory.attach(window::name::option_window, [](ApplicationCommon *app, const std::string &name) {
            return std::make_unique<gui::OptionWindow>(app, name);
        });
        windowsFactory.attach(gui::name::window::notes_dialog_yes_no,
                              [](ApplicationCommon *app, const std::string &name) {
                                  return std::make_unique<gui::DialogYesNo>(app, name);
                              });

        attachPopups({gui::popup::ID::Volume,
                      gui::popup::ID::Tethering,

M module-apps/application-notes/include/application-notes/ApplicationNotes.hpp => module-apps/application-notes/include/application-notes/ApplicationNotes.hpp +2 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 14,6 14,7 @@ namespace gui::name::window
    inline constexpr auto notes_search_result = "NotesSearchResult";
    inline constexpr auto note_dialog         = "Dialog";
    inline constexpr auto note_confirm_dialog = "ConfirmDialog";
    inline constexpr auto notes_dialog_yes_no = "DialogYesNo";
} // namespace gui::name::window

namespace app

M module-apps/application-notes/windows/NoteEditWindow.cpp => module-apps/application-notes/windows/NoteEditWindow.cpp +24 -0
@@ 2,6 2,7 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "NoteEditWindow.hpp"
#include "DialogMetadataMessage.hpp"

#include <sstream>



@@ 155,6 156,24 @@ namespace app::notes
                    std::make_unique<gui::OptionsWindowOptions>(noteEditOptions(application, edit)));
            }
        }
        if (isCurrentTextDifferentThanSaved() &&
            (inputEvent.isShortRelease(gui::KeyCode::KEY_RF) || inputEvent.isLongRelease(gui::KeyCode::KEY_RF))) {

            // Show a popup warning about possible data loss
            auto metaData = std::make_unique<gui::DialogMetadataMessage>(
                gui::DialogMetadata{utils::translate("unsaved_changes"),
                                    "delete_128px_W_G",
                                    utils::translate("exit_without_saving"),
                                    "",
                                    [=]() -> bool {
                                        application->returnToPreviousWindow(); // To exit this popup
                                        application->returnToPreviousWindow(); // To switch back to previous window
                                        return true;
                                    }});

            application->switchWindow(gui::name::window::notes_dialog_yes_no, std::move(metaData));
            return true;
        }
        return AppWindow::onInput(inputEvent);
    }



@@ 169,4 188,9 @@ namespace app::notes
    {
        return (edit != nullptr) ? edit->isEmpty() : true;
    }

    bool NoteEditWindow::isCurrentTextDifferentThanSaved()
    {
        return notesRecord->snippet != edit->getText();
    }
} // namespace app::notes

M module-apps/application-notes/windows/NoteEditWindow.hpp => module-apps/application-notes/windows/NoteEditWindow.hpp +1 -0
@@ 42,6 42,7 @@ namespace app::notes
        void setCharactersCount(std::uint32_t count);
        void setNoteText(const UTF8 &text);
        void saveNote();
        bool isCurrentTextDifferentThanSaved();

        std::unique_ptr<NoteEditWindowContract::Presenter> presenter;
        std::shared_ptr<NotesRecord> notesRecord;

M module-apps/application-phonebook/models/NewContactModel.cpp => module-apps/application-phonebook/models/NewContactModel.cpp +11 -0
@@ 235,3 235,14 @@ void NewContactModel::openTextOptions(gui::Text *text)
    auto data = std::make_unique<PhonebookInputOptionData>(text);
    application->switchWindow(gui::window::name::input_options, std::move(data));
}
bool NewContactModel::isAnyUnsavedChange(std::shared_ptr<ContactRecord> contactRecord)
{
    for (const auto &item : internalData) {
        if (item->onCheckUnsavedChangeCallback) {
            if (item->onCheckUnsavedChangeCallback(contactRecord)) {
                return true;
            }
        }
    }
    return false; // there is no change between already provided data and saved ones
}

M module-apps/application-phonebook/models/NewContactModel.hpp => module-apps/application-phonebook/models/NewContactModel.hpp +1 -0
@@ 28,6 28,7 @@ class NewContactModel : public app::InternalModel<gui::ContactListItem *>, publi
    void createData();
    bool verifyData();
    bool emptyData();
    bool isAnyUnsavedChange(std::shared_ptr<ContactRecord> contactRecord);
    [[nodiscard]] auto getRequestType() -> PhonebookItemData::RequestType;

    [[nodiscard]] auto requestRecordsCount() -> unsigned int override;

M module-apps/application-phonebook/widgets/InputBoxWithLabelAndIconWidget.cpp => module-apps/application-phonebook/widgets/InputBoxWithLabelAndIconWidget.cpp +7 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "InputBoxWithLabelAndIconWidget.hpp"


@@ 172,6 172,9 @@ namespace gui
        onLoadCallback = [&](std::shared_ptr<ContactRecord> contact) {
            tickImage->visible = contact->isOnFavourites();
        };
        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            return tickImage->visible != contact->isOnFavourites();
        };
    }
    void InputBoxWithLabelAndIconWidget::addToICEHandler()
    {


@@ 214,6 217,9 @@ namespace gui
        };
        onSaveCallback = [&](std::shared_ptr<ContactRecord> contact) { contact->addToIce(tickImage->visible); };
        onLoadCallback = [&](std::shared_ptr<ContactRecord> contact) { tickImage->visible = contact->isOnIce(); };
        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            return tickImage->visible != contact->isOnIce();
        };
    }

} /* namespace gui */

M module-apps/application-phonebook/widgets/InputLinesWithLabelWidget.cpp => module-apps/application-phonebook/widgets/InputLinesWithLabelWidget.cpp +30 -0
@@ 163,6 163,10 @@ namespace gui
        onSaveCallback  = [&](std::shared_ptr<ContactRecord> contact) { contact->primaryName = inputText->getText(); };
        onLoadCallback  = [&](std::shared_ptr<ContactRecord> contact) { inputText->setText(contact->primaryName); };
        onEmptyCallback = [&]() { return inputText->isEmpty(); };

        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            return contact->primaryName != inputText->getText();
        };
    }
    void InputLinesWithLabelWidget::secondNameHandler()
    {


@@ 176,6 180,10 @@ namespace gui
        };
        onLoadCallback  = [&](std::shared_ptr<ContactRecord> contact) { inputText->setText(contact->alternativeName); };
        onEmptyCallback = [&]() { return inputText->isEmpty(); };

        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            return contact->alternativeName != inputText->getText();
        };
    }
    void InputLinesWithLabelWidget::numberHandler()
    {


@@ 208,6 216,11 @@ namespace gui
        };

        onEmptyCallback = [&]() { return inputText->isEmpty(); };

        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            std::string savedNumber = (!contact->numbers.empty()) ? (contact->numbers[0].number.getEntered()) : "";
            return savedNumber != std::string(inputText->getText());
        };
    }
    void InputLinesWithLabelWidget::secondNumberHandler()
    {


@@ 241,6 254,11 @@ namespace gui
        };

        onEmptyCallback = [&]() { return inputText->isEmpty(); };

        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            std::string savedNumber = (contact->numbers.size() > 1) ? (contact->numbers[1].number.getEntered()) : "";
            return savedNumber != std::string(inputText->getText());
        };
    }
    void InputLinesWithLabelWidget::emailHandler()
    {


@@ 252,6 270,10 @@ namespace gui
        onSaveCallback  = [&](std::shared_ptr<ContactRecord> contact) { contact->mail = inputText->getText(); };
        onLoadCallback  = [&](std::shared_ptr<ContactRecord> contact) { inputText->setText(contact->mail); };
        onEmptyCallback = [&]() { return inputText->isEmpty(); };

        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            return contact->mail != inputText->getText();
        };
    }
    void InputLinesWithLabelWidget::addressHandler()
    {


@@ 262,6 284,10 @@ namespace gui

        onSaveCallback = [&](std::shared_ptr<ContactRecord> contact) { contact->address = inputText->getText(); };
        onLoadCallback = [&](std::shared_ptr<ContactRecord> contact) { inputText->setText(contact->address); };

        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            return contact->address != inputText->getText();
        };
    }
    void InputLinesWithLabelWidget::noteHandler()
    {


@@ 272,6 298,10 @@ namespace gui

        onSaveCallback = [&](std::shared_ptr<ContactRecord> contact) { contact->note = inputText->getText(); };
        onLoadCallback = [&](std::shared_ptr<ContactRecord> contact) { inputText->setText(contact->note); };

        onCheckUnsavedChangeCallback = [&](std::shared_ptr<ContactRecord> contact) {
            return contact->note != inputText->getText();
        };
    }

} /* namespace gui */

M module-apps/application-phonebook/windows/PhonebookNewContact.cpp => module-apps/application-phonebook/windows/PhonebookNewContact.cpp +73 -37
@@ 115,6 115,22 @@ namespace gui
        return true;
    }

    void PhonebookNewContact::showDialogUnsavedChanges(std::function<bool()> whereToGoOnYes)
    {
        // Show a popup warning about possible data loss
        auto metaData = std::make_unique<gui::DialogMetadataMessage>(
            gui::DialogMetadata{utils::translate("unsaved_changes"),
                                "delete_128px_W_G",
                                utils::translate("exit_without_saving"),
                                "",
                                [=]() -> bool {
                                    application->returnToPreviousWindow(); // To exit this popup
                                    return whereToGoOnYes();
                                }});

        application->switchWindow(gui::window::name::dialog_yes_no, std::move(metaData));
    }

    void PhonebookNewContact::setSaveButtonVisible(bool visible)
    {
        navBar->setActive(nav_bar::Side::Center, visible);


@@ 135,14 151,31 @@ namespace gui
            return true;
        }
        else if (!inputEvent.isShortRelease(KeyCode::KEY_RF) || !shouldCurrentAppBeIgnoredOnSwitchBack()) {
            if (areUnsavedChanges()) {
                if (inputEvent.isShortRelease(gui::KeyCode::KEY_RF) || inputEvent.isLongRelease(gui::KeyCode::KEY_RF)) {
                    showDialogUnsavedChanges([this]() {
                        application->returnToPreviousWindow();
                        return true;
                    });
                    return true;
                }
            }
            return AppWindow::onInput(inputEvent);
        }

        return shouldCurrentAppBeIgnoredOnSwitchBack()
                   ? app::manager::Controller::switchBack(
                         application,
                         std::make_unique<app::manager::SwitchBackRequest>(nameOfPreviousApplication.value()))
                   : true;
        auto returnWhenCurrentAppShouldBeIgnoredOnSwitchBack = [this]() {
            return shouldCurrentAppBeIgnoredOnSwitchBack()
                       ? app::manager::Controller::switchBack(
                             application,
                             std::make_unique<app::manager::SwitchBackRequest>(nameOfPreviousApplication.value()))
                       : true;
        };

        if (areUnsavedChanges()) {
            showDialogUnsavedChanges(returnWhenCurrentAppShouldBeIgnoredOnSwitchBack);
            return true;
        }
        return returnWhenCurrentAppShouldBeIgnoredOnSwitchBack();
    }

    auto PhonebookNewContact::verifyAndSave() -> bool


@@ 207,38 240,37 @@ namespace gui
            DBServiceAPI::MatchContactByPhoneNumber(application, duplicatedNumber, duplicatedNumberContactID);
        const auto oldContactRecord = (matchedContact != nullptr) ? *matchedContact : ContactRecord{};

        auto metaData         = std::make_unique<gui::DialogMetadataMessage>(
            gui::DialogMetadata{duplicatedNumber.getFormatted(),
                                "info_128px_W_G",
                                text::RichTextParser()
                                    .parse(utils::translate("app_phonebook_duplicate_numbers"),
                                           nullptr,
                                           gui::text::RichTextParser::TokenMap(
                                               {{"$CONTACT_FORMATTED_NAME$", oldContactRecord.getFormattedName()}}))
                                    ->getText(),
                                "",
                                [=]() -> bool {
                                    if (contactAction == ContactAction::Add) {
                                        contact->ID = oldContactRecord.ID;
                                    }
                                    if (!DBServiceAPI::ContactUpdate(application, *contact)) {
                                        LOG_ERROR("Contact id=%" PRIu32 " update failed", contact->ID);
                                        return false;
                                    }

                                    /* Pop "Add contact" window from the stack so that clicking
                                     * back button after saving the modified contact returns to
                                     * contacts list, not to the "Add contact" window. */
                                    application->popWindow(gui::window::name::new_contact);

                                    /* Switch to contact details */
                                    auto switchData =
                                        std::make_unique<PhonebookItemData>(contact, newContactModel->getRequestType());
                                    switchData->ignoreCurrentWindowOnStack = true;
                                    application->switchWindow(gui::window::name::contact, std::move(switchData));

                                    return true;
                                }});
        auto metaData = std::make_unique<gui::DialogMetadataMessage>(gui::DialogMetadata{
            duplicatedNumber.getFormatted(),
            "info_128px_W_G",
            text::RichTextParser()
                .parse(utils::translate("app_phonebook_duplicate_numbers"),
                       nullptr,
                       gui::text::RichTextParser::TokenMap(
                           {{"$CONTACT_FORMATTED_NAME$", oldContactRecord.getFormattedName()}}))
                ->getText(),
            "",
            [=]() -> bool {
                if (contactAction == ContactAction::Add) {
                    contact->ID = oldContactRecord.ID;
                }
                if (!DBServiceAPI::ContactUpdate(application, *contact)) {
                    LOG_ERROR("Contact id=%" PRIu32 " update failed", contact->ID);
                    return false;
                }

                /* Pop "Add contact" window from the stack so that clicking
                 * back button after saving the modified contact returns to
                 * contacts list, not to the "Add contact" window. */
                application->popWindow(gui::window::name::new_contact);

                /* Switch to contact details */
                auto switchData = std::make_unique<PhonebookItemData>(contact, newContactModel->getRequestType());
                switchData->ignoreCurrentWindowOnStack = true;
                application->switchWindow(gui::window::name::contact, std::move(switchData));

                return true;
            }});
        application->switchWindow(gui::window::name::dialog_yes_no, std::move(metaData));
    }



@@ 301,5 333,9 @@ namespace gui
               contactByID->front().primaryName.empty() and contactByID->front().alternativeName.empty() and
               contactByID->front().numbers.empty();
    }
    bool PhonebookNewContact::areUnsavedChanges() const
    {
        return newContactModel->isAnyUnsavedChange(contact);
    }

} // namespace gui

M module-apps/application-phonebook/windows/PhonebookNewContact.hpp => module-apps/application-phonebook/windows/PhonebookNewContact.hpp +2 -0
@@ 38,9 38,11 @@ namespace gui
        void showDialogDuplicatedNumber(const utils::PhoneNumber::View &duplicatedNumber,
                                        const std::uint32_t duplicatedNumberContactID = 0u);
        void showDialogDuplicatedSpeedDialNumber();
        void showDialogUnsavedChanges(std::function<bool()> whereToGoOnYes = nullptr);
        void setSaveButtonVisible(bool visible);
        void showContactDeletedNotification();
        bool checkIfContactWasDeletedDuringEditProcess() const;
        bool areUnsavedChanges() const;

        std::shared_ptr<ContactRecord> contact           = nullptr;
        std::shared_ptr<NewContactModel> newContactModel = nullptr;

M module-gui/gui/widgets/ListItem.hpp => module-gui/gui/widgets/ListItem.hpp +7 -6
@@ 1,4 1,4 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 22,10 22,11 @@ namespace gui
    class ListItemWithCallbacks : public ListItem
    {
      public:
        std::function<bool()> onEmptyCallback                           = nullptr;
        std::function<bool()> onContentChangedCallback                  = nullptr;
        std::function<bool(std::string &errorMessage)> onVerifyCallback = nullptr;
        std::function<void(std::shared_ptr<T> record)> onSaveCallback   = nullptr;
        std::function<void(std::shared_ptr<T> record)> onLoadCallback   = nullptr;
        std::function<bool()> onEmptyCallback                                       = nullptr;
        std::function<bool()> onContentChangedCallback                              = nullptr;
        std::function<bool(std::string &errorMessage)> onVerifyCallback             = nullptr;
        std::function<void(std::shared_ptr<T> record)> onSaveCallback               = nullptr;
        std::function<void(std::shared_ptr<T> record)> onLoadCallback               = nullptr;
        std::function<bool(std::shared_ptr<T> record)> onCheckUnsavedChangeCallback = nullptr;
    };
} /* namespace gui */

M pure_changelog.md => pure_changelog.md +1 -0
@@ 37,6 37,7 @@
* Fixed missing tethering icon on "Tethering is on" window
* Fixed showing "Copy text" option in empty note
* Fixed "Copy" option missing from the options list in "New message" window
* Fixed losing unsaved data on go back

## [1.7.2 2023-07-28]