1 /*
2     Copyright (C) 2011  Lasath Fernando <kde@lasath.org>
3     Copyright (C) 2013  Lasath Fernando <davidedmundson@kde.org>
4 
5     This library is free software; you can redistribute it and/or
6     modify it under the terms of the GNU Lesser General Public
7     License as published by the Free Software Foundation; either
8     version 2.1 of the License, or (at your option) any later version.
9 
10     This library is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13     Lesser General Public License for more details.
14 
15     You should have received a copy of the GNU Lesser General Public
16     License along with this library; if not, write to the Free Software
17     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18 */
19 
20 
21 #include "messages-model.h"
22 
23 #include <QPixmap>
24 
25 #include "debug.h"
26 #include <KLocalizedString>
27 #include <KConfig>
28 
29 #include <TelepathyQt/ReceivedMessage>
30 #include <TelepathyQt/TextChannel>
31 #include <TelepathyQt/Account>
32 
33 #include <KTp/message-processor.h>
34 #include <KTp/message-context.h>
35 #include <KTp/Logger/scrollback-manager.h>
36 
37 class MessagePrivate
38 {
39   public:
40     MessagePrivate(const KTp::Message &message);
41 
42     KTp::Message message;
43     MessagesModel::DeliveryStatus deliveryStatus;
44     QDateTime deliveryReportReceiveTime;
45 };
46 
MessagePrivate(const KTp::Message & message)47 MessagePrivate::MessagePrivate(const KTp::Message &message) :
48         message(message),
49         deliveryStatus(MessagesModel::DeliveryStatusUnknown)
50 {
51 }
52 
53 class MessagesModel::MessagesModelPrivate
54 {
55   public:
56     Tp::TextChannelPtr textChannel;
57     Tp::AccountPtr account;
58     ScrollbackManager *logManager;
59     QList<MessagePrivate> messages;
60     // For fast lookup of original messages upon receipt of a message delivery report.
61     QHash<QString /*messageToken*/, QPersistentModelIndex> messagesByMessageToken;
62     bool visible;
63     bool logsLoaded;
64 };
65 
MessagesModel(const Tp::AccountPtr & account,QObject * parent)66 MessagesModel::MessagesModel(const Tp::AccountPtr &account, QObject *parent) :
67         QAbstractListModel(parent),
68         d(new MessagesModelPrivate)
69 {
70     d->account = account;
71     d->visible = false;
72 
73     d->logManager = new ScrollbackManager(this);
74     d->logsLoaded = false;
75     connect(d->logManager, SIGNAL(fetched(QList<KTp::Message>)), SLOT(onHistoryFetched(QList<KTp::Message>)));
76 
77     //Load configuration for number of message to show
78     KConfig config(QLatin1String("ktelepathyrc"));
79     KConfigGroup tabConfig = config.group("Behavior");
80     d->logManager->setScrollbackLength(tabConfig.readEntry<int>("scrollbackLength", 20));
81 }
82 
roleNames() const83 QHash<int, QByteArray> MessagesModel::roleNames() const
84 {
85     QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
86     roles[TextRole] = "text";
87     roles[TimeRole] = "time";
88     roles[TypeRole] = "type";
89     roles[SenderIdRole] = "senderId";
90     roles[SenderAliasRole] = "senderAlias";
91     roles[SenderAvatarRole] = "senderAvatar";
92     roles[DeliveryStatusRole] = "deliveryStatus";
93     roles[DeliveryReportReceiveTimeRole] = "deliveryReportReceiveTime";
94     roles[PreviousMessageTypeRole] = "previousMessageType";
95     roles[NextMessageTypeRole] = "nextMessageType";
96     return roles;
97 }
98 
textChannel() const99 Tp::TextChannelPtr MessagesModel::textChannel() const
100 {
101     return d->textChannel;
102 }
103 
verifyPendingOperation(Tp::PendingOperation * op)104 bool MessagesModel::verifyPendingOperation(Tp::PendingOperation *op)
105 {
106     bool operationSucceeded = true;
107 
108     if (op->isError()) {
109         qCWarning(KTP_DECLARATIVE) << op->errorName() << "+" << op->errorMessage();
110         operationSucceeded = false;
111     }
112 
113     return operationSucceeded;
114 }
115 
setupChannelSignals(const Tp::TextChannelPtr & channel)116 void MessagesModel::setupChannelSignals(const Tp::TextChannelPtr &channel)
117 {
118     connect(channel.data(),
119             SIGNAL(messageReceived(Tp::ReceivedMessage)),
120             SLOT(onMessageReceived(Tp::ReceivedMessage)));
121     connect(channel.data(),
122             SIGNAL(messageSent(Tp::Message,Tp::MessageSendingFlags,QString)),
123             SLOT(onMessageSent(Tp::Message,Tp::MessageSendingFlags,QString)));
124     connect(channel.data(),
125             SIGNAL(pendingMessageRemoved(Tp::ReceivedMessage)),
126             SLOT(onPendingMessageRemoved()));
127 
128     connect(channel.data(), &Tp::TextChannel::messageReceived,
129             this, &MessagesModel::lastMessageChanged);
130     connect(channel.data(), &Tp::TextChannel::messageSent,
131             this, &MessagesModel::lastMessageChanged);
132     connect(channel.data(), &Tp::TextChannel::pendingMessageRemoved,
133             this, &MessagesModel::lastMessageChanged);
134 }
135 
setTextChannel(const Tp::TextChannelPtr & channel)136 void MessagesModel::setTextChannel(const Tp::TextChannelPtr &channel)
137 {
138     Q_ASSERT(channel != d->textChannel);
139     setupChannelSignals(channel);
140 
141     if (d->textChannel) {
142         removeChannelSignals(d->textChannel);
143     }
144 
145     d->textChannel = channel;
146 
147     d->logManager->setTextChannel(d->account, d->textChannel);
148 
149     //Load messages unless they have already been loaded
150     if(!d->logsLoaded) {
151         d->logManager->fetchScrollback();
152     }
153 
154     QList<Tp::ReceivedMessage> messageQueue = channel->messageQueue();
155     Q_FOREACH(const Tp::ReceivedMessage &message, messageQueue) {
156         bool messageAlreadyInModel = false;
157         Q_FOREACH(const MessagePrivate &current, d->messages) {
158             //FIXME: docs say messageToken can return an empty string. What to do if that happens?
159             //Tp::Message has an == operator. maybe I can use that?
160             if (current.message.token() == message.messageToken()) {
161                 messageAlreadyInModel = true;
162                 break;
163             }
164         }
165         if (!messageAlreadyInModel) {
166             onMessageReceived(message);
167         }
168     }
169 }
170 
account() const171 Tp::AccountPtr MessagesModel::account() const
172 {
173     return d->account;
174 }
175 
setAccount(const Tp::AccountPtr & account)176 void MessagesModel::setAccount(const Tp::AccountPtr &account)
177 {
178     d->account = account;
179 }
180 
setContactData(const QString & contactId,const QString & contactAlias)181 void MessagesModel::setContactData(const QString &contactId, const QString &contactAlias)
182 {
183     d->logManager->setAccountAndContact(d->account, contactId, contactAlias);
184 
185     //Load messages unless they have already been loaded
186     if (!d->logsLoaded) {
187         qDebug() << "Fetching scrollback";
188         d->logManager->fetchScrollback();
189     }
190 }
191 
onHistoryFetched(const QList<KTp::Message> & messages)192 void MessagesModel::onHistoryFetched(const QList<KTp::Message> &messages)
193 {
194     QList<KTp::Message> messagesToAdd;
195 
196     // Make sure we're not adding duplicated messages to the model
197     if (!d->messages.isEmpty()) {
198         int i = 0;
199         for (i = 0; i < messages.size(); i++) {
200             if (messages.at(i) == d->messages.at(0).message) {
201                 break;
202             }
203         }
204         messagesToAdd = messages.mid(0, i);
205     } else {
206         messagesToAdd = messages;
207     }
208 
209     if (!messagesToAdd.isEmpty()) {
210         //Add all messages before the ones already present in the channel
211         beginInsertRows(QModelIndex(), 0, messagesToAdd.count() - 1);
212         for (int i = messagesToAdd.size() - 1; i >= 0; i--) {
213             d->messages.prepend(messagesToAdd[i]);
214         }
215         endInsertRows();
216     }
217     d->logsLoaded = true;
218 
219     // Emit changed for the first message after the prepended
220     // logs, to make sure the bubble shape is updated
221     // through PreviousMessageTypeRole
222     QModelIndex index = createIndex(messagesToAdd.count(), 0);
223     Q_EMIT dataChanged(index, index);
224     Q_EMIT lastMessageChanged();
225 }
226 
onMessageReceived(const Tp::ReceivedMessage & message)227 void MessagesModel::onMessageReceived(const Tp::ReceivedMessage &message)
228 {
229     int unreadCount = d->textChannel->messageQueue().size();
230     if (message.isDeliveryReport()) {
231         d->textChannel->acknowledge(QList<Tp::ReceivedMessage>() << message);
232 
233         Tp::ReceivedMessage::DeliveryDetails deliveryDetails = message.deliveryDetails();
234         if(!deliveryDetails.hasOriginalToken()) {
235             qCWarning(KTP_DECLARATIVE) << "Delivery report without original message token received.";
236             // Matching the delivery report to the original message is impossible without the token.
237             return;
238         }
239 
240         QPersistentModelIndex originalMessageIndex = d->messagesByMessageToken.value(
241                     deliveryDetails.originalToken());
242         if (!originalMessageIndex.isValid() || originalMessageIndex.row() >= d->messages.count()) {
243             // The original message for this delivery report was not found.
244             return;
245         }
246 
247         MessagePrivate &originalMessage = d->messages[originalMessageIndex.row()];
248         originalMessage.deliveryReportReceiveTime = message.received();
249         switch(deliveryDetails.status()) {
250             case Tp::DeliveryStatusPermanentlyFailed:
251             case Tp::DeliveryStatusTemporarilyFailed:
252                 originalMessage.deliveryStatus = DeliveryStatusFailed;
253                 if (deliveryDetails.hasDebugMessage()) {
254                     qCDebug(KTP_DECLARATIVE) << "Delivery failure debug message:" << deliveryDetails.debugMessage();
255                 }
256                 break;
257             case Tp::DeliveryStatusDelivered:
258                 originalMessage.deliveryStatus = DeliveryStatusDelivered;
259                 break;
260             case Tp::DeliveryStatusRead:
261                 originalMessage.deliveryStatus = DeliveryStatusRead;
262                 break;
263             default:
264                 originalMessage.deliveryStatus = DeliveryStatusUnknown;
265                 break;
266         }
267         Q_EMIT dataChanged(originalMessageIndex, originalMessageIndex);
268     } else {
269         int newMessageIndex = 0;
270         const QDateTime sentTimestamp = message.sent();
271         if (sentTimestamp.isValid()) {
272             for (int i = d->messages.count() - 1; i >= 0; --i) {
273                 if (sentTimestamp > d->messages.at(i).message.time()) {
274                     newMessageIndex = i;
275                     break;
276                 }
277             }
278         } else {
279             newMessageIndex = rowCount();
280         }
281         beginInsertRows(QModelIndex(), newMessageIndex, newMessageIndex);
282 
283         d->messages.insert(newMessageIndex, KTp::MessageProcessor::instance()->processIncomingMessage(
284                                message, d->account, d->textChannel));
285 
286         endInsertRows();
287 
288         // Update the previous message in the view
289         // This will redraw the part of the bubble to not
290         // be bottom but middle one, if this is a consecutive
291         // message to the previous one
292         if (d->messages.count() > 1) {
293             Q_EMIT dataChanged(createIndex(newMessageIndex - 1, 0), createIndex(newMessageIndex - 1, 0));
294         }
295 
296         if (d->visible) {
297             acknowledgeAllMessages();
298         } else {
299             Q_EMIT unreadCountChanged(unreadCount);
300         }
301     }
302 }
303 
onMessageSent(const Tp::Message & message,Tp::MessageSendingFlags flags,const QString & messageToken)304 void MessagesModel::onMessageSent(const Tp::Message &message, Tp::MessageSendingFlags flags, const QString &messageToken)
305 {
306     Q_UNUSED(flags);
307 
308     int newMessageIndex = rowCount();
309     beginInsertRows(QModelIndex(), newMessageIndex, newMessageIndex);
310 
311     const KTp::Message &newMessage = KTp::MessageProcessor::instance()->processIncomingMessage(
312                 message, d->account, d->textChannel);
313     d->messages.append(newMessage);
314 
315     if (!messageToken.isEmpty()) {
316         // Insert the message into the lookup table for delivery reports.
317         const QPersistentModelIndex modelIndex = createIndex(newMessageIndex, 0);
318         d->messagesByMessageToken.insert(messageToken, modelIndex);
319     }
320 
321     endInsertRows();
322 
323     // Update the previous message in the view
324     // This will redraw the part of the bubble to not
325     // be bottom but middle one, if this is a consecutive
326     // message to the previous one
327     if (d->messages.count() > 1) {
328         Q_EMIT dataChanged(createIndex(newMessageIndex - 1, 0), createIndex(newMessageIndex - 1, 0));
329     }
330 }
331 
onPendingMessageRemoved()332 void MessagesModel::onPendingMessageRemoved()
333 {
334     Q_EMIT unreadCountChanged(unreadCount());
335 }
336 
data(const QModelIndex & index,int role) const337 QVariant MessagesModel::data(const QModelIndex &index, int role) const
338 {
339     QVariant result;
340 
341     if (index.isValid() && index.row() < rowCount(index.parent())) {
342         const MessagePrivate m = d->messages[index.row()];
343 
344         switch (role) {
345         case TextRole:
346             result = m.message.finalizedMessage();
347             break;
348         case TypeRole:
349             if (m.message.type() == Tp::ChannelTextMessageTypeAction) {
350                 result = MessageTypeAction;
351             } else {
352                 if (m.message.direction() == KTp::Message::LocalToRemote) {
353                     result = MessageTypeOutgoing;
354                 } else {
355                     result = MessageTypeIncoming;
356                 }
357             }
358             break;
359         case TimeRole:
360             result = m.message.time();
361             break;
362         case SenderIdRole:
363             result = m.message.senderId();
364             break;
365         case SenderAliasRole:
366             result = m.message.senderAlias();
367             break;
368         case SenderAvatarRole:
369             if (m.message.sender()) {
370                 result = QVariant::fromValue(m.message.sender()->avatarPixmap());
371             }
372             break;
373         case DeliveryStatusRole:
374             result = m.deliveryStatus;
375             break;
376         case DeliveryReportReceiveTimeRole:
377             result = m.deliveryReportReceiveTime;
378             break;
379         case PreviousMessageTypeRole:
380             if (index.row() > 0) {
381                 result = data(createIndex(index.row() - 1, 0), TypeRole);
382             }
383             break;
384         case NextMessageTypeRole:
385             if (index.row() < d->messages.size() - 1) {
386                 result = data(createIndex(index.row() + 1, 0), TypeRole);
387             }
388             break;
389         };
390     } else {
391         qWarning() << "Attempting to access data at invalid index (" << index << ")";
392     }
393 
394     return result;
395 }
396 
rowCount(const QModelIndex & parent) const397 int MessagesModel::rowCount(const QModelIndex &parent) const
398 {
399     Q_UNUSED(parent);
400     return d->messages.size();
401 }
402 
sendNewMessage(const QString & message)403 void MessagesModel::sendNewMessage(const QString &message)
404 {
405     if (message.isEmpty()) {
406         qCWarning(KTP_DECLARATIVE) << "Attempting to send empty string, this is not supported";
407     } else {
408         if (!d->textChannel) {
409             qWarning(KTP_DECLARATIVE) << "Attempting to send a message without a channel, returning";
410             return;
411         }
412 
413         Tp::PendingOperation *op;
414         QString modifiedMessage = message;
415         if (d->textChannel->supportsMessageType(Tp::ChannelTextMessageTypeAction)
416                 && modifiedMessage.startsWith(QLatin1String("/me "))) {
417             //remove "/me " from the start of the message
418             modifiedMessage.remove(0,4);
419             op = d->textChannel->send(modifiedMessage, Tp::ChannelTextMessageTypeAction);
420         } else {
421             op = d->textChannel->send(modifiedMessage);
422         }
423         connect(op,
424                 SIGNAL(finished(Tp::PendingOperation*)),
425                 SLOT(verifyPendingOperation(Tp::PendingOperation*)));
426     }
427 }
428 
removeChannelSignals(const Tp::TextChannelPtr & channel)429 void MessagesModel::removeChannelSignals(const Tp::TextChannelPtr &channel)
430 {
431     QObject::disconnect(channel.data(),
432                         SIGNAL(messageReceived(Tp::ReceivedMessage)),
433                         this,
434                         SLOT(onMessageReceived(Tp::ReceivedMessage))
435                        );
436     QObject::disconnect(channel.data(),
437                         SIGNAL(messageSent(Tp::Message,Tp::MessageSendingFlags,QString)),
438                         this,
439                         SLOT(onMessageSent(Tp::Message,Tp::MessageSendingFlags,QString))
440                        );
441 }
442 
unreadCount() const443 int MessagesModel::unreadCount() const
444 {
445     if (d->textChannel) {
446         return d->textChannel->messageQueue().size();
447     }
448 
449     return 0;
450 }
451 
acknowledgeAllMessages()452 void MessagesModel::acknowledgeAllMessages()
453 {
454     if (d->textChannel.isNull()) {
455         return;
456     }
457 
458     QList<Tp::ReceivedMessage> queue = d->textChannel->messageQueue();
459 
460     d->textChannel->acknowledge(queue);
461     Q_EMIT unreadCountChanged(queue.size());
462 }
463 
setVisibleToUser(bool visible)464 void MessagesModel::setVisibleToUser(bool visible)
465 {
466     if (d->visible != visible) {
467         d->visible = visible;
468         Q_EMIT visibleToUserChanged(d->visible);
469     }
470 
471     if (visible) {
472         acknowledgeAllMessages();
473     }
474 }
475 
isVisibleToUser() const476 bool MessagesModel::isVisibleToUser() const
477 {
478     return d->visible;
479 }
480 
~MessagesModel()481 MessagesModel::~MessagesModel()
482 {
483     delete d;
484 }
485 
shouldStartOpened() const486 bool MessagesModel::shouldStartOpened() const
487 {
488     return d->textChannel->isRequested();
489 }
490 
lastMessage() const491 QString MessagesModel::lastMessage() const
492 {
493     const QModelIndex index = createIndex(rowCount() - 1, 0);
494 
495     if (!index.isValid()) {
496         return QString();
497     }
498 
499     return data(index, MessagesModel::TextRole).toString().simplified();
500 }
501 
lastMessageDateTime() const502 QDateTime MessagesModel::lastMessageDateTime() const
503 {
504     const QModelIndex index = createIndex(rowCount() - 1, 0);
505 
506     if (!index.isValid()) {
507         return QDateTime();
508     }
509 
510     return data(index, MessagesModel::TimeRole).toDateTime();
511 }
512 
fetchMoreHistory()513 void MessagesModel::fetchMoreHistory()
514 {
515     if (d->messages.isEmpty() || !d->logsLoaded) {
516         return;
517     }
518 
519     d->logsLoaded = false;
520 
521     const KTp::Message message = d->messages.at(0).message;
522 
523     const QString token = message.token().isEmpty() ? message.time().toString(Qt::ISODate) + message.mainMessagePart()
524                                                     : message.token();
525     d->logManager->setScrollbackLength(10);
526     d->logManager->fetchHistory(rowCount() + 10, token);
527 }
528