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