Files:
The Messages example is a simple messenger application to list, view, reply to and delete the messages available on the system or composer new ones.
In this example we'll learn how to use the bb::pim::message API of the BB10 framework to work with the messages available on the system.
The application has a clean separation between business logic and UI representation. All the business logic is encapsulated inside the three C++ classes Messages, MessageViewer and MessageComposer. These classes use the bb::pim::message API internally to communicate with the message service of BB10 and provide all the necessary functionality and data to the UI via properties, signals and slots. The Messages object is exported to the UI under the name '_messages'.
The UI of this sample application consists of four pages:
The main page contains a ListView that displays a list of messages and a TextField where the user can type in a text which is used as filter criterion for the list.
// The message list filter input TextField { hintText: qsTr ("Filter by...") onTextChanging: _messages.filter = text }
Whenever the content of the TextField is changed by the user, the 'filter' property of the exported Messages object is updated.
// The list view with all messages ListView { dataModel: _messages.model listItemComponents: ListItemComponent { type: "item" StandardListItem { title: ListItemData.subject description: ListItemData.time } } onTriggered: { clearSelection() select(indexPath) _messages.setCurrentMessage(indexPath) _messages.viewMessage(); navigationPane.push(messageViewer.createObject()) } }
The ListView uses the model provided by the Messages object as data model and shows the subject and time properties inside the items.
Whenever the user clicks on an item, setCurrentMessage() is called on the Messages object, which will mark the selected message as the 'current' message for viewing and editing. Afterwards the viewMessage() method is invoked on the Messages object. This will setup the MessageViewer object to make the data of the current message available to the 'view message' page. Finally, the 'view message' page is pushed on the NavigationPane.
attachedObjects: [ ComponentDefinition { id: messageComposer source: "MessageComposer.qml" }, ComponentDefinition { id: messageViewer source: "MessageViewer.qml" }, RenderFence { raised: true onReached: { _messages.addAccounts(accounts) } } ]
This page is loaded dynamically from a ComponentDefinition that references the file MessageViewer.qml
The main page also contains an ActionItem inside its action bar, which can be invoked by the user to create a new message.
actions: [ ActionItem { title: qsTr ("New") imageSource: "asset:///images/action_composemessage.png" ActionBar.placement: ActionBarPlacement.OnBar onTriggered: { _messages.composeMessage() navigationPane.push(messageComposer.createObject()) } } ]
When the action is triggered, the composeMessage() method is invoked on the Messages object, which will setup the MessageComposer object to be in creation mode. Afterwards the 'compose new message' page is pushed on the NavigationPane. This page is loaded dynamically from a ComponentDefinition that references the file MessageComposer.qml.
The 'view message' page is implemented inside MessageViewer.qml and retrieves all the data to display from the MessageViewer object, which is accessible as a property of the Messages object.
Label { verticalAlignment: VerticalAlignment.Bottom text: _messages.messageViewer.sender textStyle { base: SystemDefaults.TextStyles.TitleText } }
The UI of the page consists of a list of Labels, one for each message property (sender, subject, time and body). Their 'text' properties are bound against the properties provided by the MessageViewer object. So whenever the message that is currently handled by the MessageViewer is changed, the UI will be updated automatically.
actions: [ ActionItem { title: qsTr ("Reply") imageSource: "asset:///images/action_replymessage.png" onTriggered: { _messages.composeReplyMessage() navigationPane.push(messageComposer.createObject()) } }, DeleteActionItem { onTriggered: { _messages.deleteMessage() navigationPane.pop() } } ]
To edit or delete the currently displayed message, the page contains two ActionItems. If the one for deleting the message is triggered, the deleteMessage() method is invoked on the Messages object, which will call the appropriated methods on the bb::pim::message API internally. If the action for editing the message is triggered, the composeReplyMessage() method is invoked on the Messages object, which will setup the MessageComposer object to be in editing mode and make the data of the current message available to the 'reply to message' page. Afterwards the 'reply to message' page is pushed on the NavigationPane.
attachedObjects: [ ComponentDefinition { id: messageComposer source: "MessageComposer.qml" } ]
The 'reply to message' page is loaded dynamically from a ComponentDefinition that references the file MessageComposer.qml.
For composing a new message or replying to an existing one the same UI (MessageComposer.qml) is used. The underlying business object MessageComposer provides the property 'mode' to differ between the CreateMode and ReplyMode.
The page contains two actions in its TitleBar to send the current message or cancel the operation.
titleBar: TitleBar { id: pageTitleBar // The 'Create/Save' action acceptAction: ActionItem { title: qsTr ("Send") onTriggered: { _messages.messageComposer.composeMessage() navigationPane.pop() } } // The 'Cancel' action dismissAction: ActionItem { title: qsTr ("Cancel") onTriggered: navigationPane.pop() } }
An invocation of the 'Send' action will call the composeMessage() method on the MessageComposer object, which will do the right thing internally, depending on the current mode.
If the user selects the dismiss action, the current page is popped from the NavigationPane.
TextField { id: recipientField hintText: qsTr ("test.person@example.org") inputMode: TextFieldInputMode.EmailAddress onTextChanging: _messages.messageComposer.recipient = text }
For each property of a message, the page contains an input field (e.g. a TextField for the recipient). Whenever the user changes the content of the field, the associated property of the MessageComposer object will be updated.
If the UI is in ReplyMode, the content of the input fields is initialized with the values from the MessageComposer object after the UI has been created.
onCreationCompleted: { if (_messages.messageComposer.mode == MessageComposer.ReplyMode) { subjectField.text = _messages.messageComposer.subject recipientField.text = _messages.messageComposer.recipient bodyField.text = _messages.messageComposer.body } }
To have a clean separation between business logic and UI, the business logic is implemented in the three C++ classes Messages, MessageViewer and MessageComposer.
The Messages class is the central point to access the business logic from within the UI. Therefor the object is exported to QML under the name '_messages' inside the main function.
// Load the UI description from main.qml QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(&app); // Make the Messages object available to the UI as context property qml->setContextProperty("_messages", new Messages(&app));
The Messages object provides the list of available messages as a custom property 'model' of type bb::cascades::GroupDataModel, so that a ListView in the UI can use it directly as its data model. Additionally the Messages object provides a 'filter' property to define a filter string that is applied on the list of messages. The other two business logic objects MessageViewer and MessageComposer can be accessed through the 'messageViewer' and 'messageComposer' properties.
class Messages : public QObject { Q_OBJECT // The model that provides the filtered list of messages Q_PROPERTY(bb::cascades::GroupDataModel *model READ model CONSTANT); // The pattern to filter the list of messages Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged); // The viewer object for the current message Q_PROPERTY(MessageViewer* messageViewer READ messageViewer CONSTANT); // The composer object for the current message Q_PROPERTY(MessageComposer* messageComposer READ messageComposer CONSTANT); public: Messages(QObject *parent = 0); /** * A helper method to fill the DropDown with available accounts. */ Q_INVOKABLE void addAccounts(QObject* dropDownObject) const; public Q_SLOTS: /** * Marks the message with the given @p indexPath as current. */ void setCurrentMessage(const QVariantList &indexPath); /** * Selects the account to work with. */ void setSelectedAccount(bb::cascades::Option *selectedOption); /** * Prepares the message composer to compose a new message. */ void composeMessage(); /** * Prepares the message composer to compose a reply to the current message. */ void composeReplyMessage(); /** * Prepares the message viewer to display the current message. */ void viewMessage(); /** * Deletes the current message. */ void deleteMessage(); Q_SIGNALS: // The change notification signal for the property void filterChanged(); private Q_SLOTS: // Filters the messages in the model according to the filter property void filterMessages(); private: // The accessor methods of the properties bb::cascades::GroupDataModel* model() const; QString filter() const; void setFilter(const QString &filter); MessageViewer* messageViewer() const; MessageComposer* messageComposer() const; // The central object to access the message service bb::pim::message::MessageService* m_messageService; // The property values bb::cascades::GroupDataModel* m_model; QString m_filter; // The controller object for viewing a message MessageViewer* m_messageViewer; // The controller object for composing a message MessageComposer* m_messageComposer; // The ID of the current message bb::pim::message::MessageKey m_currentMessageId; // The current account bb::pim::account::Account m_currentAccount; QList<bb::pim::account::Account> m_accountList; };
To use the MessageViewer and MessageComposer objects as property types, they must be registered to the QML type system inside the main function as well.
// Register our custom types with QML, so that they can be used as property types qmlRegisterUncreatableType<MessageComposer>("com.example.bb10samples.pim.messages", 1, 0, "MessageComposer", "Usage as property type and access to enums"); qmlRegisterType<MessageViewer>();
Inside the constructor all member objects are initialized. The MessageService is the central point of the bb::pim::message API to access message information on the BB10 platform.
Messages::Messages(QObject *parent) : QObject(parent) , m_messageService(new MessageService(this)) , m_model(new GroupDataModel(this)) , m_messageViewer(new MessageViewer(m_messageService, this)) , m_messageComposer(new MessageComposer(m_messageService, this)) , m_currentMessageId(-1) { // Disable grouping in data model m_model->setGrouping(ItemGrouping::None); // Ensure to invoke the filterMessages() method whenever a message has been added, changed or removed bool ok = connect(m_messageService, SIGNAL(messagesAdded(bb::pim::account::AccountKey, QList<bb::pim::message::ConversationKey>, QList<bb::pim::message::MessageKey>)), SLOT(filterMessages())); Q_ASSERT(ok); ok = connect(m_messageService, SIGNAL(messageAdded(bb::pim::account::AccountKey, bb::pim::message::ConversationKey, bb::pim::message::MessageKey)), SLOT(filterMessages())); Q_ASSERT(ok); ok = connect(m_messageService, SIGNAL(messageUpdated(bb::pim::account::AccountKey, bb::pim::message::ConversationKey, bb::pim::message::MessageKey, bb::pim::message::MessageUpdate)), SLOT(filterMessages())); Q_ASSERT(ok); ok = connect(m_messageService, SIGNAL(messageRemoved(bb::pim::account::AccountKey, bb::pim::message::ConversationKey, bb::pim::message::MessageKey, QString)), SLOT(filterMessages())); Q_ASSERT(ok); // Initialize the current account if there is any m_accountList = AccountService().accounts(Service::Messages); if(!m_accountList.isEmpty()) m_currentAccount = m_accountList.first(); // Fill the data model with messages initially filterMessages(); }
The filterMessages() method retrieves all messages that match the specified filter from the MessageService and fills the data model with the result. The ID of the message is stored inside the model together with the data that will be displayed in the ListView.
void Messages::filterMessages() { if (!m_currentAccount.isValid()) return; // Use the entered filter string as search value MessageSearchFilter filter; filter.addSearchCriteria(SearchFilterCriteria::Any, m_filter); const QList<Message> messages = m_messageService->searchLocal(m_currentAccount.id(), filter); // Clear the old message information from the model m_model->clear(); // Iterate over the list of message IDs foreach (const Message &message, messages) { // Copy the data into a model entry QVariantMap entry; entry["messageId"] = message.id(); entry["subject"] = message.subject(); entry["time"] = message.serverTimestamp().toString(); // Add the entry to the model m_model->insert(entry); } }
Whenever the user changes the filter criterion, the setFilter() method is invoked, which updates the filter value and calls the filterMessages() method again.
void Messages::setFilter(const QString &filter) { if (m_filter == filter) return; m_filter = filter; emit filterChanged(); // Update the model now that the filter criterion has changed filterMessages(); }
Whenever the user selects a message in the ListView, the setCurrentMessage() method is invoked. If the selected index path is valid, the ID of the message is extracted and stored as 'current' message.
void Messages::setCurrentMessage(const QVariantList &indexPath) { // Extract the ID of the selected message from the model if (indexPath.isEmpty()) { m_currentMessageId = -1; } else { const QVariantMap entry = m_model->data(indexPath).toMap(); m_currentMessageId = entry.value("messageId").toInt(); } }
Afterwards the UI invokes the viewMessage() method, that triggers the MessageViewer to load the data for the current message.
void Messages::viewMessage() { // Prepare the message viewer for displaying the current message m_messageViewer->setMessage(m_currentAccount.id(), m_currentMessageId); }
If the user triggers the 'Delete' action from the 'view message' page, deleteMessage() is invoked, which forwards this request to the MessageService.
void Messages::deleteMessage() { m_messageService->remove(m_currentAccount.id(), m_currentMessageId); }
If the user wants to reply to the current message, the UI calls composeReplyMessage(), which triggers the MessageComposer to load the data of the current message and switches the MessageComposer into ReplyMode.
void Messages::composeReplyMessage() { // Prepare the message composer for composing a reply to the current message m_messageComposer->setAccountId(m_currentAccount.id()); m_messageComposer->loadMessage(m_currentMessageId); m_messageComposer->setMode(MessageComposer::ReplyMode); }
If the user wants to compose a new message, the UI calls composeMessage(), which resets the MessageComposer and switches it into CreateMode.
void Messages::composeMessage() { // Prepare the message composer for composing a new message m_messageComposer->reset(); m_messageComposer->setAccountId(m_currentAccount.id()); m_messageComposer->setMode(MessageComposer::CreateMode); }
The MessageViewer class is an UI-independent representation of the message viewer, that provides all the functionality and data as slots and properties. It encapsulates all the logic of loading a message from the persistent storage, provides its data as properties and updates the properties automatically if the message has changed in the storage backend.
class MessageViewer : public QObject { Q_OBJECT // The data properties of the message that is displayed Q_PROPERTY(QString subject READ subject NOTIFY subjectChanged) Q_PROPERTY(QString sender READ senderContact NOTIFY senderChanged) Q_PROPERTY(QString time READ time NOTIFY timeChanged) Q_PROPERTY(QString body READ body NOTIFY bodyChanged) public: MessageViewer(bb::pim::message::MessageService *service, QObject *parent = 0); // Sets the ID of the message that should be displayed. void setMessage(bb::pim::account::AccountKey accountId, bb::pim::message::MessageKey messageId); Q_SIGNALS: // The change notification signals of the properties void subjectChanged(); void senderChanged(); void timeChanged(); void bodyChanged(); private Q_SLOTS: /** * This slot is invoked whenever the message service reports that a message has been changed. */ void messageUpdated(bb::pim::account::AccountKey, bb::pim::message::ConversationKey, bb::pim::message::MessageKey, const bb::pim::message::MessageUpdate&); private: // The accessor methods of the properties QString subject() const; QString senderContact() const; QString time() const; QString body() const; // Loads the message from the persistent storage and updates the properties void updateMessage(); // The central object to access the message service bb::pim::message::MessageService* m_messageService; // The ID of the message that is displayed bb::pim::message::MessageKey m_messageId; // The ID of the account of the message that is displayed bb::pim::account::AccountKey m_accountId; // The property values QString m_subject; QString m_sender; QDateTime m_time; QString m_body; };
Inside the constructor the messageUpdated() signal of the MessageService is connected against the custom messageUpdated() slot to reload the currently displayed message from the persistent storage if it has been changed by some other entity.
MessageViewer::MessageViewer(MessageService *service, QObject *parent) : QObject(parent) , m_messageService(service) , m_messageId(-1) , m_accountId(-1) { // Ensure to invoke the messageUpdated() method whenever a message has been changed bool ok = connect(m_messageService, SIGNAL(messageUpdated(bb::pim::account::AccountKey, bb::pim::message::ConversationKey, bb::pim::message::MessageKey, bb::pim::message::MessageUpdate)), SLOT(messageUpdated(bb::pim::account::AccountKey, bb::pim::message::ConversationKey, bb::pim::message::MessageKey, bb::pim::message::MessageUpdate))); Q_ASSERT(ok); Q_UNUSED(ok); }
The method setMessage() is invoked by the Messages object to prepare the viewer to display a message in the UI. In this method the passed ID is stored locally and updateMessage() is called afterwards.
void MessageViewer::setMessage(AccountKey accountId, MessageKey messageId) { if (m_accountId != accountId || m_messageId != messageId) { m_accountId = accountId; m_messageId = messageId; // Trigger a refetch of the message for the new ID updateMessage(); } }
Inside updateMessage() the actual message data are loaded from the persistent storage through the MessageService object. If the value of a message property has changed, the change notification signal is emitted.
void MessageViewer::updateMessage() { // Store previous values const QString oldSubject = m_subject; const QString oldSender = m_sender; const QDateTime oldTime = m_time; const QString oldBody = m_body; // Fetch new values from persistent storage const Message message = m_messageService->message(m_accountId, m_messageId); m_subject = message.subject(); m_sender = message.sender().displayableName(); m_time = message.serverTimestamp(); m_body = message.body(MessageBody::PlainText).plainText(); if (m_body.isEmpty()) m_body = message.body(MessageBody::Html).plainText(); // Check whether values have changed if (oldSubject != m_subject) emit subjectChanged(); if (oldSender != m_sender) emit senderChanged(); if (oldTime != m_time) emit timeChanged(); if (oldBody != m_body) emit bodyChanged(); }
The custom slot messageUpdated() checks whether the currently displayed message is in the change set and calls updateMessage() accordingly.
void MessageViewer::messageUpdated(bb::pim::account::AccountKey accountId, bb::pim::message::ConversationKey, bb::pim::message::MessageKey messageId, const bb::pim::message::MessageUpdate&) { /** * Call updateMessage() only if the message we are currently displaying * has been changed. */ if (m_accountId == accountId && m_messageId == messageId) updateMessage(); }
The MessageComposer class is an UI-independent representation of the message composer, that provides all the functionality and data as slots and properties. It encapsulates all the logic of composing a new message or replying to an existing one.
class MessageComposer : public QObject { Q_OBJECT // The data properties of the message that is displayed Q_PROPERTY(QString subject READ subject WRITE setSubject NOTIFY subjectChanged) Q_PROPERTY(QString recipient READ recipient WRITE setRecipient NOTIFY recipientChanged) Q_PROPERTY(QString body READ body WRITE setBody NOTIFY bodyChanged) // Defines whether the composer is in 'create' or 'reply' mode Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged) Q_ENUMS(Mode) public: /** * Describes the mode of the message composer. * The mode information are used to adapt the behavior of the composer and * provide hints to the UI. */ enum Mode { CreateMode, ReplyMode }; MessageComposer(bb::pim::message::MessageService *service, QObject *parent = 0); // Sets the ID of the current account. void setAccountId(bb::pim::account::AccountKey accountId); void setMode(Mode mode); Mode mode() const; public Q_SLOTS: /** * Loads the message with the given ID. */ void loadMessage(bb::pim::message::MessageKey messageId); /** * Composes and sends the reply of the currently loaded message if in 'reply' mode or composes and sends * a new message if in 'create' mode. */ void composeMessage(); /** * Resets all fields of the message composer. */ void reset(); Q_SIGNALS: // The change notification signals of the properties void subjectChanged(); void recipientChanged(); void bodyChanged(); void modeChanged(); private: // The accessor methods of the properties void setSubject(const QString &subject); QString subject() const; void setRecipient(const QString &recipient); QString recipient() const; void setBody(const QString &body); QString body() const; // The central object to access the message service bb::pim::message::MessageService* m_messageService; // The ID of the message that is displayed bb::pim::message::MessageKey m_messageId; // The ID of the account of the message that is displayed bb::pim::account::AccountKey m_accountId; // The property values QString m_subject; QString m_recipient; QString m_body; Mode m_mode; };
Inside the constructor the member variables are initialized with the default values.
MessageComposer::MessageComposer(MessageService *service, QObject *parent) : QObject(parent) , m_messageService(service) , m_messageId(-1) , m_accountId(-1) , m_mode(CreateMode) { }
If the user wants to reply to an existing message, the Messages object invokes loadMessage() to load the message data from the persistent storage and make them available to the UI through the properties.
void MessageComposer::loadMessage(MessageKey messageId) { Q_ASSERT(m_accountId != -1); m_messageId = messageId; // Fetch new values from persistent storage const Message message = m_messageService->message(m_accountId, m_messageId); m_subject = message.subject(); m_recipient = message.sender().address(); m_body = message.body(MessageBody::PlainText).plainText(); if (m_body.isEmpty()) m_body = message.body(MessageBody::Html).plainText(); // Adapt message to be a reply m_subject = QString::fromLatin1("Re: %1").arg(m_subject); m_body.replace("\n", "\n> "); m_body = QString::fromLatin1("> %1").arg(m_body); emit subjectChanged(); emit recipientChanged(); emit bodyChanged(); }
When the user clicks on the 'Send' button in the UI, composeMessage() is invoked. Depending on the current mode, a new message or reply message is created. In both cases the MessageBuilder class is used to add or change the attributes of the Message object.
void MessageComposer::composeMessage() { const MessageContact recipient = MessageContact(-1, MessageContact::To, QString(), m_recipient); const QByteArray bodyData = m_body.toUtf8(); // Create a message builder to create/modify the message MessageBuilder *builder = (m_mode == CreateMode ? MessageBuilder::create(m_accountId) : MessageBuilder::create(m_accountId, m_messageService->message(m_accountId, m_messageId))); builder->subject(m_subject); builder->removeAllRecipients(); builder->addRecipient(recipient); builder->body(MessageBody::PlainText, bodyData); // Send the new message via current account m_messageService->send(m_accountId, *builder); }
If the user wants to compose a new message, the Messages object invokes the reset() method to clear all fields of the MessageComposer.
void MessageComposer::reset() { // Reset all properties m_accountId = -1; m_messageId = -1; m_subject.clear(); m_recipient.clear(); m_body.clear(); // Emit the change notifications emit subjectChanged(); emit recipientChanged(); emit bodyChanged(); }