// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved. // For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md #include "ApplicationMessages.hpp" #include "MessagesStyle.hpp" #include "NewMessage.hpp" #include "SMSdata.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace gui { class NewMessageWindow::MessageMemento { public: void setState(const gui::Text *_state) { assert(_state); state = std::make_unique(_state->backupText()); } void restoreState(gui::Text *_state) { assert(_state); if (state == nullptr) { return; } const auto currentText = _state->getText(); _state->restoreFrom(*state); if (!currentText.empty()) { _state->addText(currentText); } state.reset(); } private: std::unique_ptr state; }; std::unique_ptr NewMessageWindow::memento = std::make_unique(); NewMessageWindow::NewMessageWindow(app::ApplicationCommon *app) : AppWindow(app, name::window::new_sms), app::AsyncCallbackReceiver{app} { buildInterface(); } bool NewMessageWindow::onInput(const InputEvent &inputEvent) { if (!inputEvent.isShortRelease(KeyCode::KEY_RF) || !shouldCurrentAppBeIgnoredOnSwitchBack()) { return AppWindow::onInput(inputEvent); } return app::manager::Controller::switchBack( application, std::make_unique(nameOfPreviousApplication.value(), nullptr, true)); } void NewMessageWindow::onBeforeShow(ShowMode mode, SwitchData *data) { memento->restoreState(message); if (data == nullptr) { return; } if (auto searchRequest = dynamic_cast(data); searchRequest != nullptr) { LOG_INFO("Received search results"); saveInfoAboutPreviousAppForProperSwitchBack(data); contact = searchRequest->result; selectedNumber = searchRequest->selectedNumber; recipient->setText(contact->getFormattedName()); } else if (auto textData = dynamic_cast(data); textData != nullptr) { const auto &text = textData->text; LOG_INFO("Received sms text data"); if (textData->concatenate == SMSTextData::Concatenate::True) { message->addText(text); } else { message->setText(text); } setFocusItem(message); } else if (auto sendRequest = dynamic_cast(data); sendRequest != nullptr) { LOG_INFO("Received sms send request"); saveInfoAboutPreviousAppForProperSwitchBack(data); phoneNumber = sendRequest->getPhoneNumber(); contact = DBServiceAPI::MatchContactByPhoneNumber(application, phoneNumber); if (!contact || contact->isTemporary()) { recipient->setText(phoneNumber.getFormatted()); } else { recipient->setText(contact->getFormattedName()); } message->setText(sendRequest->textData); } updateNavBar(); } bool NewMessageWindow::selectContact() { // select contact only if there is no entered number if (recipient->getText().empty()) { return app::manager::Controller::sendAction(application, app::manager::actions::ShowContacts, std::make_unique(), app::manager::OnSwitchBehaviour::RunInBackground); } return true; } bool NewMessageWindow::sendSms() { auto app = dynamic_cast(application); assert(app != nullptr); const auto &number = getPhoneNumber(); if (const auto success = app->sendSms(number.getView(), message->getText()); !success) { LOG_ERROR("Failed to send the SMS."); return false; } setFocusItem(nullptr); // to make sure it is not possible to send text message multiple times return switchToThreadWindow(number.getView()); } auto NewMessageWindow::getPhoneNumber() const -> utils::PhoneNumber { if (phoneNumber.getEntered().empty()) { if (contact && !contact->numbers.empty()) { return contact->numbers.at(selectedNumber).number; } return utils::PhoneNumber{recipient->getText()}; } return phoneNumber; } bool NewMessageWindow::switchToThreadWindow(const utils::PhoneNumber::View &number) { auto query = std::make_unique(number); auto task = app::AsyncQuery::createFromQuery(std::move(query), db::Interface::Name::SMSThread); task->setCallback([this](auto response) { const auto result = dynamic_cast(response); if (result == nullptr) { return false; } // clear data only when message is sent contact = nullptr; phoneNumber.clear(); recipient->clear(); message->clear(); setFocusItem(body); const auto &thread = result->getThread(); auto switchData = std::make_unique(std::make_unique(thread)); switchData->ignoreCurrentWindowOnStack = true; switchData->nameOfSenderApplication = nameOfPreviousApplication; application->switchWindow(gui::name::window::thread_view, std::move(switchData)); return true; }); task->execute(application, this); return true; } void NewMessageWindow::updateNavBar() { if (getFocusItem() == recipient) { navBar->setActive(nav_bar::Side::Left, false); if (recipient->getText().empty()) { navBar->setText(nav_bar::Side::Center, utils::translate(style::strings::common::contacts)); return; } navBar->setActive(nav_bar::Side::Center, false); } } void NewMessageWindow::buildInterface() { namespace msgStyle = style::messages::newMessage; AppWindow::buildInterface(); navBar->setText(nav_bar::Side::Left, utils::translate(style::strings::common::options)); navBar->setText(nav_bar::Side::Right, utils::translate(style::strings::common::back)); setTitle(utils::translate("sms_title_message")); const uint32_t w = this->getWidth() - style::window::default_left_margin - style::window::default_right_margin; const uint32_t h = this->getHeight() - style::window::default_vertical_pos - navBar->getHeight(); body = new gui::VBox(this, style::window::default_left_margin, style::window::default_vertical_pos, w, h); auto recipientLabel = new Label(body, 0, 0, body->getWidth(), msgStyle::recipientLabel::h); recipientLabel->setText(utils::translate("sms_add_rec_num")); recipientLabel->activeItem = false; recipientLabel->setEdges(gui::RectangleEdge::None); recipientLabel->setFont(style::window::font::small); recipientLabel->setAlignment(Alignment(gui::Alignment::Horizontal::Left, gui::Alignment::Vertical::Bottom)); auto recipientHBox = new gui::HBox(body, 0, 0, body->getWidth(), msgStyle::text::h); recipientHBox->setAlignment(gui::Alignment::Vertical::Center); recipientHBox->setEdges(gui::RectangleEdge::Bottom); recipientHBox->setPenFocusWidth(style::window::default_border_focus_w); recipientHBox->setPenWidth(style::window::default_border_rect_no_focus); recipient = new gui::Text( recipientHBox, 0, 0, body->getWidth() - msgStyle::recipientImg::w, msgStyle::text::h, ExpandMode::None); recipient->setEdges(gui::RectangleEdge::None); recipient->setInputMode(new InputMode({InputMode::phone})); recipient->setFont(style::window::font::mediumbold); recipient->setAlignment(Alignment(gui::Alignment::Horizontal::Left, gui::Alignment::Vertical::Center)); recipient->activatedCallback = [=](Item &) -> bool { return selectContact(); }; recipient->focusChangedCallback = [=](Item &) -> bool { updateNavBar(); return true; }; recipient->inputCallback = [this]([[maybe_unused]] Item &, const InputEvent &inputEvent) -> bool { if (contact != nullptr) { if (inputEvent.isShortRelease(KeyCode::KEY_PND)) { recipient->clear(); return true; } if (inputEvent.isDigit()) { return true; } } return false; }; recipient->setTextChangedCallback([this]([[maybe_unused]] Item &item, const UTF8 &text) { if (recipient->getText().empty()) { contact = nullptr; phoneNumber.clear(); } updateNavBar(); }); auto img = new gui::Image(recipientHBox, 0, 0, 0, 0, "phonebook_32px_W_G"); img->activeItem = false; auto labelMessage = new Label(body, 0, 0, body->getWidth(), msgStyle::messageLabel::h); labelMessage->setText(utils::translate("app_messages_message")); labelMessage->activeItem = false; labelMessage->setEdges(gui::RectangleEdge::None); labelMessage->setFont(style::window::font::small); labelMessage->setAlignment(Alignment(gui::Alignment::Horizontal::Left, gui::Alignment::Vertical::Bottom)); message = new gui::Text(nullptr, 0, 0, body->getWidth(), msgStyle::text::h, ExpandMode::Up); message->setMaximumSize(body->getWidth(), msgStyle::text::maxH); message->setTextLimitType(gui::TextLimitType::MaxSignsCount, msgConstants::maxConcatenatedLen); message->setEdges(gui::RectangleEdge::Bottom); message->setInputMode(new InputMode( {InputMode::Abc, InputMode::ABC, InputMode::abc, InputMode::digit}, [=](const UTF8 &text) { navBarTemporaryMode(text); }, [=]() { navBarRestoreFromTemporaryMode(); }, [=]() { selectSpecialCharacter(); })); message->setPenFocusWidth(style::window::default_border_focus_w); 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 { if (recipient->getText().empty() || message->getText().empty()) { return false; } if (!sendSms()) { LOG_ERROR("sendSms failed"); } return true; }; message->focusChangedCallback = [=](Item &) -> bool { if (recipient->getText().empty()) { navBar->setActive(nav_bar::Side::Center, false); } else { navBar->setText(nav_bar::Side::Center, utils::translate(style::strings::common::send)); } navBar->setActive(nav_bar::Side::Left, true); return true; }; message->inputCallback = [=](Item &, const InputEvent &event) { if (event.isShortRelease(KeyCode::KEY_LF)) { auto app = dynamic_cast(application); assert(app != nullptr); memento->setState(message); return app->newMessageOptions(getName(), message); } if (event.isShortRelease(KeyCode::KEY_RF)) { onClose(CloseReason::WindowSwitch); } return false; }; body->addWidget(message); body->addWidget(new Span(Axis::Y, body->getHeight())); message->setText(""); // to set actual size of Text body->setEdges(gui::RectangleEdge::None); body->setVisible(true); body->setNavigation(); setFocusItem(body); } void NewMessageWindow::onClose(CloseReason reason) { if (message->getText().empty()) { // Nothing to do if text is empty. return; } if (reason != CloseReason::ApplicationClose) { memento->setState(message); message->clear(); return; } if (const auto handled = handleMessageText(); !handled) { message->clear(); } } auto NewMessageWindow::handleMessageText() -> bool { if (const auto &number = getPhoneNumber(); !number.get().empty()) { return addDraft(number); } return false; } auto NewMessageWindow::addDraft(const utils::PhoneNumber &number) -> bool { auto query = std::make_unique(number.getView()); auto task = app::AsyncQuery::createFromQuery(std::move(query), db::Interface::Name::SMSThread); task->setCallback([this, number](auto response) { const auto result = dynamic_cast(response); if (result == nullptr) { return false; } const auto &thread = result->getThread(); if (!thread.isValid()) { storeMessageDraft(number.getView(), message->getText()); return true; } return addDraftToExistingThread(thread.ID, number.getView(), message->getText()); }); task->execute(application, this, std::nullopt, app::ReceiverBehavior::WaitForResponseToClose); return true; } auto NewMessageWindow::addDraftToExistingThread(unsigned int threadId, const utils::PhoneNumber::View &number, const UTF8 &text) -> bool { auto query = std::make_unique(threadId); auto task = app::AsyncQuery::createFromQuery(std::move(query), db::Interface::Name::SMS); task->setCallback([this, number](auto response) { const auto result = dynamic_cast(response); if (result == nullptr) { return false; } auto lastSms = result->record; if (lastSms.has_value() && lastSms->type == SMSType::DRAFT) { storeMessageDraft(lastSms.value(), message->getText()); return true; } storeMessageDraft(number, message->getText()); return true; }); task->execute(application, this, std::nullopt, app::ReceiverBehavior::WaitForResponseToClose); return true; } void NewMessageWindow::storeMessageDraft(const utils::PhoneNumber::View &number, const UTF8 &text) { auto app = dynamic_cast(application); assert(app != nullptr); app->createDraft(number, text); message->clear(); } void NewMessageWindow::storeMessageDraft(SMSRecord &sms, const UTF8 &text) { auto app = dynamic_cast(application); assert(app != nullptr); app->updateDraft(sms, text); message->clear(); } } // namespace gui