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 ¤t, 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