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