diff --git a/KTp/Declarative/messages-model.cpp b/KTp/Declarative/messages-model.cpp index 640a050..8c6fa8e 100644 --- a/KTp/Declarative/messages-model.cpp +++ b/KTp/Declarative/messages-model.cpp @@ -1,475 +1,511 @@ /* Copyright (C) 2011 Lasath Fernando Copyright (C) 2013 Lasath Fernando This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "messages-model.h" #include #include "debug.h" #include #include #include #include #include #include #include #include class MessagePrivate { public: MessagePrivate(const KTp::Message &message); KTp::Message message; MessagesModel::DeliveryStatus deliveryStatus; QDateTime deliveryReportReceiveTime; }; MessagePrivate::MessagePrivate(const KTp::Message &message) : message(message), deliveryStatus(MessagesModel::DeliveryStatusUnknown) { } class MessagesModel::MessagesModelPrivate { public: Tp::TextChannelPtr textChannel; Tp::AccountPtr account; ScrollbackManager *logManager; QList messages; // For fast lookup of original messages upon receipt of a message delivery report. QHash messagesByMessageToken; bool visible; bool logsLoaded; }; MessagesModel::MessagesModel(const Tp::AccountPtr &account, QObject *parent) : QAbstractListModel(parent), d(new MessagesModelPrivate) { d->account = account; d->visible = false; d->logManager = new ScrollbackManager(this); d->logsLoaded = false; connect(d->logManager, SIGNAL(fetched(QList)), SLOT(onHistoryFetched(QList))); //Load configuration for number of message to show KConfig config(QLatin1String("ktelepathyrc")); KConfigGroup tabConfig = config.group("Behavior"); d->logManager->setScrollbackLength(tabConfig.readEntry("scrollbackLength", 10)); } QHash MessagesModel::roleNames() const { QHash roles = QAbstractListModel::roleNames(); roles[TextRole] = "text"; roles[TimeRole] = "time"; roles[TypeRole] = "type"; roles[SenderIdRole] = "senderId"; roles[SenderAliasRole] = "senderAlias"; roles[SenderAvatarRole] = "senderAvatar"; roles[DeliveryStatusRole] = "deliveryStatus"; roles[DeliveryReportReceiveTimeRole] = "deliveryReportReceiveTime"; roles[PreviousMessageTypeRole] = "previousMessageType"; roles[NextMessageTypeRole] = "nextMessageType"; return roles; } Tp::TextChannelPtr MessagesModel::textChannel() const { return d->textChannel; } bool MessagesModel::verifyPendingOperation(Tp::PendingOperation *op) { bool operationSucceeded = true; if (op->isError()) { qCWarning(KTP_DECLARATIVE) << op->errorName() << "+" << op->errorMessage(); operationSucceeded = false; } return operationSucceeded; } void MessagesModel::setupChannelSignals(const Tp::TextChannelPtr &channel) { connect(channel.data(), SIGNAL(messageReceived(Tp::ReceivedMessage)), SLOT(onMessageReceived(Tp::ReceivedMessage))); connect(channel.data(), SIGNAL(messageSent(Tp::Message,Tp::MessageSendingFlags,QString)), SLOT(onMessageSent(Tp::Message,Tp::MessageSendingFlags,QString))); connect(channel.data(), SIGNAL(pendingMessageRemoved(Tp::ReceivedMessage)), SLOT(onPendingMessageRemoved())); connect(channel.data(), &Tp::TextChannel::messageReceived, this, &MessagesModel::lastMessageChanged); connect(channel.data(), &Tp::TextChannel::messageSent, this, &MessagesModel::lastMessageChanged); connect(channel.data(), &Tp::TextChannel::pendingMessageRemoved, this, &MessagesModel::lastMessageChanged); } void MessagesModel::setTextChannel(const Tp::TextChannelPtr &channel) { Q_ASSERT(channel != d->textChannel); setupChannelSignals(channel); if (d->textChannel) { removeChannelSignals(d->textChannel); } d->textChannel = channel; d->logManager->setTextChannel(d->account, d->textChannel); //Load messages unless they have already been loaded if(!d->logsLoaded) { d->logManager->fetchScrollback(); } QList messageQueue = channel->messageQueue(); Q_FOREACH(const Tp::ReceivedMessage &message, messageQueue) { bool messageAlreadyInModel = false; Q_FOREACH(const MessagePrivate ¤t, d->messages) { //FIXME: docs say messageToken can return an empty string. What to do if that happens? //Tp::Message has an == operator. maybe I can use that? if (current.message.token() == message.messageToken()) { messageAlreadyInModel = true; break; } } if (!messageAlreadyInModel) { onMessageReceived(message); } } } Tp::AccountPtr MessagesModel::account() const { return d->account; } void MessagesModel::setAccount(const Tp::AccountPtr &account) { d->account = account; } void MessagesModel::onHistoryFetched(const QList &messages) { - if (!messages.isEmpty()) { + QList messagesToAdd; + + // Make sure we're not adding duplicated messages to the model + if (!d->messages.isEmpty()) { + int i = 0; + for (i = 0; i < messages.size(); i++) { + if (messages.at(i) == d->messages.at(0).message) { + break; + } + } + messagesToAdd = messages.mid(0, i); + } else { + messagesToAdd = messages; + } + + if (!messagesToAdd.isEmpty()) { //Add all messages before the ones already present in the channel - beginInsertRows(QModelIndex(), 0, messages.count() - 1); - for(int i=messages.size()-1;i>=0;i--) { - d->messages.prepend(messages[i]); + beginInsertRows(QModelIndex(), 0, messagesToAdd.count() - 1); + for (int i = messagesToAdd.size() - 1; i >= 0; i--) { + d->messages.prepend(messagesToAdd[i]); } endInsertRows(); } d->logsLoaded = true; + // Emit changed for the first message after the prepended + // logs, to make sure the bubble shape is updated + // through PreviousMessageTypeRole + QModelIndex index = createIndex(messagesToAdd.count(), 0); + Q_EMIT dataChanged(index, index); Q_EMIT lastMessageChanged(); } void MessagesModel::onMessageReceived(const Tp::ReceivedMessage &message) { int unreadCount = d->textChannel->messageQueue().size(); if (message.isDeliveryReport()) { d->textChannel->acknowledge(QList() << message); Tp::ReceivedMessage::DeliveryDetails deliveryDetails = message.deliveryDetails(); if(!deliveryDetails.hasOriginalToken()) { qCWarning(KTP_DECLARATIVE) << "Delivery report without original message token received."; // Matching the delivery report to the original message is impossible without the token. return; } QPersistentModelIndex originalMessageIndex = d->messagesByMessageToken.value( deliveryDetails.originalToken()); if (!originalMessageIndex.isValid() || originalMessageIndex.row() >= d->messages.count()) { // The original message for this delivery report was not found. return; } MessagePrivate &originalMessage = d->messages[originalMessageIndex.row()]; originalMessage.deliveryReportReceiveTime = message.received(); switch(deliveryDetails.status()) { case Tp::DeliveryStatusPermanentlyFailed: case Tp::DeliveryStatusTemporarilyFailed: originalMessage.deliveryStatus = DeliveryStatusFailed; if (deliveryDetails.hasDebugMessage()) { qCDebug(KTP_DECLARATIVE) << "Delivery failure debug message:" << deliveryDetails.debugMessage(); } break; case Tp::DeliveryStatusDelivered: originalMessage.deliveryStatus = DeliveryStatusDelivered; break; case Tp::DeliveryStatusRead: originalMessage.deliveryStatus = DeliveryStatusRead; break; default: originalMessage.deliveryStatus = DeliveryStatusUnknown; break; } Q_EMIT dataChanged(originalMessageIndex, originalMessageIndex); } else { int newMessageIndex = 0; const QDateTime sentTimestamp = message.sent(); if (sentTimestamp.isValid()) { for (int i = d->messages.count() - 1; i >= 0; --i) { if (sentTimestamp > d->messages.at(i).message.time()) { newMessageIndex = i; break; } } } else { newMessageIndex = rowCount(); } beginInsertRows(QModelIndex(), newMessageIndex, newMessageIndex); d->messages.insert(newMessageIndex, KTp::MessageProcessor::instance()->processIncomingMessage( message, d->account, d->textChannel)); endInsertRows(); // Update the previous message in the view // This will redraw the part of the bubble to not // be bottom but middle one, if this is a consecutive // message to the previous one if (d->messages.count() > 1) { Q_EMIT dataChanged(createIndex(newMessageIndex - 1, 0), createIndex(newMessageIndex - 1, 0)); } if (d->visible) { acknowledgeAllMessages(); } else { Q_EMIT unreadCountChanged(unreadCount); } } } void MessagesModel::onMessageSent(const Tp::Message &message, Tp::MessageSendingFlags flags, const QString &messageToken) { Q_UNUSED(flags); int newMessageIndex = rowCount(); beginInsertRows(QModelIndex(), newMessageIndex, newMessageIndex); const KTp::Message &newMessage = KTp::MessageProcessor::instance()->processIncomingMessage( message, d->account, d->textChannel); d->messages.append(newMessage); if (!messageToken.isEmpty()) { // Insert the message into the lookup table for delivery reports. const QPersistentModelIndex modelIndex = createIndex(newMessageIndex, 0); d->messagesByMessageToken.insert(messageToken, modelIndex); } endInsertRows(); // Update the previous message in the view // This will redraw the part of the bubble to not // be bottom but middle one, if this is a consecutive // message to the previous one if (d->messages.count() > 1) { Q_EMIT dataChanged(createIndex(newMessageIndex - 1, 0), createIndex(newMessageIndex - 1, 0)); } } void MessagesModel::onPendingMessageRemoved() { Q_EMIT unreadCountChanged(unreadCount()); } QVariant MessagesModel::data(const QModelIndex &index, int role) const { QVariant result; if (index.isValid() && index.row() < rowCount(index.parent())) { const MessagePrivate m = d->messages[index.row()]; switch (role) { case TextRole: result = m.message.finalizedMessage(); break; case TypeRole: if (m.message.type() == Tp::ChannelTextMessageTypeAction) { result = MessageTypeAction; } else { if (m.message.direction() == KTp::Message::LocalToRemote) { result = MessageTypeOutgoing; } else { result = MessageTypeIncoming; } } break; case TimeRole: result = m.message.time(); break; case SenderIdRole: result = m.message.senderId(); break; case SenderAliasRole: result = m.message.senderAlias(); break; case SenderAvatarRole: if (m.message.sender()) { result = QVariant::fromValue(m.message.sender()->avatarPixmap()); } break; case DeliveryStatusRole: result = m.deliveryStatus; break; case DeliveryReportReceiveTimeRole: result = m.deliveryReportReceiveTime; break; case PreviousMessageTypeRole: if (index.row() > 0) { result = data(createIndex(index.row() - 1, 0), TypeRole); } break; case NextMessageTypeRole: if (index.row() < d->messages.size() - 1) { result = data(createIndex(index.row() + 1, 0), TypeRole); } break; }; } else { qWarning() << "Attempting to access data at invalid index (" << index << ")"; } return result; } int MessagesModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return d->messages.size(); } void MessagesModel::sendNewMessage(const QString &message) { if (message.isEmpty()) { qCWarning(KTP_DECLARATIVE) << "Attempting to send empty string, this is not supported"; } else { Tp::PendingOperation *op; QString modifiedMessage = message; if (d->textChannel->supportsMessageType(Tp::ChannelTextMessageTypeAction) && modifiedMessage.startsWith(QLatin1String("/me "))) { //remove "/me " from the start of the message modifiedMessage.remove(0,4); op = d->textChannel->send(modifiedMessage, Tp::ChannelTextMessageTypeAction); } else { op = d->textChannel->send(modifiedMessage); } connect(op, SIGNAL(finished(Tp::PendingOperation*)), SLOT(verifyPendingOperation(Tp::PendingOperation*))); } } void MessagesModel::removeChannelSignals(const Tp::TextChannelPtr &channel) { QObject::disconnect(channel.data(), SIGNAL(messageReceived(Tp::ReceivedMessage)), this, SLOT(onMessageReceived(Tp::ReceivedMessage)) ); QObject::disconnect(channel.data(), SIGNAL(messageSent(Tp::Message,Tp::MessageSendingFlags,QString)), this, SLOT(onMessageSent(Tp::Message,Tp::MessageSendingFlags,QString)) ); } int MessagesModel::unreadCount() const { if (d->textChannel) { return d->textChannel->messageQueue().size(); } return 0; } void MessagesModel::acknowledgeAllMessages() { if (d->textChannel.isNull()) { return; } QList queue = d->textChannel->messageQueue(); d->textChannel->acknowledge(queue); Q_EMIT unreadCountChanged(queue.size()); } void MessagesModel::setVisibleToUser(bool visible) { if (d->visible != visible) { d->visible = visible; Q_EMIT visibleToUserChanged(d->visible); } if (visible) { acknowledgeAllMessages(); } } bool MessagesModel::isVisibleToUser() const { return d->visible; } MessagesModel::~MessagesModel() { delete d; } bool MessagesModel::shouldStartOpened() const { return d->textChannel->isRequested(); } QString MessagesModel::lastMessage() const { const QModelIndex index = createIndex(rowCount() - 1, 0); if (!index.isValid()) { return QString(); } return data(index, MessagesModel::TextRole).toString().simplified(); } QDateTime MessagesModel::lastMessageDateTime() const { const QModelIndex index = createIndex(rowCount() - 1, 0); if (!index.isValid()) { return QDateTime(); } return data(index, MessagesModel::TimeRole).toDateTime(); } + +void MessagesModel::fetchMoreHistory() +{ + if (d->messages.isEmpty() || !d->logsLoaded) { + return; + } + + d->logsLoaded = false; + + const KTp::Message message = d->messages.at(0).message; + + const QString token = message.token().isEmpty() ? message.time().toString(Qt::ISODate) + message.mainMessagePart() + : message.token(); + d->logManager->setScrollbackLength(10); + d->logManager->fetchHistory(rowCount() + 10, token); +} diff --git a/KTp/Declarative/messages-model.h b/KTp/Declarative/messages-model.h index cb6893c..e079a2d 100644 --- a/KTp/Declarative/messages-model.h +++ b/KTp/Declarative/messages-model.h @@ -1,114 +1,116 @@ /* Copyright (C) 2011 Lasath Fernando This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef MESSAGES_MODEL_H #define MESSAGES_MODEL_H #include #include #include #include class MessagesModel : public QAbstractListModel { Q_OBJECT Q_ENUMS(MessageType) Q_ENUMS(DeliveryStatus) Q_PROPERTY(bool visibleToUser READ isVisibleToUser WRITE setVisibleToUser NOTIFY visibleToUserChanged) Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadCountChanged) Q_PROPERTY(bool shouldStartOpened READ shouldStartOpened CONSTANT) public: MessagesModel(const Tp::AccountPtr &account, QObject *parent = 0); ~MessagesModel() override; enum Roles { TextRole = Qt::UserRole, //String TypeRole, //MessagesModel::MessageType (for now!) TimeRole, //QDateTime SenderIdRole, //string SenderAliasRole, //string SenderAvatarRole, //pixmap DeliveryStatusRole, //MessagesModel::DeliveryStatus DeliveryReportReceiveTimeRole, //QDateTime PreviousMessageTypeRole, // allows for painting the messages as grouped NextMessageTypeRole, }; enum MessageType { MessageTypeIncoming, MessageTypeOutgoing, MessageTypeAction, MessageTypeNotice }; enum DeliveryStatus { DeliveryStatusUnknown, DeliveryStatusDelivered, DeliveryStatusRead, // implies DeliveryStatusDelivered DeliveryStatusFailed }; QHash roleNames() const Q_DECL_OVERRIDE; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; Tp::TextChannelPtr textChannel() const; void setTextChannel(const Tp::TextChannelPtr &channel); Tp::AccountPtr account() const; void setAccount(const Tp::AccountPtr &account); bool isVisibleToUser() const; void setVisibleToUser(bool visible); int unreadCount() const; void acknowledgeAllMessages(); bool shouldStartOpened() const; QString lastMessage() const; QDateTime lastMessageDateTime() const; + Q_SIGNALS: void visibleToUserChanged(bool visible); void unreadCountChanged(int unreadMesssagesCount); void lastMessageChanged(); public Q_SLOTS: + void fetchMoreHistory(); void sendNewMessage(const QString &message); private Q_SLOTS: void onMessageReceived(const Tp::ReceivedMessage &message); void onMessageSent(const Tp::Message &message, Tp::MessageSendingFlags flags, const QString &messageToken); void onPendingMessageRemoved(); bool verifyPendingOperation(Tp::PendingOperation *op); void onHistoryFetched(const QList &messages); private: void setupChannelSignals(const Tp::TextChannelPtr &channel); void removeChannelSignals(const Tp::TextChannelPtr &channel); class MessagesModelPrivate; MessagesModelPrivate *d; }; #endif // CONVERSATION_MODEL_H