1 /***************************************************************************
2  *   Copyright (C) 2010 by David Edmundson <kde@davidedmundson.co.uk>      *
3  *   Copyright (C) 2014 by Marcin Ziemiński <zieminn@gmail.com>            *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program 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         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA            *
19  ***************************************************************************/
20 
21 #include "chat-widget.h"
22 
23 #include "ui_chat-widget.h"
24 #include "adium-theme-header-info.h"
25 #include "adium-theme-content-info.h"
26 #include "adium-theme-message-info.h"
27 #include "adium-theme-status-info.h"
28 #include "channel-contact-model.h"
29 #include "notify-filter.h"
30 #include "text-chat-config.h"
31 #include "contact-delegate.h"
32 #include "authenticationwizard.h"
33 #include "otr-notifications.h"
34 #include "ktp-debug.h"
35 
36 #include <QKeyEvent>
37 #include <QAction>
38 #include <QMenu>
39 #include <QSortFilterProxyModel>
40 #include <QMimeType>
41 #include <QMimeDatabase>
42 #include <QLineEdit>
43 #include <QColorDialog>
44 #include <QTemporaryFile>
45 #include <QFileDialog>
46 
47 #include <KNotification>
48 #include <KAboutData>
49 #include <KColorScheme>
50 #include <KMessageWidget>
51 #include <KMessageBox>
52 #include <KIconLoader>
53 #include <KLocalizedString>
54 
55 #include <TelepathyQt/Account>
56 #include <TelepathyQt/Message>
57 #include <TelepathyQt/Types>
58 #include <TelepathyQt/AvatarData>
59 #include <TelepathyQt/Connection>
60 #include <TelepathyQt/Presence>
61 #include <TelepathyQt/PendingChannelRequest>
62 #include <TelepathyQt/OutgoingFileTransferChannel>
63 
64 #include <KTp/presence.h>
65 #include <KTp/actions.h>
66 #include <KTp/message-processor.h>
67 #include <KTp/Logger/scrollback-manager.h>
68 #include <KTp/Widgets/contact-info-dialog.h>
69 #include <KTp/OTR/channel-adapter.h>
70 #include <KTp/OTR/utils.h>
71 
72 #include <sonnet/speller.h>
73 
74 Q_DECLARE_METATYPE(Tp::ContactPtr)
75 
76 const QString groupChatOnlineIcon(QLatin1String("im-irc"));
77 // FIXME We should have a proper icon for this
78 const QString groupChatOfflineIcon(QLatin1String("im-irc"));
79 
80 class ChatWidgetPrivate
81 {
82 public:
ChatWidgetPrivate(const Tp::TextChannelPtr & textChannel)83     ChatWidgetPrivate(const Tp::TextChannelPtr &textChannel) :
84         remoteContactChatState(Tp::ChannelChatStateInactive),
85         isGroupChat(false),
86         channel(new KTp::ChannelAdapter(textChannel)),
87         contactsMenu(0),
88         fileResourceTransferMenu(0),
89         fileTransferMenuAction(0),
90         shareImageMenuAction(0),
91         messageWidgetSwitchOnlineAction(0),
92         logsLoaded(false),
93         exchangedMessagesCount(0),
94         hasNewOTRstatus(false)
95     {
96     }
97     /** Stores whether the channel is ready with all contacts upgraded*/
98     bool chatViewInitialized;
99     Tp::ChannelChatState remoteContactChatState;
100     bool isGroupChat;
101     QString title;
102     QString contactName;
103     QString yourName;
104     QString currentKeyboardLayoutLanguage;
105     KTp::ChannelAdapterPtr channel;
106     Tp::AccountPtr account;
107     ShareProvider *shareProvider;
108     Ui::ChatWidget ui;
109     ChannelContactModel *contactModel;
110     QMenu *contactsMenu;
111     QMenu *fileResourceTransferMenu;
112     // Used with imageShareMenu
113     QAction *fileTransferMenuAction;
114     QAction *shareImageMenuAction;
115     QString fileToTransferPath;
116     QAction *messageWidgetSwitchOnlineAction;
117     ScrollbackManager *logManager;
118     QTimer *pausedStateTimer;
119     bool logsLoaded;
120     uint exchangedMessagesCount;
121     bool hasNewOTRstatus;
122 
123     QList< Tp::OutgoingFileTransferChannelPtr > tmpFileTransfers;
124 
125     static QString telepathyComponentName();
126     KTp::AbstractMessageFilter *notifyFilter;
127 };
128 
129 
130 //FIXME I would like this to be part of the main KDE Telepathy library as a static function somewhere.
telepathyComponentName()131 QString ChatWidgetPrivate::telepathyComponentName()
132 {
133     return QStringLiteral("ktelepathy");
134 }
135 
ChatWidget(const Tp::TextChannelPtr & channel,const Tp::AccountPtr & account,QWidget * parent)136 ChatWidget::ChatWidget(const Tp::TextChannelPtr & channel, const Tp::AccountPtr &account, QWidget *parent)
137     : QWidget(parent),
138       d(new ChatWidgetPrivate(channel))
139 {
140     d->account = account;
141     d->logManager = new ScrollbackManager(this);
142     connect(d->logManager, SIGNAL(fetched(QList<KTp::Message>)), SLOT(onHistoryFetched(QList<KTp::Message>)));
143 
144     connect(d->account.data(), SIGNAL(currentPresenceChanged(Tp::Presence)),
145             this, SLOT(currentPresenceChanged(Tp::Presence)));
146 
147     ShareProvider::ShareService serviceType = static_cast<ShareProvider::ShareService>(TextChatConfig::instance()->imageShareServiceType());
148     d->shareProvider = new ShareProvider(serviceType, this);
149     connect(d->shareProvider, SIGNAL(finishedSuccess(ShareProvider*,QString)), this, SLOT(onShareProviderFinishedSuccess(ShareProvider*,QString)));
150     connect(d->shareProvider, SIGNAL(finishedError(ShareProvider*,QString)), this, SLOT(onShareProviderFinishedFailure(ShareProvider*,QString)));
151 
152     d->chatViewInitialized = false;
153     d->isGroupChat = (channel->targetHandleType() == Tp::HandleTypeContact ? false : true);
154 
155     d->ui.setupUi(this);
156     if (d->isGroupChat) {
157         d->contactsMenu = new QMenu(this);
158         QAction *action = d->contactsMenu->addAction(QIcon::fromTheme(QLatin1String("text-x-generic")),
159                                    i18n("Open chat window"),
160                                    this, SLOT(onOpenContactChatWindowClicked()));
161         action->setObjectName(QLatin1String("OpenChatWindowAction"));
162         action = d->contactsMenu->addAction(QIcon::fromTheme(QLatin1String("mail-attachment")),
163                                             i18n("Send file"),
164                                             this, SLOT(onSendFileClicked()));
165         action->setObjectName(QLatin1String("SendFileAction"));
166         d->contactsMenu->addSeparator();
167         d->contactsMenu->addAction(i18n("Show info..."),
168                                    this, SLOT(onShowContactDetailsClicked()));
169 
170         d->ui.contactsView->setContextMenuPolicy(Qt::CustomContextMenu);
171         d->ui.contactsView->setItemDelegate(new ContactDelegate(this));
172 
173         connect(d->ui.contactsView, SIGNAL(customContextMenuRequested(QPoint)),
174                 this, SLOT(onContactsViewContextMenuRequested(QPoint)));
175     }
176 
177     KTp::ContactPtr targetContact = KTp::ContactPtr::qObjectCast(d->channel->textChannel()->targetContact());
178 
179     d->fileResourceTransferMenu = new QMenu(this);
180     // This action's text is going to be changed in the dropEvent method to add the destination image service.
181     d->shareImageMenuAction = new QAction(QIcon::fromTheme(QLatin1String("insert-image")), i18n("Share Image"), this);
182     connect(d->shareImageMenuAction, SIGNAL(triggered(bool)), this, SLOT(onShareImageMenuActionTriggered()));
183     d->fileTransferMenuAction = new QAction(QIcon::fromTheme(QLatin1String("mail-attachment")), i18n("Send File"), this);
184 
185     d->fileTransferMenuAction->setEnabled(targetContact && targetContact->fileTransferCapability());
186     d->fileResourceTransferMenu->addAction(d->fileTransferMenuAction);
187     connect(d->fileTransferMenuAction, SIGNAL(triggered(bool)), this, SLOT(onFileTransferMenuActionTriggered()));
188 
189     // connect channel signals
190     setupChannelSignals();
191 
192     // create contactModel and start keeping track of contacts.
193     d->contactModel = new ChannelContactModel(d->channel->textChannel(), this);
194     setupContactModelSignals();
195 
196     /* Enable nick completion only in group chats */
197     if (d->isGroupChat) {
198         d->ui.sendMessageBox->setContactModel(d->contactModel);
199     }
200 
201     d->ui.messageWidget->setText(i18n("Your message cannot be sent because the account %1 is offline. Please try again when the account is connected again.", d->account->displayName()));
202     d->ui.messageWidget->setMessageType(KMessageWidget::Warning);
203     d->ui.messageWidget->setCloseButtonVisible(true);
204     d->ui.messageWidget->setWordWrap(true);
205     // Hide for the first time
206     d->ui.messageWidget->hide();
207     d->messageWidgetSwitchOnlineAction = new QAction(i18n("Connect %1", d->account->displayName()), d->ui.messageWidget);
208     connect(d->messageWidgetSwitchOnlineAction, SIGNAL(triggered(bool)), d->ui.messageWidget, SLOT(animatedHide()));
209     connect(d->messageWidgetSwitchOnlineAction, SIGNAL(triggered(bool)), this, SLOT(onMessageWidgetSwitchOnlineActionTriggered()));
210 
211     QSortFilterProxyModel *sortModel = new QSortFilterProxyModel(this);
212     sortModel->setSourceModel(d->contactModel);
213     sortModel->setSortRole(Qt::DisplayRole);
214     sortModel->setSortCaseSensitivity(Qt::CaseInsensitive);
215     sortModel->setSortLocaleAware(true);
216     sortModel->setDynamicSortFilter(true);
217     sortModel->sort(0);
218 
219     d->ui.contactsView->setModel(sortModel);
220 
221     d->yourName = channel->groupSelfContact()->alias();
222 
223     d->ui.sendMessageBox->setAcceptDrops(false);
224     d->ui.chatArea->setAcceptDrops(false);
225     setAcceptDrops(true);
226 
227     /* Prepare the chat area */
228     connect(d->ui.chatArea, SIGNAL(zoomFactorChanged(qreal)), SIGNAL(zoomFactorChanged(qreal)));
229     connect(d->ui.chatArea, SIGNAL(textPasted()), d->ui.sendMessageBox, SLOT(pasteSelection()));
230     initChatArea();
231 
232     d->pausedStateTimer = new QTimer(this);
233     d->pausedStateTimer->setSingleShot(true);
234 
235     // Spellchecking set up will trigger textChanged() signal of d->ui.sendMessageBox
236     // and our handler checks state of the timer created above.
237     loadSpellCheckingOption();
238 
239     // make clicking in the main HTML area put focus in the input text box
240     d->ui.chatArea->setFocusProxy(d->ui.sendMessageBox);
241     //make activating the tab select the text area
242     setFocusProxy(d->ui.sendMessageBox);
243 
244     connect(d->ui.sendMessageBox, SIGNAL(returnKeyPressed()), SLOT(sendMessage()));
245 
246     connect(d->ui.searchBar, &ChatSearchBar::findTextSignal, this, &ChatWidget::findTextInChat);
247     connect(d->ui.searchBar, &ChatSearchBar::findNextSignal, this, &ChatWidget::findNextTextInChat);
248     connect(d->ui.searchBar, &ChatSearchBar::findPreviousSignal, this, &ChatWidget::findPreviousTextInChat);
249     connect(d->ui.searchBar, &ChatSearchBar::flagsChangedSignal, this, &ChatWidget::findTextInChat);
250 
251     connect(this, SIGNAL(searchTextComplete(bool)), d->ui.searchBar, SLOT(onSearchTextComplete(bool)));
252 
253     connect(d->pausedStateTimer, SIGNAL(timeout()), this, SLOT(onChatPausedTimerExpired()));
254 
255     // initialize LogManager
256     KConfig config(QLatin1String("ktelepathyrc"));
257     KConfigGroup tabConfig = config.group("Behavior");
258     d->logManager->setScrollbackLength(tabConfig.readEntry<int>("scrollbackLength", 4));
259     d->logManager->setTextChannel(d->account, d->channel->textChannel());
260     m_previousConversationAvailable = d->logManager->exists();
261 
262     d->notifyFilter = new NotifyFilter(this);
263 
264     // setup new otr channel and connect to signals
265     if(d->channel->isOTRsuppored()) {
266         setupOTR();
267     }
268 }
269 
~ChatWidget()270 ChatWidget::~ChatWidget()
271 {
272     saveSpellCheckingOption();
273     delete d;
274 }
275 
changeEvent(QEvent * e)276 void ChatWidget::changeEvent(QEvent *e)
277 {
278     QWidget::changeEvent(e);
279     switch (e->type()) {
280     case QEvent::LanguageChange:
281         d->ui.retranslateUi(this);
282         break;
283     default:
284         break;
285     }
286 }
287 
resizeEvent(QResizeEvent * e)288 void ChatWidget::resizeEvent(QResizeEvent *e)
289 {
290     //set the maximum height of a text box to a third of the total window height (but no smaller than the minimum size)
291     int textBoxHeight = e->size().height() / 3;
292     if (textBoxHeight < d->ui.sendMessageBox->minimumSizeHint().height()) {
293         textBoxHeight = d->ui.sendMessageBox->minimumSizeHint().height();
294     }
295     d->ui.sendMessageBox->setMaximumHeight(textBoxHeight);
296     QWidget::resizeEvent(e);
297 }
298 
account() const299 Tp::AccountPtr ChatWidget::account() const
300 {
301     return d->account;
302 }
303 
icon() const304 QIcon ChatWidget::icon() const
305 {
306     if (!d->isGroupChat) {
307         if (d->account->currentPresence() != Tp::Presence::offline()) {
308             //normal chat - self and one other person.
309             //find the other contact which isn't self.
310             Tp::ContactPtr otherContact = d->channel->textChannel()->targetContact();
311             QIcon presenceIcon = KTp::Presence(otherContact->presence()).icon();
312 
313             if (otherContact->clientTypes().contains(QLatin1String("phone"))) {
314                 //we paint a warning symbol in the right-bottom corner
315                 QPixmap phonePixmap = KIconLoader::global()->loadIcon(QLatin1String("phone"), KIconLoader::NoGroup, 16);
316                 QPixmap pixmap = presenceIcon.pixmap(32, 32);
317                 QPainter painter(&pixmap);
318                 painter.drawPixmap(8, 8, 24, 24, phonePixmap);
319                 return QIcon(pixmap);
320             }
321             return presenceIcon;
322         } else {
323             return KTp::Presence(Tp::Presence::offline()).icon();
324         }
325     } else {
326         //group chat
327         if (d->account->currentPresence() != Tp::Presence::offline()) {
328             return QIcon::fromTheme(groupChatOnlineIcon);
329         } else {
330             return QIcon::fromTheme(groupChatOfflineIcon);
331         }
332     }
333 }
334 
accountIcon() const335 QIcon ChatWidget::accountIcon() const
336 {
337     return QIcon::fromTheme(d->account->iconName());
338 }
339 
isGroupChat() const340 bool ChatWidget::isGroupChat() const
341 {
342     return d->isGroupChat;
343 }
344 
chatSearchBar() const345 ChatSearchBar *ChatWidget::chatSearchBar() const
346 {
347     return d->ui.searchBar;
348 }
349 
setChatEnabled(bool enable)350 void ChatWidget::setChatEnabled(bool enable)
351 {
352     d->ui.contactsView->setEnabled(enable);
353     Q_EMIT iconChanged(icon());
354 }
355 
setTextChannel(const Tp::TextChannelPtr & newTextChannelPtr)356 void ChatWidget::setTextChannel(const Tp::TextChannelPtr &newTextChannelPtr)
357 {
358 
359     d->channel.reset();
360     d->channel = KTp::ChannelAdapterPtr(new KTp::ChannelAdapter(newTextChannelPtr));
361     d->contactModel->setTextChannel(newTextChannelPtr);
362 
363     // connect signals for the new textchannel
364     setupChannelSignals();
365     if(d->channel->isOTRsuppored()) {
366         setupOTR();
367     }
368 
369     //if the UI is ready process any messages in queue
370     if (d->chatViewInitialized) {
371         Q_FOREACH (const Tp::ReceivedMessage &message, d->channel->messageQueue()) {
372             handleIncomingMessage(message, true);
373         }
374     }
375     setChatEnabled(true);
376     onContactPresenceChange(
377             d->channel->textChannel()->groupSelfContact(),
378             KTp::Presence(d->channel->textChannel()->groupSelfContact()->presence()));
379 }
380 
textChannel() const381 Tp::TextChannelPtr ChatWidget::textChannel() const
382 {
383     return d->channel->textChannel();
384 }
385 
keyPressEvent(QKeyEvent * e)386 void ChatWidget::keyPressEvent(QKeyEvent *e)
387 {
388     if (e->matches(QKeySequence::Copy)) {
389         d->ui.chatArea->triggerPageAction(QWebEnginePage::Copy);
390         return;
391     }
392 
393     if (e->key() == Qt::Key_PageUp ||
394         e->key() == Qt::Key_PageDown) {
395         d->ui.chatArea->event(e);
396         return;
397     }
398 
399     QWidget::keyPressEvent(e);
400 }
401 
temporaryFileTransferStateChanged(Tp::FileTransferState state,Tp::FileTransferStateChangeReason reason)402 void ChatWidget::temporaryFileTransferStateChanged(Tp::FileTransferState state, Tp::FileTransferStateChangeReason reason)
403 {
404     Q_UNUSED(reason);
405 
406     if ((state == Tp::FileTransferStateCompleted) || (state == Tp::FileTransferStateCancelled)) {
407         Tp::OutgoingFileTransferChannel *channel = qobject_cast<Tp::OutgoingFileTransferChannel*>(sender());
408         Q_ASSERT(channel);
409 
410         QString localFile = QUrl(channel->uri()).toLocalFile();
411         if (QFile::exists(localFile)) {
412             QFile::remove(localFile);
413             qCDebug(KTP_TEXTUI_LIB) << "File" << localFile << "removed";
414         }
415 
416         d->tmpFileTransfers.removeAll(Tp::OutgoingFileTransferChannelPtr(channel));
417     }
418 }
419 
420 
temporaryFileTransferChannelCreated(Tp::PendingOperation * operation)421 void ChatWidget::temporaryFileTransferChannelCreated(Tp::PendingOperation *operation)
422 {
423     Tp::PendingChannelRequest *request = qobject_cast<Tp::PendingChannelRequest*>(operation);
424     Q_ASSERT(request);
425 
426     Tp::OutgoingFileTransferChannelPtr transferChannel;
427     transferChannel = Tp::OutgoingFileTransferChannelPtr::qObjectCast<Tp::Channel>(request->channelRequest()->channel());
428     Q_ASSERT(!transferChannel.isNull());
429 
430     /* Make sure the pointer lives until the transfer is over
431      * so that the signal connection below lasts until the end */
432     d->tmpFileTransfers << transferChannel;
433 
434     connect(transferChannel.data(), SIGNAL(stateChanged(Tp::FileTransferState,Tp::FileTransferStateChangeReason)),
435             this, SLOT(temporaryFileTransferStateChanged(Tp::FileTransferState,Tp::FileTransferStateChangeReason)));
436 }
437 
438 
dropEvent(QDropEvent * e)439 void ChatWidget::dropEvent(QDropEvent *e)
440 {
441     const QMimeData *data = e->mimeData();
442     ShareProvider::ShareService shareServiceType = TextChatConfig::instance()->imageShareServiceType();
443     d->shareProvider->setShareServiceType(shareServiceType);
444 
445     d->shareImageMenuAction->setText(i18n("Share Image via %1", ShareProvider::availableShareServices().key(shareServiceType)));
446     d->fileResourceTransferMenu->clear();
447 
448     if (data->hasUrls()) {
449         Q_FOREACH(const QUrl &url, data->urls()) {
450             if (url.isLocalFile()) {
451 		 // Not sure if this the best way to determine the MIME type of the file
452         QMimeDatabase db;
453         QString mime = db.mimeTypeForUrl(url).name();
454 		 if (mime.startsWith(QLatin1String("image/"))) {
455 		    d->fileTransferMenuAction->setText(i18n("Send Image via File Transfer"));
456 		    d->fileResourceTransferMenu->addAction(d->shareImageMenuAction);
457 		    d->fileResourceTransferMenu->addAction(d->fileTransferMenuAction);
458 		 } else {
459 		   QFileInfo fileInfo(url.toLocalFile());
460 		   d->fileTransferMenuAction->setText(i18n("Send File"));
461 		   d->fileResourceTransferMenu->addAction(d->fileTransferMenuAction);
462 		 }
463 		 d->fileToTransferPath = url.toLocalFile();
464 		 d->fileResourceTransferMenu->popup(mapToGlobal(e->pos()));
465             } else {
466                 d->ui.sendMessageBox->append(url.toString());
467             }
468         }
469         e->acceptProposedAction();
470     } else if (data->hasText()) {
471         d->ui.sendMessageBox->append(data->text());
472         e->acceptProposedAction();
473     } else if (data->hasHtml()) {
474         d->ui.sendMessageBox->insertHtml(data->html());
475         e->acceptProposedAction();
476     } else if (data->hasImage()) {
477         QImage image = qvariant_cast<QImage>(data->imageData());
478 
479         QTemporaryFile tmpFile(d->account->displayName() + QStringLiteral("-XXXXXX.png"));
480         tmpFile.setAutoRemove(false);
481         if (!tmpFile.open()) {
482             return;
483         }
484         tmpFile.close();
485 
486         if (!image.save(tmpFile.fileName(), "PNG")) {
487             return;
488         }
489 
490 	d->fileToTransferPath = tmpFile.fileName();
491 	d->fileResourceTransferMenu->popup(mapToGlobal(e->pos()));
492 
493         qCDebug(KTP_TEXTUI_LIB) << "Starting Uploading of" << tmpFile.fileName();
494         e->acceptProposedAction();
495     }
496 
497     QWidget::dropEvent(e);
498 }
499 
dragEnterEvent(QDragEnterEvent * e)500 void ChatWidget::dragEnterEvent(QDragEnterEvent *e)
501 {
502     if (e->mimeData()->hasHtml() || e->mimeData()->hasImage() ||
503         e->mimeData()->hasText() || e->mimeData()->hasUrls()) {
504             e->accept();
505     }
506 
507     QWidget::dragEnterEvent(e);
508 }
509 
title() const510 QString ChatWidget::title() const
511 {
512     return d->title;
513 }
514 
titleColor() const515 QColor ChatWidget::titleColor() const
516 {
517     /*return a color to set the tab text as in order of importance
518     typing
519     unread messages
520     user offline
521 
522     */
523 
524     KColorScheme scheme(QPalette::Active, KColorScheme::Window);
525 
526     if (TextChatConfig::instance()->showOthersTyping() && (d->remoteContactChatState == Tp::ChannelChatStateComposing)) {
527         qCDebug(KTP_TEXTUI_LIB) << "remote is typing";
528         return scheme.foreground(KColorScheme::PositiveText).color();
529     }
530 
531     if (unreadMessageCount() > 0 && !isOnTop()) {
532         qCDebug(KTP_TEXTUI_LIB) << "unread messages";
533         return scheme.foreground(KColorScheme::ActiveText).color();
534     }
535 
536     //normal chat - self and one other person.
537     if (!d->isGroupChat) {
538         //find the other contact which isn't self.
539         Q_FOREACH(const Tp::ContactPtr & contact, d->channel->textChannel()->groupContacts()) {
540             if (contact != d->channel->textChannel()->groupSelfContact()) {
541                 if (contact->presence().type() == Tp::ConnectionPresenceTypeOffline ||
542                     contact->presence().type() == Tp::ConnectionPresenceTypeHidden) {
543                     return scheme.foreground(KColorScheme::InactiveText).color();
544                 }
545             }
546         }
547     }
548 
549     return scheme.foreground(KColorScheme::NormalText).color();
550 }
551 
toggleSearchBar() const552 void ChatWidget::toggleSearchBar() const
553 {
554     if(d->ui.searchBar->isVisible()) {
555         d->ui.searchBar->toggleView(false);
556     } else {
557         d->ui.searchBar->toggleView(true);
558     }
559 }
560 
setupChannelSignals()561 void ChatWidget::setupChannelSignals()
562 {
563     connect(d->channel.data(), SIGNAL(messageReceived(Tp::ReceivedMessage)),
564             SLOT(handleIncomingMessage(Tp::ReceivedMessage)));
565     connect(d->channel.data(), SIGNAL(pendingMessageRemoved(Tp::ReceivedMessage)),
566             SIGNAL(unreadMessagesChanged()));
567     connect(d->channel.data(), SIGNAL(messageSent(Tp::Message,Tp::MessageSendingFlags,QString)),
568             SLOT(handleMessageSent(Tp::Message,Tp::MessageSendingFlags,QString)));
569     connect(d->channel->textChannel().data(), SIGNAL(chatStateChanged(Tp::ContactPtr,Tp::ChannelChatState)),
570             SLOT(onChatStatusChanged(Tp::ContactPtr,Tp::ChannelChatState)));
571     connect(d->channel->textChannel().data(), SIGNAL(invalidated(Tp::DBusProxy*,QString,QString)),
572             this, SLOT(onChannelInvalidated()));
573     connect(d->channel->textChannel().data(), SIGNAL(groupMembersChanged(Tp::Contacts,
574                                                           Tp::Contacts,
575                                                           Tp::Contacts,
576                                                           Tp::Contacts,
577                                                           Tp::Channel::GroupMemberChangeDetails)),
578             this, SLOT(onParticipantsChanged(Tp::Contacts,
579                                              Tp::Contacts,
580                                              Tp::Contacts,
581                                              Tp::Contacts,
582                                              Tp::Channel::GroupMemberChangeDetails)));
583 
584     if (d->channel->textChannel()->hasChatStateInterface()) {
585         connect(d->ui.sendMessageBox, SIGNAL(textChanged()), SLOT(onInputBoxChanged()));
586     }
587 }
588 
setupContactModelSignals()589 void ChatWidget::setupContactModelSignals()
590 {
591     connect(d->contactModel, SIGNAL(contactPresenceChanged(Tp::ContactPtr,KTp::Presence)),
592             SLOT(onContactPresenceChange(Tp::ContactPtr,KTp::Presence)));
593     connect(d->contactModel, SIGNAL(contactAliasChanged(Tp::ContactPtr,QString)),
594             SLOT(onContactAliasChanged(Tp::ContactPtr,QString)));
595     connect(d->contactModel, SIGNAL(contactBlockStatusChanged(Tp::ContactPtr,bool)),
596        SLOT(onContactBlockStatusChanged(Tp::ContactPtr,bool)));
597     connect(d->contactModel,SIGNAL(contactClientTypesChanged(Tp::ContactPtr,QStringList)),
598             SLOT(onContactClientTypesChanged(Tp::ContactPtr,QStringList)));
599 }
600 
onHistoryFetched(const QList<KTp::Message> & messages)601 void ChatWidget::onHistoryFetched(const QList<KTp::Message> &messages)
602 {
603     d->chatViewInitialized = true;
604 
605     qCDebug(KTP_TEXTUI_LIB) << "found" << messages.count() << "messages in history";
606     if (!messages.isEmpty()) {
607         QDate date = messages.first().time().date();
608         Q_FOREACH(const KTp::Message &message, messages) {
609             if (message.time().date() != date) {
610                 date = message.time().date();
611                 d->ui.chatArea->addStatusMessage(date.toString(Qt::LocaleDate));
612             }
613 
614             d->ui.chatArea->addMessage(message);
615         }
616 
617         if (date != QDate::currentDate()) {
618             d->ui.chatArea->addStatusMessage(QDate::currentDate().toString(Qt::LocaleDate));
619         }
620     }
621 
622     //process any messages we've 'missed' whilst initialising.
623     Q_FOREACH(const Tp::ReceivedMessage &message, d->channel->messageQueue()) {
624         handleIncomingMessage(message, true);
625     }
626 }
627 
unreadMessageCount() const628 int ChatWidget::unreadMessageCount() const
629 {
630     return d->channel->messageQueue().size() + (d->hasNewOTRstatus ? 1 : 0);
631 }
632 
acknowledgeMessages()633 void ChatWidget::acknowledgeMessages()
634 {
635     qCDebug(KTP_TEXTUI_LIB);
636     //if we're not initialised we can't have shown anything, even if we are on top, therefore ignore all requests to do so
637     if (d->chatViewInitialized) {
638         //acknowledge everything in the message queue.
639         d->channel->acknowledge(d->channel->messageQueue());
640     }
641     if(d->hasNewOTRstatus) {
642         d->hasNewOTRstatus = false;
643         Q_EMIT unreadMessagesChanged();
644     }
645 }
646 
updateSendMessageShortcuts(const QList<QKeySequence> & shortcuts)647 void ChatWidget::updateSendMessageShortcuts(const QList<QKeySequence> &shortcuts)
648 {
649     d->ui.sendMessageBox->setSendMessageShortcuts(shortcuts);
650 }
651 
isOnTop() const652 bool ChatWidget::isOnTop() const
653 {
654     qCDebug(KTP_TEXTUI_LIB) << ( isActiveWindow() && isVisible() );
655     return ( isActiveWindow() && isVisible() );
656 }
657 
otrStatus() const658 OtrStatus ChatWidget::otrStatus() const
659 {
660     if(d->channel->isOTRsuppored()) {
661         return OtrStatus(d->channel->otrTrustLevel());
662     } else {
663         return OtrStatus();
664     }
665 }
666 
blockTextInput(bool block)667 void ChatWidget::blockTextInput(bool block)
668 {
669     if(block) {
670         d->ui.sendMessageBox->setDisabled(true);
671     } else {
672         d->ui.sendMessageBox->setEnabled(true);
673     }
674 }
675 
startOtrSession()676 void ChatWidget::startOtrSession()
677 {
678     if(!d->channel->isOTRsuppored()) return;
679     if(!d->channel->isValid()) {
680         d->ui.messageWidget->removeAction(d->messageWidgetSwitchOnlineAction);
681         if (d->account->requestedPresence().type() == Tp::ConnectionPresenceTypeOffline) {
682             d->ui.messageWidget->addAction(d->messageWidgetSwitchOnlineAction);
683         }
684         d->ui.messageWidget->animatedShow();
685         return;
686     }
687 
688     d->channel->initializeOTR();
689     if(d->channel->otrTrustLevel() == KTp::OTRTrustLevelNotPrivate) {
690         d->ui.chatArea->addStatusMessage(i18n("Attempting to start a private OTR session with %1", d->contactName));
691     }
692     else {
693         d->ui.chatArea->addStatusMessage(i18n("Attempting to restart a private OTR session with %1", d->contactName));
694     }
695 }
696 
stopOtrSession()697 void ChatWidget::stopOtrSession()
698 {
699     qCDebug(KTP_TEXTUI_LIB);
700     if(!d->channel->isOTRsuppored() || d->channel->otrTrustLevel() == KTp::OTRTrustLevelNotPrivate) {
701         return;
702     }
703     if(!d->channel->isValid()) {
704         d->ui.messageWidget->removeAction(d->messageWidgetSwitchOnlineAction);
705         if (d->account->requestedPresence().type() == Tp::ConnectionPresenceTypeOffline) {
706             d->ui.messageWidget->addAction(d->messageWidgetSwitchOnlineAction);
707         }
708         d->ui.messageWidget->animatedShow();
709         return;
710     }
711 
712     d->channel->stopOTR();
713     d->ui.chatArea->addStatusMessage(i18n("Terminating OTR session"));
714 }
715 
authenticateBuddy()716 void ChatWidget::authenticateBuddy()
717 {
718     if(!d->channel->isOTRsuppored()) return;
719 
720     AuthenticationWizard *wizard = AuthenticationWizard::findWizard(d->channel.data());
721     if(wizard) {
722         wizard->raise();
723         wizard->showNormal();
724     } else {
725         new AuthenticationWizard(d->channel.data(), d->contactName, this, true);
726     }
727 }
728 
setupOTR()729 void ChatWidget::setupOTR()
730 {
731     qCDebug(KTP_TEXTUI_LIB);
732 
733     connect(d->channel.data(), SIGNAL(otrTrustLevelChanged(KTp::OTRTrustLevel, KTp::OTRTrustLevel)),
734             SLOT(onOTRTrustLevelChanged(KTp::OTRTrustLevel, KTp::OTRTrustLevel)));
735     connect(d->channel.data(), SIGNAL(sessionRefreshed()),
736             SLOT(onOTRsessionRefreshed()));
737     connect(d->channel.data(), SIGNAL(peerAuthenticationRequestedQA(const QString&)),
738             SLOT(onPeerAuthenticationRequestedQA(const QString&)));
739     connect(d->channel.data(), SIGNAL(peerAuthenticationRequestedSS()),
740             SLOT(onPeerAuthenticationRequestedSS()));
741     connect(d->channel.data(), SIGNAL(peerAuthenticationConcluded(bool)),
742             SLOT(onPeerAuthenticationConcluded(bool)));
743     connect(d->channel.data(), SIGNAL(peerAuthenticationInProgress()),
744             SLOT(onPeerAuthenticationInProgress()));
745     connect(d->channel.data(), SIGNAL(peerAuthenticationAborted()),
746             SLOT(onPeerAuthenticationAborted()));
747     connect(d->channel.data(), SIGNAL(peerAuthenticationError()),
748             SLOT(onPeerAuthenticationFailed()));
749     connect(d->channel.data(), SIGNAL(peerAuthenticationCheated()),
750             SLOT(onPeerAuthenticationFailed()));
751 }
752 
onOTRTrustLevelChanged(KTp::OTRTrustLevel trustLevel,KTp::OTRTrustLevel previous)753 void ChatWidget::onOTRTrustLevelChanged(KTp::OTRTrustLevel trustLevel, KTp::OTRTrustLevel previous)
754 {
755     qCDebug(KTP_TEXTUI_LIB);
756 
757     if (trustLevel == previous) {
758         return;
759     }
760 
761     d->hasNewOTRstatus = true;
762     switch(trustLevel) {
763         case KTp::OTRTrustLevelUnverified:
764             if(previous == KTp::OTRTrustLevelPrivate) {
765                 d->ui.chatArea->addStatusMessage(i18n("The OTR session is now unverified"));
766             }
767             else {
768                 d->ui.chatArea->addStatusMessage(i18n("Unverified OTR session started"));
769                 if(!this->isActiveWindow()) {
770                     OTRNotifications::otrSessionStarted(this, d->channel->textChannel()->targetContact(), false);
771                 }
772             }
773             break;
774         case KTp::OTRTrustLevelPrivate:
775             if(previous == KTp::OTRTrustLevelUnverified) {
776                 d->ui.chatArea->addStatusMessage(i18n("The OTR session is now private"));
777             }
778             else {
779                 d->ui.chatArea->addStatusMessage(i18n("Private OTR session started"));
780                 if(!this->isActiveWindow()) {
781                     OTRNotifications::otrSessionStarted(this, d->channel->textChannel()->targetContact(), true);
782                 }
783             }
784             break;
785         case KTp::OTRTrustLevelFinished:
786             d->ui.chatArea->addStatusMessage(i18n("%1 has ended the OTR session. You should do the same", d->contactName));
787             if(!this->isActiveWindow()) {
788                 OTRNotifications::otrSessionFinished(this, d->channel->textChannel()->targetContact());
789             }
790             break;
791 
792         default: break;
793     }
794 
795     Q_EMIT unreadMessagesChanged();
796     Q_EMIT otrStatusChanged(OtrStatus(trustLevel));
797 }
798 
onOTRsessionRefreshed()799 void ChatWidget::onOTRsessionRefreshed()
800 {
801     const QString msg = d->channel->otrTrustLevel() == KTp::OTRTrustLevelPrivate ?
802       i18n("Successfully refreshed private OTR session") :
803       i18n("Successfully refreshed unverified OTR session");
804     d->ui.chatArea->addStatusMessage(msg);
805 }
806 
onPeerAuthenticationRequestedQA(const QString & question)807 void ChatWidget::onPeerAuthenticationRequestedQA(const QString &question)
808 {
809     AuthenticationWizard *wizard = new AuthenticationWizard(d->channel.data(), d->contactName, this, false, question);
810     if(!wizard->isActiveWindow()) {
811         OTRNotifications::authenticationRequested(wizard, d->channel->textChannel()->targetContact());
812     }
813 }
814 
onPeerAuthenticationRequestedSS()815 void ChatWidget::onPeerAuthenticationRequestedSS()
816 {
817     AuthenticationWizard *wizard = new AuthenticationWizard(d->channel.data(), d->contactName, this, false);
818     if(!wizard->isActiveWindow()) {
819         OTRNotifications::authenticationRequested(wizard, d->channel->textChannel()->targetContact());
820     }
821 }
822 
onPeerAuthenticationConcluded(bool authenticated)823 void ChatWidget::onPeerAuthenticationConcluded(bool authenticated)
824 {
825     AuthenticationWizard *wizard = AuthenticationWizard::findWizard(d->channel.data());
826     if(wizard) {
827         wizard->raise();
828         wizard->showNormal();
829         wizard->finished(authenticated);
830     }
831     if(!wizard->isActiveWindow()) {
832         OTRNotifications::authenticationConcluded(wizard, d->channel->textChannel()->targetContact(), authenticated);
833     }
834 }
835 
onPeerAuthenticationInProgress()836 void ChatWidget::onPeerAuthenticationInProgress()
837 {
838     AuthenticationWizard *wizard = AuthenticationWizard::findWizard(d->channel.data());
839     if(wizard) {
840         wizard->raise();
841         wizard->showNormal();
842         wizard->nextState();
843     }
844 }
845 
onPeerAuthenticationAborted()846 void ChatWidget::onPeerAuthenticationAborted()
847 {
848     AuthenticationWizard *wizard = AuthenticationWizard::findWizard(d->channel.data());
849     if(wizard) {
850         wizard->raise();
851         wizard->showNormal();
852         wizard->aborted();
853     }
854     if(!wizard->isActiveWindow()) {
855         OTRNotifications::authenticationAborted(wizard, d->channel->textChannel()->targetContact());
856     }
857 }
858 
onPeerAuthenticationFailed()859 void ChatWidget::onPeerAuthenticationFailed()
860 {
861     AuthenticationWizard *wizard = AuthenticationWizard::findWizard(d->channel.data());
862     if(wizard) {
863         wizard->raise();
864         wizard->showNormal();
865         wizard->finished(false);
866     }
867     if(!wizard->isActiveWindow()) {
868         OTRNotifications::authenticationFailed(wizard, d->channel->textChannel()->targetContact());
869     }
870 }
871 
handleIncomingMessage(const Tp::ReceivedMessage & message,bool alreadyNotified)872 void ChatWidget::handleIncomingMessage(const Tp::ReceivedMessage &message, bool alreadyNotified)
873 {
874     if (d->chatViewInitialized) {
875 
876         d->exchangedMessagesCount++;
877 
878         //debug the message parts (looking for HTML etc)
879 //        Q_FOREACH(Tp::MessagePart part, message.parts())
880 //        {
881 //            qDebug() << "***";
882 //            Q_FOREACH(QString key, part.keys())
883 //            {
884 //                qDebug() << key << part.value(key).variant();
885 //            }
886 //        }
887 //      turns out we have no HTML, because no CM supports it yet
888 
889         if (message.isDeliveryReport()) {
890             QString text;
891             Tp::ReceivedMessage::DeliveryDetails reportDetails = message.deliveryDetails();
892 
893             if (reportDetails.hasDebugMessage()) {
894                 qCDebug(KTP_TEXTUI_LIB) << "delivery report debug message: " << reportDetails.debugMessage();
895             }
896 
897             if (reportDetails.isError()) {
898                 switch (reportDetails.error()) {
899                 case Tp::ChannelTextSendErrorOffline:
900                     if (reportDetails.hasEchoedMessage()) {
901                         if(message.sender() && message.sender()->isBlocked()) {
902                             text = i18n("Delivery of the message \"%1\" "
903                                         "failed because the remote contact is blocked",
904                                         reportDetails.echoedMessage().text());
905                          } else {
906                             text = i18n("Delivery of the message \"%1\" "
907                                         "failed because the remote contact is offline",
908                                         reportDetails.echoedMessage().text());
909                          }
910                     } else {
911                         if(message.sender() && message.sender()->isBlocked()) {
912                             text = i18n("Delivery of a message failed "
913                                         "because the remote contact is blocked");
914                         } else {
915                             text = i18n("Delivery of a message failed "
916                                         "because the remote contact is offline");
917                         }
918                     }
919                     break;
920                 case Tp::ChannelTextSendErrorInvalidContact:
921                     if (reportDetails.hasEchoedMessage()) {
922                         text = i18n("Delivery of the message \"%1\" "
923                                     "failed because the remote contact is not valid",
924                                     reportDetails.echoedMessage().text());
925                     } else {
926                         text = i18n("Delivery of a message failed "
927                                     "because the remote contact is not valid");
928                     }
929                     break;
930                 case Tp::ChannelTextSendErrorPermissionDenied:
931                     if (reportDetails.hasEchoedMessage()) {
932                         text = i18n("Delivery of the message \"%1\" failed because "
933                                     "you do not have permission to speak in this room",
934                                     reportDetails.echoedMessage().text());
935                     } else {
936                         text = i18n("Delivery of a message failed because "
937                                     "you do not have permission to speak in this room");
938                     }
939                     break;
940                 case Tp::ChannelTextSendErrorTooLong:
941                     if (reportDetails.hasEchoedMessage()) {
942                         text = i18n("Delivery of the message \"%1\" "
943                                     "failed because it was too long",
944                                     reportDetails.echoedMessage().text());
945                     } else {
946                         text = i18n("Delivery of a message failed "
947                                     "because it was too long");
948                     }
949                     break;
950                 default:
951                     if (reportDetails.hasEchoedMessage()) {
952                         text = i18n("Delivery of the message \"%1\" failed: %2",
953                                     reportDetails.echoedMessage().text(),
954                                     message.text());
955                     } else {
956                         text = i18n("Delivery of a message failed: %1", message.text());
957                     }
958                     break;
959                 }
960             } else {
961                 //TODO: handle delivery reports properly
962                 qCWarning(KTP_TEXTUI_LIB) << "Ignoring delivery report";
963                 d->channel->acknowledge(QList<Tp::ReceivedMessage>() << message);
964                 return;
965             }
966 
967             d->ui.chatArea->addStatusMessage(text, message.sender()->alias(), message.received());
968         } else {
969             KTp::Message processedMessage(KTp::MessageProcessor::instance()->processIncomingMessage(message, d->account, d->channel->textChannel()));
970             if (!alreadyNotified) {
971                 d->notifyFilter->filterMessage(processedMessage,
972                         KTp::MessageContext(d->account, d->channel->textChannel()));
973             }
974             if(KTp::Utils::isOtrEvent(message)) {
975                 d->ui.chatArea->addStatusMessage(KTp::Utils::processOtrMessage(message));
976             } else {
977                 d->ui.chatArea->addMessage(processedMessage);
978             }
979         }
980 
981         //if the window is on top, ack straight away. Otherwise they stay in the message queue for acking when activated..
982         if (isOnTop()) {
983             d->channel->acknowledge(QList<Tp::ReceivedMessage>() << message);
984         } else {
985             Q_EMIT unreadMessagesChanged();
986         }
987     }
988 
989 }
990 
handleMessageSent(const Tp::Message & message,Tp::MessageSendingFlags,const QString &)991 void ChatWidget::handleMessageSent(const Tp::Message &message, Tp::MessageSendingFlags, const QString&)
992 {
993     KTp::Message processedMessage(KTp::MessageProcessor::instance()->processIncomingMessage(message, d->account, d->channel->textChannel()));
994     d->notifyFilter->filterMessage(processedMessage,
995                                    KTp::MessageContext(d->account, d->channel->textChannel()));
996     d->ui.chatArea->addMessage(processedMessage);
997     d->exchangedMessagesCount++;
998 }
999 
chatViewReady()1000 void ChatWidget::chatViewReady()
1001 {
1002     disconnect(d->ui.chatArea, SIGNAL(loadFinished(bool)), this, SLOT(chatViewReady()));
1003 
1004     if (!d->logsLoaded || d->exchangedMessagesCount > 0) {
1005         if (d->exchangedMessagesCount == 0) {
1006             d->logManager->fetchScrollback();
1007         } else {
1008             d->logManager->fetchHistory(d->exchangedMessagesCount + d->logManager->scrollbackLength());
1009         }
1010     }
1011 
1012     d->logsLoaded = true;
1013 }
1014 
1015 
sendMessage()1016 void ChatWidget::sendMessage()
1017 {
1018     if(d->channel->isOTRsuppored() && d->channel->otrTrustLevel() == KTp::OTRTrustLevelFinished) {
1019         d->ui.chatArea->addStatusMessage(i18n("%1 has already closed his/her private connection to you. "
1020                     "Your message was not sent. Either end your private conversation, or restart it.", d->contactName));
1021         return;
1022     }
1023 
1024     QString message = d->ui.sendMessageBox->toPlainText();
1025 
1026     if (!message.isEmpty()) {
1027         message = KTp::MessageProcessor::instance()->processOutgoingMessage(
1028                 message, d->account, d->channel->textChannel()).text();
1029 
1030         if (d->channel->isValid()) {
1031             if (d->channel->supportsMessageType(Tp::ChannelTextMessageTypeAction) && message.startsWith(QLatin1String("/me "))) {
1032                 //remove "/me " from the start of the message
1033                 message.remove(0,4);
1034 
1035                 d->channel->send(message, Tp::ChannelTextMessageTypeAction);
1036             } else {
1037                 d->channel->send(message);
1038             }
1039             d->ui.sendMessageBox->clear();
1040         } else {
1041             d->ui.messageWidget->removeAction(d->messageWidgetSwitchOnlineAction);
1042             if (d->account->requestedPresence().type() == Tp::ConnectionPresenceTypeOffline) {
1043                 d->ui.messageWidget->addAction(d->messageWidgetSwitchOnlineAction);
1044             }
1045 
1046             d->ui.messageWidget->animatedShow();
1047         }
1048     }
1049 }
1050 
onChatStatusChanged(const Tp::ContactPtr & contact,Tp::ChannelChatState state)1051 void ChatWidget::onChatStatusChanged(const Tp::ContactPtr & contact, Tp::ChannelChatState state)
1052 {
1053     //don't show our own status changes.
1054     if (contact == d->channel->textChannel()->groupSelfContact()) {
1055         return;
1056     }
1057 
1058     if (state == Tp::ChannelChatStateGone) {
1059         if (d->ui.chatArea->showJoinLeaveChanges()) {
1060             d->ui.chatArea->addStatusMessage(i18n("%1 has left the chat", contact->alias()), contact->alias());
1061         }
1062     }
1063 
1064     if (d->isGroupChat) {
1065         //In a multiperson chat just because this user is no longer typing it doesn't mean that no-one is.
1066         //loop through each contact, check no-one is in composing mode.
1067 
1068         Tp::ChannelChatState tempState = Tp::ChannelChatStateInactive;
1069 
1070         Q_FOREACH (const Tp::ContactPtr & contact, d->channel->textChannel()->groupContacts()) {
1071             if (contact == d->channel->textChannel()->groupSelfContact()) {
1072                 continue;
1073             }
1074 
1075             tempState = d->channel->textChannel()->chatState(contact);
1076 
1077             if (tempState == Tp::ChannelChatStateComposing) {
1078                 state = tempState;
1079                 break;
1080             } else if (tempState == Tp::ChannelChatStatePaused && state != Tp::ChannelChatStateComposing) {
1081                 state = tempState;
1082             }
1083         }
1084     }
1085 
1086     if (state != d->remoteContactChatState) {
1087         d->remoteContactChatState = state;
1088         Q_EMIT userTypingChanged(state);
1089     }
1090 }
1091 
onContactPresenceChange(const Tp::ContactPtr & contact,const KTp::Presence & presence)1092 void ChatWidget::onContactPresenceChange(const Tp::ContactPtr & contact, const KTp::Presence &presence)
1093 {
1094     QString message;
1095     bool isYou = (contact == d->channel->textChannel()->groupSelfContact());
1096 
1097     if (isYou) {
1098         if (presence.statusMessage().isEmpty()) {
1099             message = i18nc("Your presence status", "You are now marked as %1",
1100                             presence.displayString());
1101         } else {
1102             message = i18nc("Your presence status with status message",
1103                             "You are now marked as %1 - %2",
1104                             presence.displayString(), presence.statusMessage());
1105         }
1106     }
1107     else {
1108         if (presence.statusMessage().isEmpty()) {
1109             message = i18nc("User's name, with their new presence status (i.e online/away)","%1 is %2", contact->alias(), presence.displayString());
1110         } else {
1111             message = i18nc("User's name, with their new presence status (i.e online/away) and a sepecified presence message","%1 is %2 - %3",
1112                             contact->alias(),
1113                             presence.displayString(),
1114                             presence.statusMessage());
1115         }
1116     }
1117 
1118     if (!message.isNull()) {
1119         if (d->ui.chatArea->showPresenceChanges()) {
1120             d->ui.chatArea->addStatusMessage(message, contact->alias());
1121         }
1122     }
1123 
1124     //if in a non-group chat situation, and the other contact has changed state...
1125     if (!d->isGroupChat && !isYou) {
1126         Q_EMIT iconChanged(icon());
1127     }
1128 
1129     Q_EMIT contactPresenceChanged(presence);
1130 }
1131 
onContactAliasChanged(const Tp::ContactPtr & contact,const QString & alias)1132 void ChatWidget::onContactAliasChanged(const Tp::ContactPtr & contact, const QString& alias)
1133 {
1134     QString message;
1135     bool isYou = (contact == d->channel->textChannel()->groupSelfContact());
1136 
1137     if (isYou) {
1138         if (d->yourName != alias) {
1139             message = i18n("You are now known as %1", alias);
1140             d->yourName = alias;
1141         }
1142     } else if (!d->isGroupChat) {
1143         //HACK the title is the contact alias on non-groupchats,
1144         //but we should have a better way of keeping the previous
1145         //aliases of all contacts
1146         if (d->contactName != alias) {
1147             message = i18n("%1 is now known as %2", d->contactName, alias);
1148             d->contactName = alias;
1149         }
1150     }
1151 
1152     if (!message.isEmpty()) {
1153         d->ui.chatArea->addStatusMessage(i18n("%1 has left the chat", contact->alias()), contact->alias());
1154     }
1155 
1156     //if in a non-group chat situation, and the other contact has changed alias...
1157     if (!d->isGroupChat && !isYou) {
1158         Q_EMIT titleChanged(alias);
1159     }
1160 }
1161 
onContactBlockStatusChanged(const Tp::ContactPtr & contact,bool blocked)1162 void ChatWidget::onContactBlockStatusChanged(const Tp::ContactPtr &contact, bool blocked)
1163 {
1164     QString message;
1165     if(blocked) {
1166         message = i18n("%1 is now blocked.", contact->alias());
1167     } else {
1168         message = i18n("%1 is now unblocked.", contact->alias());
1169     }
1170 
1171     d->ui.chatArea->addStatusMessage(message);
1172 
1173     Q_EMIT contactBlockStatusChanged(blocked);
1174 }
1175 
onContactClientTypesChanged(const Tp::ContactPtr & contact,const QStringList & clientTypes)1176 void ChatWidget::onContactClientTypesChanged(const Tp::ContactPtr &contact, const QStringList &clientTypes)
1177 {
1178     Q_UNUSED(clientTypes)
1179     bool isYou = (contact == d->channel->textChannel()->groupSelfContact());
1180 
1181     if (!d->isGroupChat && !isYou) {
1182         Q_EMIT iconChanged(icon());
1183     }
1184 }
1185 
onParticipantsChanged(Tp::Contacts groupMembersAdded,Tp::Contacts groupLocalPendingMembersAdded,Tp::Contacts groupRemotePendingMembersAdded,Tp::Contacts groupMembersRemoved,Tp::Channel::GroupMemberChangeDetails details)1186 void ChatWidget::onParticipantsChanged(Tp::Contacts groupMembersAdded,
1187                                        Tp::Contacts groupLocalPendingMembersAdded,
1188                                        Tp::Contacts groupRemotePendingMembersAdded,
1189                                        Tp::Contacts groupMembersRemoved,
1190                                        Tp::Channel::GroupMemberChangeDetails details) {
1191     Q_UNUSED(groupLocalPendingMembersAdded);
1192     Q_UNUSED(groupRemotePendingMembersAdded);
1193     Q_UNUSED(groupMembersRemoved);
1194     Q_UNUSED(details);
1195 
1196     if (groupMembersAdded.count() > 0 && (d->ui.chatArea->showJoinLeaveChanges())) {
1197         d->ui.chatArea->addStatusMessage(i18n("%1 has joined the chat", groupMembersAdded.toList().value(0).data()->alias()), groupMembersAdded.toList().value(0).data()->alias());
1198     }
1199     // Temporarily detect on-demand rooms by checking for gabble-created string "private-chat"
1200     if (d->isGroupChat && d->channel->textChannel()->targetId().startsWith(QLatin1String("private-chat"))) {
1201         QList<QString> contactAliasList;
1202         if (d->channel->textChannel()->groupContacts().count() > 0) {
1203             Q_FOREACH (const Tp::ContactPtr &contact, d->channel->textChannel()->groupContacts()) {
1204                 contactAliasList.append(contact->alias());
1205             }
1206             contactAliasList.removeAll(d->channel->textChannel()->groupSelfContact()->alias());
1207             qSort(contactAliasList);
1208 
1209             int aliasesToShow = qMin(contactAliasList.length(), 2);
1210             QString newTitle;
1211 
1212             //This filters each contact alias and tries to make a best guess at intelligently
1213             //shortening their alias to ensure the tab isn't too long, (hard-limited to 10) e.g.:
1214             //Robert@kdetalk.net is filtered at the @, giving Robert, and
1215             //Fred Jones is filtered by the ' ', giving Fred.
1216             Q_FOREACH (const QString &contactAlias, contactAliasList) {
1217                 aliasesToShow--;
1218                 if (contactAlias.indexOf(QLatin1Char(' ')) != -1) {
1219                     newTitle += contactAlias.left(contactAlias.indexOf(QLatin1Char(' '))).left(10);
1220                 } else if (contactAlias.indexOf(QLatin1Char('@')) != -1) {
1221                     newTitle += contactAlias.left(contactAlias.indexOf(QLatin1Char('@'))).left(10);
1222                 } else {
1223                     newTitle += contactAlias.left(10);
1224                 }
1225                 if (aliasesToShow > 0) {
1226                     newTitle += QLatin1String(", ");
1227                 } else {
1228                     break;
1229                 }
1230             }
1231             if (contactAliasList.count() > 2) {
1232                 newTitle.append(QLatin1String(" +")).append(QString::number(contactAliasList.size()-2));
1233             }
1234 
1235             Q_EMIT titleChanged(newTitle);
1236         }
1237         if (contactAliasList.count() == 0) {
1238                 Q_EMIT titleChanged(i18n("Group Chat"));
1239         }
1240     }
1241 }
1242 
onChannelInvalidated()1243 void ChatWidget::onChannelInvalidated()
1244 {
1245     setChatEnabled(false);
1246 }
1247 
onInputBoxChanged()1248 void ChatWidget::onInputBoxChanged()
1249 {
1250     //if the box is empty
1251     bool textBoxEmpty = d->ui.sendMessageBox->toPlainText().isEmpty();
1252 
1253     if(!textBoxEmpty) {
1254         //if the timer is active, it means the user is continuously typing
1255         if (d->pausedStateTimer->isActive()) {
1256             //just restart the timer and don't spam with chat state changes
1257             d->pausedStateTimer->start(5000);
1258         } else {
1259             //if the user has just typed some text, set state to Composing and start the timer
1260             //unless "show me typing" is off; in that case set state to Active and stop the timer
1261             if (TextChatConfig::instance()->showMeTyping()) {
1262                 d->channel->textChannel()->requestChatState(Tp::ChannelChatStateComposing);
1263                 d->pausedStateTimer->start(5000);
1264             } else {
1265                 d->channel->textChannel()->requestChatState(Tp::ChannelChatStateActive);
1266                 d->pausedStateTimer->stop();
1267             }
1268         }
1269     } else {
1270         //if the user typed no text/cleared the input field, set Active and stop the timer
1271         d->channel->textChannel()->requestChatState(Tp::ChannelChatStateActive);
1272         d->pausedStateTimer->stop();
1273     }
1274 }
1275 
findTextInChat(const QString & text,QWebEnginePage::FindFlags flags)1276 void ChatWidget::findTextInChat(const QString& text, QWebEnginePage::FindFlags flags)
1277 {
1278     // reset highlights
1279     d->ui.chatArea->findText(QString(), flags);
1280 
1281     d->ui.chatArea->findText(text, flags, [&] (bool found) { Q_EMIT searchTextComplete(found); });
1282 }
1283 
findNextTextInChat(const QString & text,QWebEnginePage::FindFlags flags)1284 void ChatWidget::findNextTextInChat(const QString& text, QWebEnginePage::FindFlags flags)
1285 {
1286     d->ui.chatArea->findText(text, flags);
1287 }
1288 
findPreviousTextInChat(const QString & text,QWebEnginePage::FindFlags flags)1289 void ChatWidget::findPreviousTextInChat(const QString& text, QWebEnginePage::FindFlags flags)
1290 {
1291     // for "backwards" search
1292     flags |= QWebEnginePage::FindBackward;
1293     d->ui.chatArea->findText(text, flags);
1294 }
1295 
setSpellDictionary(const QString & dict)1296 void ChatWidget::setSpellDictionary(const QString &dict)
1297 {
1298     d->ui.sendMessageBox->setSpellCheckingLanguage(dict);
1299 }
1300 
currentKeyboardLayoutLanguage() const1301 QString ChatWidget::currentKeyboardLayoutLanguage() const
1302 {
1303     return d->currentKeyboardLayoutLanguage;
1304 }
1305 
setCurrentKeyboardLayoutLanguage(const QString & language)1306 void ChatWidget::setCurrentKeyboardLayoutLanguage(const QString &language)
1307 {
1308     d->currentKeyboardLayoutLanguage = language;
1309 }
1310 
saveSpellCheckingOption()1311 void ChatWidget::saveSpellCheckingOption()
1312 {
1313     QString spellCheckingLanguage = spellDictionary();
1314     KSharedConfigPtr config = KSharedConfig::openConfig(QLatin1String("ktp-text-uirc"));
1315     KConfigGroup configGroup = config->group(d->channel->textChannel()->targetId());
1316     if (spellCheckingLanguage != Sonnet::Speller().defaultLanguage()) {
1317         configGroup.writeEntry("language", spellCheckingLanguage);
1318     } else {
1319         if (configGroup.exists()) {
1320             configGroup.deleteEntry("language");
1321             configGroup.deleteGroup();
1322         } else {
1323             return;
1324         }
1325     }
1326     configGroup.sync();
1327 }
1328 
loadSpellCheckingOption()1329 void ChatWidget::loadSpellCheckingOption()
1330 {
1331     // KTextEdit::setSpellCheckingLanguage() (see call below) does not do anything if there
1332     // is no highlighter created yet, and KTextEdit::setCheckSpellingEnabled() does not create
1333     // it if there is no focus on widget.
1334     // Therefore d->ui.sendMessageBox->setSpellCheckingLanguage() call below would be is ignored.
1335     // While this looks like KTextEditor bug (espesially taking into account its documentation),
1336     // just a call to KTextEditor::createHighlighter() before setting language fixes this
1337     d->ui.sendMessageBox->createHighlighter();
1338 
1339     KSharedConfigPtr config = KSharedConfig::openConfig(QLatin1String("ktp-text-uirc"));
1340     KConfigGroup configGroup = config->group(d->channel->textChannel()->targetId());
1341     QString spellCheckingLanguage;
1342     if (configGroup.exists()) {
1343         spellCheckingLanguage = configGroup.readEntry("language");
1344     } else {
1345         spellCheckingLanguage = Sonnet::Speller().defaultLanguage();
1346     }
1347     d->ui.sendMessageBox->setSpellCheckingLanguage(spellCheckingLanguage);
1348 }
1349 
spellDictionary() const1350 QString ChatWidget::spellDictionary() const
1351 {
1352     return d->ui.sendMessageBox->spellCheckingLanguage();
1353 }
1354 
remoteChatState()1355 Tp::ChannelChatState ChatWidget::remoteChatState()
1356 {
1357     return d->remoteContactChatState;
1358 }
1359 
previousConversationAvailable()1360 bool ChatWidget::previousConversationAvailable()
1361 {
1362     return m_previousConversationAvailable;
1363 }
1364 
clear()1365 void ChatWidget::clear()
1366 {
1367     // Don't reload logs when re-initializing */
1368     d->logsLoaded = true;
1369     d->exchangedMessagesCount = 0;
1370     d->ui.sendMessageBox->clearHistory();
1371     initChatArea();
1372 }
1373 
setZoomFactor(qreal zoomFactor)1374 void ChatWidget::setZoomFactor(qreal zoomFactor)
1375 {
1376     d->ui.chatArea->setZoomFactor(zoomFactor);
1377 }
1378 
zoomFactor() const1379 qreal ChatWidget::zoomFactor() const
1380 {
1381     return d->ui.chatArea->zoomFactor();
1382 }
1383 
initChatArea()1384 void ChatWidget::initChatArea()
1385 {
1386     connect(d->ui.chatArea, SIGNAL(loadFinished(bool)), SLOT(chatViewReady()), Qt::QueuedConnection);
1387 
1388     d->ui.chatArea->load((d->isGroupChat ? AdiumThemeView::GroupChat : AdiumThemeView::SingleUserChat));
1389 
1390     AdiumThemeHeaderInfo info;
1391 
1392     info.setGroupChat(d->isGroupChat);
1393     //normal chat - self and one other person.
1394     if (d->isGroupChat) {
1395         // A temporary solution to display a roomname instead of a full jid
1396         // This should be reworked as soon as TpQt is offering the
1397         // room name property
1398         // Temporarily detect on-demand rooms by checking for
1399         // gabble-created string "private-chat"
1400         if (d->channel->textChannel()->targetId().contains(QLatin1String("private-chat"))) {
1401             info.setChatName(i18n("Group Chat"));
1402         } else {
1403             QString roomName = d->channel->textChannel()->targetId();
1404             roomName = roomName.left(roomName.indexOf(QLatin1Char('@')));
1405             info.setChatName(roomName);
1406         }
1407     } else {
1408         Tp::ContactPtr otherContact = d->channel->textChannel()->targetContact();
1409 
1410         Q_ASSERT(otherContact);
1411 
1412         d->contactName = otherContact->alias();
1413         info.setDestinationDisplayName(otherContact->alias());
1414         info.setDestinationName(otherContact->id());
1415         info.setChatName(otherContact->alias());
1416         info.setIncomingIconPath(QUrl::fromLocalFile(otherContact->avatarData().fileName));
1417         d->ui.contactsView->hide();
1418     }
1419 
1420     info.setSourceName(d->channel->textChannel()->connection()->protocolName());
1421 
1422     //set up anything related to 'self'
1423     info.setOutgoingIconPath(QUrl::fromLocalFile(d->channel->textChannel()->groupSelfContact()->avatarData().fileName));
1424 
1425     //set the message time
1426     if (!d->channel->messageQueue().isEmpty()) {
1427         info.setTimeOpened(d->channel->messageQueue().first().received());
1428     } else {
1429         info.setTimeOpened(QDateTime::currentDateTime());
1430     }
1431 
1432     info.setService(d->account->serviceName());
1433      // check iconPath docs for minus sign in -KIconLoader::SizeMedium
1434     info.setServiceIconPath(KIconLoader::global()->iconPath(d->account->iconName(), -KIconLoader::SizeMedium));
1435     d->ui.chatArea->initialise(info);
1436 
1437     //set the title of this chat.
1438     d->title = info.chatName();
1439 }
1440 
onChatPausedTimerExpired()1441 void ChatWidget::onChatPausedTimerExpired()
1442 {
1443     if (TextChatConfig::instance()->showMeTyping()) {
1444         d->channel->textChannel()->requestChatState(Tp::ChannelChatStatePaused);
1445     } else {
1446         d->channel->textChannel()->requestChatState(Tp::ChannelChatStateActive);
1447     }
1448 }
1449 
currentPresenceChanged(const Tp::Presence & presence)1450 void ChatWidget::currentPresenceChanged(const Tp::Presence &presence)
1451 {
1452     if (presence == Tp::Presence::offline()) {
1453         d->ui.chatArea->addStatusMessage(i18n("You are now offline"), d->yourName);
1454         iconChanged(icon());
1455     } else {
1456         if (d->ui.messageWidget && d->ui.messageWidget->isVisible()) {
1457             d->ui.messageWidget->animatedHide();
1458         }
1459     }
1460 }
1461 
addEmoticonToChat(const QString & emoticon)1462 void ChatWidget::addEmoticonToChat(const QString &emoticon)
1463 {
1464     d->ui.sendMessageBox->insertPlainText(QLatin1String(" ") + emoticon);
1465     d->ui.sendMessageBox->setFocus();
1466 }
1467 
reloadTheme()1468 void ChatWidget::reloadTheme()
1469 {
1470     d->logsLoaded = false;
1471     d->chatViewInitialized = false;
1472 
1473     initChatArea();
1474 }
1475 
onContactsViewContextMenuRequested(const QPoint & point)1476 void ChatWidget::onContactsViewContextMenuRequested(const QPoint& point)
1477 {
1478     const QModelIndex index = d->ui.contactsView->indexAt(point);
1479     if (!index.isValid()) {
1480         return;
1481     }
1482 
1483     const KTp::ContactPtr contact = KTp::ContactPtr::qObjectCast<Tp::Contact>(index.data(KTp::ContactRole).value<Tp::ContactPtr>());
1484 
1485     bool isSelfContact = ((Tp::ContactPtr) contact == textChannel()->groupSelfContact());
1486     d->contactsMenu->findChild<QAction*>(QLatin1String("OpenChatWindowAction"))->setEnabled(!isSelfContact);
1487 
1488     d->contactsMenu->findChild<QAction*>(QLatin1String("SendFileAction"))->setEnabled(contact->fileTransferCapability());
1489 
1490     d->contactsMenu->setProperty("Contact", QVariant::fromValue(contact));
1491     d->contactsMenu->popup(d->ui.contactsView->mapToGlobal(point));
1492 }
1493 
onFileTransferMenuActionTriggered()1494 void ChatWidget::onFileTransferMenuActionTriggered()
1495 {
1496     if (!d->fileToTransferPath.isEmpty()) {
1497 	KTp::Actions::startFileTransfer(d->account, d->channel->textChannel()->targetContact(), d->fileToTransferPath);
1498     }
1499 }
1500 
onMessageWidgetSwitchOnlineActionTriggered()1501 void ChatWidget::onMessageWidgetSwitchOnlineActionTriggered()
1502 {
1503     d->account->setRequestedPresence(Tp::Presence::available());
1504 }
1505 
onShareImageMenuActionTriggered()1506 void ChatWidget::onShareImageMenuActionTriggered()
1507 {
1508     if (!d->fileToTransferPath.isEmpty()) {
1509         d->shareProvider->publish(d->fileToTransferPath);
1510     }
1511 }
1512 
onShowContactDetailsClicked()1513 void ChatWidget::onShowContactDetailsClicked()
1514 {
1515     const KTp::ContactPtr contact = d->contactsMenu->property("Contact").value<KTp::ContactPtr>();
1516     Q_ASSERT(!contact.isNull());
1517 
1518     KTp::ContactInfoDialog *dlg = new KTp::ContactInfoDialog(d->account, contact, this);
1519     connect(dlg, SIGNAL(finished()), dlg, SLOT(deleteLater()));
1520     dlg->show();
1521 }
1522 
onShareProviderFinishedSuccess(ShareProvider * provider,const QString & imageUrl)1523 void ChatWidget::onShareProviderFinishedSuccess(ShareProvider* provider, const QString& imageUrl)
1524 {
1525     Q_UNUSED(provider);
1526     if (!imageUrl.isEmpty()) {
1527         d->channel->send(imageUrl);
1528     }
1529 }
1530 
onShareProviderFinishedFailure(ShareProvider * provider,const QString & errorMessage)1531 void ChatWidget::onShareProviderFinishedFailure(ShareProvider* provider, const QString& errorMessage)
1532 {
1533     Q_UNUSED(provider);
1534     d->ui.chatArea->addStatusMessage(i18n("Uploading Image has Failed with Error: %1", errorMessage));
1535 }
1536 
1537 
onOpenContactChatWindowClicked()1538 void ChatWidget::onOpenContactChatWindowClicked()
1539 {
1540     const KTp::ContactPtr contact = d->contactsMenu->property("Contact").value<KTp::ContactPtr>();
1541     Q_ASSERT(!contact.isNull());
1542     KTp::Actions::startChat(d->account, contact);
1543 }
1544 
onSendFileClicked()1545 void ChatWidget::onSendFileClicked()
1546 {
1547     const KTp::ContactPtr contact = d->contactsMenu->property("Contact").value<KTp::ContactPtr>();
1548     Q_ASSERT(!contact.isNull());
1549     const QString filename = QFileDialog::getOpenFileName();
1550     if (filename.isEmpty() || !QFile::exists(filename)) {
1551         return;
1552     }
1553 
1554     KTp::Actions::startFileTransfer(d->account, contact, filename);
1555 }
1556