1 /*
2     Copyright (C) 2010  David Edmundson    <kde@davidedmundson.co.uk>
3     Copyright (C) 2011  Dominik Schmidt    <dev@dominik-schmidt.de>
4     Copyright (C) 2011  Francesco Nwokeka  <francesco.nwokeka@gmail.com>
5     Copyright (C) 2014  Daniel Vrátil      <dvratil@redhat.com>
6 
7     This program is free software: you can redistribute it and/or modify
8     it under the terms of the GNU General Public License as published by
9     the Free Software Foundation, either version 2 of the License, or
10     (at your option) any later version.
11 
12     This program is distributed in the hope that it will be useful,
13     but WITHOUT ANY WARRANTY; without even the implied warranty of
14     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15     GNU General Public License for more details.
16 
17     You should have received a copy of the GNU General Public License
18     along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 #include "telepathy-chat-ui.h"
22 #include "chat-tab.h"
23 #include "chat-window.h"
24 #include "text-chat-config.h"
25 #include "notify-filter.h"
26 #include "text-chat-config.h"
27 #include "defines.h"
28 
29 #include <KConfigGroup>
30 #include <KWindowSystem>
31 
32 #include <QDebug>
33 #include <QEventLoopLocker>
34 
35 #include <TelepathyQt/ChannelClassSpec>
36 #include <TelepathyQt/TextChannel>
37 #include <TelepathyQt/ChannelRequest>
38 #include <TelepathyQt/ChannelRequestHints>
39 
40 #include <KTp/message-processor.h>
41 
42 #include <KAboutData>
43 #include <KLocalizedString>
44 #include "../ktptextui_version.h"
45 
46 
channelClassList()47 inline Tp::ChannelClassSpecList channelClassList()
48 {
49     return Tp::ChannelClassSpecList() << Tp::ChannelClassSpec::textChat()
50                                       << Tp::ChannelClassSpec::unnamedTextChat()
51                                       << Tp::ChannelClassSpec::textChatroom();
52 }
53 
54 
TelepathyChatUi(int & argc,char * argv[])55 TelepathyChatUi::TelepathyChatUi(int &argc, char *argv[])
56     : KTp::TelepathyHandlerApplication(argc, argv, -1, -1),
57       AbstractClientHandler(channelClassList())
58 {
59     // We need to set up KAboutData in here, before the ChatWindow gets created,
60     // otherwise the Settings and Help menu will not have the Application Name
61     // set and will contain just "ktp-text-ui".
62     KAboutData aboutData("ktp-text-ui", i18n("Chat Application"), QStringLiteral(KTP_TEXT_UI_VERSION_STRING));
63     aboutData.addAuthor(i18n("David Edmundson"), i18n("Developer"), "david@davidedmundson.co.uk");
64     aboutData.addAuthor(i18n("Marcin Ziemiński"), i18n("Developer"), "zieminn@gmail.com");
65     aboutData.addAuthor(i18n("Dominik Schmidt"), i18n("Past Developer"), "kde@dominik-schmidt.de");
66     aboutData.addAuthor(i18n("Francesco Nwokeka"), i18n("Past Developer"), "francesco.nwokeka@gmail.com");
67     aboutData.setProductName("telepathy/text-ui"); //set the correct name for bug reporting
68     aboutData.setLicense(KAboutLicense::GPL_V2);
69 
70     QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("telepathy-kde")));
71     KAboutData::setApplicationData(aboutData);
72 
73     m_eventLoopLocker = 0;
74     m_notifyFilter = new NotifyFilter;
75     ChatWindow *window = createWindow();
76     window->show();
77 }
78 
~TelepathyChatUi()79 TelepathyChatUi::~TelepathyChatUi()
80 {
81     Q_FOREACH (const Tp::TextChannelPtr &channel, m_channelAccountMap.keys()) {
82         channel->requestClose();
83     }
84     delete m_notifyFilter;
85 }
86 
createWindow()87 ChatWindow* TelepathyChatUi::createWindow()
88 {
89     ChatWindow* window = new ChatWindow();
90 
91     connect(window, SIGNAL(detachRequested(ChatTab*)), this, SLOT(dettachTab(ChatTab*)));
92     connect(window, SIGNAL(aboutToClose(ChatWindow*)), this, SLOT(onWindowAboutToClose(ChatWindow*)));
93 
94     m_chatWindows.push_back(window);
95 
96     return window;
97 }
98 
isHiddenChannel(const Tp::AccountPtr & account,const Tp::TextChannelPtr & channel,Tp::TextChannelPtr * oldChannel) const99 bool TelepathyChatUi::isHiddenChannel(const Tp::AccountPtr &account,
100                                       const Tp::TextChannelPtr& channel,
101                                       Tp::TextChannelPtr *oldChannel) const
102 {
103     if (channel->targetHandleType() != Tp::HandleTypeRoom) {
104         return false;
105     }
106 
107     QHash<Tp::TextChannelPtr,Tp::AccountPtr>::const_iterator it = m_channelAccountMap.constBegin();
108     for ( ; it != m_channelAccountMap.constEnd(); ++it) {
109         if (channel->targetId() == it.key()->targetId()
110             && channel->targetHandleType() == it.key()->targetHandleType()
111             && account == it.value())
112         {
113             *oldChannel = it.key();
114             return true;
115         }
116     }
117 
118     return false;
119 }
120 
dettachTab(ChatTab * tab)121 void TelepathyChatUi::dettachTab(ChatTab* tab)
122 {
123     ChatWindow* window = createWindow();
124     tab->setChatWindow(window);
125     window->show();
126 }
127 
handleChannels(const Tp::MethodInvocationContextPtr<> & context,const Tp::AccountPtr & account,const Tp::ConnectionPtr & connection,const QList<Tp::ChannelPtr> & channels,const QList<Tp::ChannelRequestPtr> & channelRequests,const QDateTime & userActionTime,const Tp::AbstractClientHandler::HandlerInfo & handlerInfo)128 void TelepathyChatUi::handleChannels(const Tp::MethodInvocationContextPtr<> & context,
129         const Tp::AccountPtr &account,
130         const Tp::ConnectionPtr &connection,
131         const QList<Tp::ChannelPtr> &channels,
132         const QList<Tp::ChannelRequestPtr> &channelRequests,
133         const QDateTime &userActionTime,
134         const Tp::AbstractClientHandler::HandlerInfo &handlerInfo)
135 {
136     Q_UNUSED(connection);
137     Q_UNUSED(userActionTime);
138     Q_UNUSED(handlerInfo);
139 
140     Tp::TextChannelPtr textChannel;
141     Q_FOREACH(const Tp::ChannelPtr & channel, channels) {
142         textChannel = Tp::TextChannelPtr::dynamicCast(channel);
143         if (textChannel) {
144             break;
145         }
146     }
147 
148     Q_ASSERT(textChannel);
149 
150     /*this works round a "bug" in which kwin will _deliberately_ stop the TextUi claiming focus
151      * because it thinks the user is busy interacting with the contact list.
152      * If the special hint org.kde.telepathy forceRaiseWindow is set to true, then we use KWindowSystem::forceActiveWindow
153      * to claim focus.
154      */
155     bool windowRaise = true;
156 
157     //find the relevant channelRequest
158     Q_FOREACH(const Tp::ChannelRequestPtr channelRequest, channelRequests) {
159         windowRaise = !channelRequest->hints().hint(QLatin1String("org.kde.telepathy"), QLatin1String("suppressWindowRaise")).toBool();
160     }
161 
162     qDebug() << "Incomming channel" << textChannel->targetId();
163     qDebug() << "raise window hint set to: " << windowRaise;
164 
165     Tp::TextChannelPtr oldTextChannel;
166     const bool isKnown = isHiddenChannel(account, textChannel, &oldTextChannel);
167     if (isKnown) {
168         // windowRaise is false, this is just an update after reconnect, so update
169         // cache, but don't create window
170         if (!windowRaise) {
171             releaseChannel(oldTextChannel, account, false);
172             takeChannel(textChannel, account, false);
173             return;
174         }
175     }
176 
177     bool tabFound = false;
178 
179     //search for any tabs which are already handling this channel.
180     for (int i = 0; i < m_chatWindows.count() && !tabFound; ++i) {
181         ChatWindow *window = m_chatWindows.at(i);
182         ChatTab* tab = window->getTab(account, textChannel);
183 
184         if (tab) {
185             tabFound = true;
186             if (windowRaise) {
187                 window->focusChat(tab);                 // set focus on selected tab
188                 KWindowSystem::forceActiveWindow(window->winId());
189             }
190 
191             // check if channel is invalid. Replace only if invalid
192             // You get this status if user goes offline and then back on without closing the chat
193             if (!tab->textChannel()->isValid()) {
194                 tab->setTextChannel(textChannel);    // replace with new one
195                 tab->setChatEnabled(true);           // re-enable chat
196             }
197         }
198     }
199 
200     //if it's a group chat, we've been invited to. Join it
201     if (textChannel->groupLocalPendingContacts().contains(textChannel->groupSelfContact())) {
202         textChannel->groupAddContacts(QList<Tp::ContactPtr>() << textChannel->groupSelfContact());
203     }
204 
205     //if there is currently no tab containing the incoming channel.
206     if (!tabFound) {
207         ChatWindow* window = 0;
208         switch (TextChatConfig::instance()->openMode()) {
209             case TextChatConfig::FirstWindow:
210                 window = m_chatWindows.count()?m_chatWindows[0]:createWindow();
211                 break;
212             case TextChatConfig::NewWindow:
213                 //as we now create a window on load, if we are in one window per chat mode
214                 //we need to check if the first made window is empty
215                 if (m_chatWindows.count() == 1 && ! m_chatWindows[0]->getCurrentTab()) {
216                     window = m_chatWindows[0];
217                 } else {
218                     window = createWindow();
219                 }
220                 break;
221         }
222 
223         Q_ASSERT(window);
224 
225         ChatTab* tab = new ChatTab(textChannel, account);
226         tab->setChatWindow(window);
227         connect(tab, SIGNAL(aboutToClose(ChatTab*)),
228                 this, SLOT(onTabAboutToClose(ChatTab*)));
229         window->show();
230 
231         if (windowRaise) {
232             KWindowSystem::forceActiveWindow(window->winId());
233         }
234     }
235 
236     // the channel now has a tab and a window that owns it, so we can release it
237     if (!oldTextChannel.isNull()) {
238         releaseChannel(oldTextChannel, account);
239     }
240 
241     context->setFinished();
242 }
243 
bypassApproval() const244 bool TelepathyChatUi::bypassApproval() const
245 {
246     return false;
247 }
248 
onTabAboutToClose(ChatTab * tab)249 void TelepathyChatUi::onTabAboutToClose(ChatTab *tab)
250 {
251     const Tp::TextChannelPtr channel = tab->textChannel();
252 
253     // Close 1-on-1 chats, but keep group chats opened if user has configured so
254     if (channel->targetHandleType() == Tp::HandleTypeContact || !TextChatConfig::instance()->dontLeaveGroupChats()) {
255         channel->requestClose();
256     } else {
257         takeChannel(channel, tab->account());
258     }
259 }
260 
onWindowAboutToClose(ChatWindow * window)261 void TelepathyChatUi::onWindowAboutToClose(ChatWindow* window)
262 {
263     Q_ASSERT(window);
264     m_chatWindows.removeOne(window);
265 
266     // Take all tabs now. When tab emits aboutToClose, it's too late to call KGlobal::ref(),
267     Q_FOREACH (ChatTab *tab, window->tabs()) {
268         disconnect(tab, SIGNAL(aboutToClose(ChatTab*)),
269                    this, SLOT(onTabAboutToClose(ChatTab*)));
270         onTabAboutToClose(tab);
271     }
272 }
273 
takeChannel(const Tp::TextChannelPtr & channel,const Tp::AccountPtr & account,bool ref)274 void TelepathyChatUi::takeChannel(const Tp::TextChannelPtr& channel, const Tp::AccountPtr& account, bool ref)
275 {
276     m_channelAccountMap.insert(channel, account);
277     connectChannelNotifications(channel, true);
278     connectAccountNotifications(account, true);
279 
280     if (ref && !m_eventLoopLocker) {
281         m_eventLoopLocker = new QEventLoopLocker();
282     }
283 }
284 
releaseChannel(const Tp::TextChannelPtr & channel,const Tp::AccountPtr & account,bool unref)285 void TelepathyChatUi::releaseChannel(const Tp::TextChannelPtr& channel, const Tp::AccountPtr& account, bool unref)
286 {
287     m_channelAccountMap.remove(channel);
288     connectChannelNotifications(channel, false);
289     if (m_channelAccountMap.keys(account).count() == 0) {
290         connectAccountNotifications(account, false);
291     }
292 
293     if (unref && m_eventLoopLocker) {
294         delete m_eventLoopLocker;
295         m_eventLoopLocker = 0;
296     }
297 }
298 
connectAccountNotifications(const Tp::AccountPtr & account,bool enable)299 void TelepathyChatUi::connectAccountNotifications(const Tp::AccountPtr& account, bool enable)
300 {
301     if (enable) {
302         connect(account.constData(), SIGNAL(connectionStatusChanged(Tp::ConnectionStatus)),
303                 this, SLOT(onConnectionStatusChanged(Tp::ConnectionStatus)),
304                 Qt::UniqueConnection);
305     } else {
306         disconnect(account.constData(), SIGNAL(connectionStatusChanged(Tp::ConnectionStatus)),
307                    this, SLOT(onConnectionStatusChanged(Tp::ConnectionStatus)));
308     }
309 }
310 
311 
connectChannelNotifications(const Tp::TextChannelPtr & textChannel,bool enable)312 void TelepathyChatUi::connectChannelNotifications(const Tp::TextChannelPtr &textChannel, bool enable)
313 {
314     if (enable) {
315         connect(textChannel.constData(), SIGNAL(messageReceived(Tp::ReceivedMessage)),
316                 this, SLOT(onGroupChatMessageReceived(Tp::ReceivedMessage)));
317         connect(textChannel.constData(), SIGNAL(invalidated(Tp::DBusProxy*,QString,QString)),
318                 this, SLOT(onChannelInvalidated()));
319     } else {
320         disconnect(textChannel.constData(), SIGNAL(messageReceived(Tp::ReceivedMessage)),
321                 this, SLOT(onGroupChatMessageReceived(Tp::ReceivedMessage)));
322         disconnect(textChannel.constData(), SIGNAL(invalidated(Tp::DBusProxy*,QString,QString)),
323                 this, SLOT(onChannelInvalidated()));
324     }
325 }
326 
327 
onGroupChatMessageReceived(const Tp::ReceivedMessage & message)328 void TelepathyChatUi::onGroupChatMessageReceived(const Tp::ReceivedMessage& message)
329 {
330     const Tp::TextChannelPtr channel(qobject_cast<Tp::TextChannel*>(sender()));
331     Tp::AccountPtr account = m_channelAccountMap.value(channel);
332 
333     KTp::Message processedMessage(KTp::MessageProcessor::instance()->processIncomingMessage(message, account, channel));
334     m_notifyFilter->filterMessage(processedMessage, KTp::MessageContext(account, channel));
335 }
336 
onChannelInvalidated()337 void TelepathyChatUi::onChannelInvalidated()
338 {
339     const Tp::TextChannelPtr channel(qobject_cast<Tp::TextChannel*>(sender()));
340     releaseChannel(channel, m_channelAccountMap.value(channel));
341 }
342 
onConnectionStatusChanged(Tp::ConnectionStatus status)343 void TelepathyChatUi::onConnectionStatusChanged(Tp::ConnectionStatus status)
344 {
345     if (status != Tp::ConnectionStatusConnected) {
346         return;
347     }
348 
349     Tp::ChannelRequestHints hints;
350     hints.setHint(QLatin1String("org.kde.telepathy"),QLatin1String("suppressWindowRaise"), QVariant(true));
351 
352     const Tp::AccountPtr account(qobject_cast<Tp::Account*>(sender()));
353     Q_FOREACH (const Tp::TextChannelPtr &channel, m_channelAccountMap.keys(account)) {
354         account->ensureTextChatroom(channel->targetId(),
355                                     QDateTime::currentDateTime(),
356                                     QLatin1String(KTP_TEXTUI_CLIENT_PATH),
357                                     hints);
358     }
359 }
360