1 /*
2 Copyright (C) 2011 Lasath Fernando <kde@lasath.org>
3 Copyright (C) 2016 Martin Klapetek <mklapetek@kde.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public
16 License along with this library; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20
21 #include "conversation.h"
22 #include "messages-model.h"
23
24 #include <TelepathyQt/TextChannel>
25 #include <TelepathyQt/Account>
26 #include <TelepathyQt/PendingChannelRequest>
27 #include <TelepathyQt/PendingChannel>
28
29 #include "debug.h"
30
31 #include "channel-delegator.h"
32
33 class Conversation::ConversationPrivate
34 {
35 public:
ConversationPrivate()36 ConversationPrivate()
37 {
38 messages = nullptr;
39 delegated = false;
40 valid = false;
41 isGroupChat = false;
42 }
43
44 MessagesModel *messages;
45 //stores if the conversation has been delegated to another client and we are only observing the channel
46 //and not handling it.
47 bool delegated;
48 bool valid;
49 Tp::AccountPtr account;
50 QTimer *pausedStateTimer;
51 KPeople::PersonData *personData;
52 // May be null for group chats.
53 KTp::ContactPtr targetContact;
54 bool isGroupChat;
55 };
56
Conversation(const Tp::TextChannelPtr & channel,const Tp::AccountPtr & account,QObject * parent)57 Conversation::Conversation(const Tp::TextChannelPtr &channel,
58 const Tp::AccountPtr &account,
59 QObject *parent) :
60 QObject(parent),
61 d (new ConversationPrivate)
62 {
63 qCDebug(KTP_DECLARATIVE);
64 d->valid = false;
65 d->isGroupChat = false;
66
67 d->account = account;
68 connect(d->account.data(), SIGNAL(connectionChanged(Tp::ConnectionPtr)), SLOT(onAccountConnectionChanged(Tp::ConnectionPtr)));
69
70 d->messages = new MessagesModel(account, this);
71 connect(d->messages, &MessagesModel::unreadCountChanged, this, &Conversation::unreadMessagesChanged);
72 connect(d->messages, &MessagesModel::lastMessageChanged, this, &Conversation::lastMessageChanged);
73 setTextChannel(channel);
74
75 d->delegated = false;
76
77 d->pausedStateTimer = new QTimer(this);
78 d->pausedStateTimer->setSingleShot(true);
79 connect(d->pausedStateTimer, SIGNAL(timeout()), this, SLOT(onChatPausedTimerExpired()));
80 }
81
Conversation(const QString & contactId,const Tp::AccountPtr & account,QObject * parent)82 Conversation::Conversation(const QString &contactId,
83 const Tp::AccountPtr &account,
84 QObject *parent)
85 : QObject(parent),
86 d(new ConversationPrivate)
87 {
88 d->valid = true;
89 d->isGroupChat = false;
90 d->account = account;
91 d->personData = new KPeople::PersonData(QStringLiteral("ktp://") + d->account->objectPath().mid(35) + QStringLiteral("?") + contactId);
92
93 d->messages = new MessagesModel(account, this);
94 connect(d->messages, &MessagesModel::unreadCountChanged, this, &Conversation::unreadMessagesChanged);
95 connect(d->messages, &MessagesModel::lastMessageChanged, this, &Conversation::lastMessageChanged);
96 d->messages->setContactData(contactId, d->personData->name());
97
98 Q_EMIT avatarChanged();
99 Q_EMIT titleChanged();
100 Q_EMIT presenceIconChanged();
101 Q_EMIT validityChanged(d->valid);
102 }
103
setTextChannel(const Tp::TextChannelPtr & channel)104 void Conversation::setTextChannel(const Tp::TextChannelPtr &channel)
105 {
106 if (d->messages->account().isNull()) {
107 d->messages->setAccount(d->account);
108 }
109 if (d->messages->textChannel() != channel) {
110 d->messages->setTextChannel(channel);
111 d->valid = channel->isValid();
112 connect(channel.data(), SIGNAL(invalidated(Tp::DBusProxy*,QString,QString)),
113 SLOT(onChannelInvalidated(Tp::DBusProxy*,QString,QString)));
114
115 connect(channel.data(), &Tp::TextChannel::chatStateChanged, this, &Conversation::contactTypingChanged);
116
117 if (channel->targetContact().isNull()) {
118 d->isGroupChat = true;
119 } else {
120 d->isGroupChat = false;
121 d->targetContact = KTp::ContactPtr::qObjectCast(channel->targetContact());
122 d->personData = new KPeople::PersonData(QStringLiteral("ktp://") + d->account->objectPath().mid(35) + QStringLiteral("?") + d->targetContact->id());
123
124 connect(d->targetContact.constData(), SIGNAL(aliasChanged(QString)), SIGNAL(titleChanged()));
125 connect(d->targetContact.constData(), SIGNAL(presenceChanged(Tp::Presence)), SIGNAL(presenceIconChanged()));
126 connect(d->targetContact.constData(), SIGNAL(avatarDataChanged(Tp::AvatarData)), SIGNAL(avatarChanged()));
127 }
128
129 Q_EMIT avatarChanged();
130 Q_EMIT titleChanged();
131 Q_EMIT presenceIconChanged();
132 Q_EMIT validityChanged(d->valid);
133 }
134 }
135
textChannel() const136 Tp::TextChannelPtr Conversation::textChannel() const
137 {
138 return d->messages->textChannel();
139 }
140
messages() const141 MessagesModel* Conversation::messages() const
142 {
143 return d->messages;
144 }
145
title() const146 QString Conversation::title() const
147 {
148 if (d->isGroupChat) {
149 QString roomName = textChannel()->targetId();
150 return roomName.left(roomName.indexOf(QLatin1Char('@')));
151 } else {
152 return d->personData->name();
153 }
154 }
155
presenceIcon() const156 QIcon Conversation::presenceIcon() const
157 {
158 if (d->isGroupChat) {
159 return KTp::Presence(Tp::Presence::available()).icon();
160 } else if (!d->targetContact.isNull()) {
161 return KTp::Presence(d->targetContact->presence()).icon();
162 }
163
164 return QIcon();
165 }
166
avatar() const167 QIcon Conversation::avatar() const
168 {
169 if (d->isGroupChat) {
170 return QIcon();
171 } else {
172 const QString path = d->targetContact->avatarData().fileName;
173 QIcon icon;
174 if (!path.isEmpty()) {
175 icon = QIcon(path);
176 }
177 if (icon.availableSizes().isEmpty()) {
178 icon = QIcon::fromTheme(QStringLiteral("im-user"));
179 }
180 return icon;
181 }
182 }
183
targetContact() const184 KTp::ContactPtr Conversation::targetContact() const
185 {
186 if (d->isGroupChat) {
187 return KTp::ContactPtr();
188 } else {
189 return d->targetContact;
190 }
191 }
192
account() const193 Tp::AccountPtr Conversation::account() const
194 {
195 return d->account;
196 }
197
accountObject() const198 Tp::Account* Conversation::accountObject() const
199 {
200 if (!d->account.isNull()) {
201 return d->account.data();
202 }
203
204 return nullptr;
205 }
206
isValid() const207 bool Conversation::isValid() const
208 {
209 return d->valid;
210 }
211
onChannelInvalidated(Tp::DBusProxy * proxy,const QString & errorName,const QString & errorMessage)212 void Conversation::onChannelInvalidated(Tp::DBusProxy *proxy, const QString &errorName, const QString &errorMessage)
213 {
214 qCDebug(KTP_DECLARATIVE) << proxy << errorName << ":" << errorMessage;
215
216 d->valid = false;
217
218 Q_EMIT validityChanged(d->valid);
219 }
220
onAccountConnectionChanged(const Tp::ConnectionPtr & connection)221 void Conversation::onAccountConnectionChanged(const Tp::ConnectionPtr& connection)
222 {
223 //if we have reconnected and we were handling the channel
224 if (connection && ! d->delegated) {
225
226 //general convention is to never use ensureAndHandle when we already have a client registrar
227 //ensureAndHandle will implicity create a new temporary client registrar which is a waste
228 //it's also more code to get the new channel
229
230 //However, we cannot use use ensureChannel as normal because without being able to pass a preferredHandler
231 //we need a preferredHandler so that this handler is the one that ends up with the channel if multi handlers are active
232 //we do not know the name that this handler is currently registered with
233 Tp::PendingChannel *pendingChannel = d->account->ensureAndHandleTextChat(textChannel()->targetId());
234 connect(pendingChannel, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onCreateChannelFinished(Tp::PendingOperation*)));
235 }
236 }
237
onCreateChannelFinished(Tp::PendingOperation * op)238 void Conversation::onCreateChannelFinished(Tp::PendingOperation* op)
239 {
240 Tp::PendingChannel *pendingChannelOp = qobject_cast<Tp::PendingChannel*>(op);
241 Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(pendingChannelOp->channel());
242 if (textChannel) {
243 setTextChannel(textChannel);
244 }
245 }
246
delegateToProperClient()247 void Conversation::delegateToProperClient()
248 {
249 ChannelDelegator::delegateChannel(d->account, d->messages->textChannel());
250 d->delegated = true;
251 Q_EMIT conversationCloseRequested();
252 }
253
requestClose()254 void Conversation::requestClose()
255 {
256 qCDebug(KTP_DECLARATIVE);
257
258 if (!d->messages->textChannel().isNull()) {
259 d->messages->textChannel()->requestClose();
260 }
261 }
262
updateTextChanged(const QString & message)263 void Conversation::updateTextChanged(const QString &message)
264 {
265 if (!message.isEmpty()) {
266 //if the timer is active, it means the user is continuously typing
267 if (d->pausedStateTimer->isActive()) {
268 //just restart the timer and don't spam with chat state changes
269 d->pausedStateTimer->start(5000);
270 } else {
271 //if the user has just typed some text, set state to Composing and start the timer
272 d->messages->textChannel()->requestChatState(Tp::ChannelChatStateComposing);
273 d->pausedStateTimer->start(5000);
274 }
275 } else {
276 //if the user typed no text/cleared the input field, set Active and stop the timer
277 d->messages->textChannel()->requestChatState(Tp::ChannelChatStateActive);
278 d->pausedStateTimer->stop();
279 }
280 }
281
onChatPausedTimerExpired()282 void Conversation::onChatPausedTimerExpired()
283 {
284 d->messages->textChannel()->requestChatState(Tp::ChannelChatStatePaused);
285 }
286
~Conversation()287 Conversation::~Conversation()
288 {
289 qCDebug(KTP_DECLARATIVE);
290 //if we are not handling the channel do nothing.
291 // d->messages is valid here (destroyed in a deeper base class destructor)
292 // but the textChannel actually can be invalid.
293 if (!d->delegated && !d->messages->textChannel().isNull()) {
294 d->messages->textChannel()->requestClose();
295 }
296 delete d;
297 }
298
hasUnreadMessages() const299 bool Conversation::hasUnreadMessages() const
300 {
301 if (d->messages) {
302 return d->messages->unreadCount() > 0;
303 }
304
305 return false;
306 }
307
personData() const308 KPeople::PersonData* Conversation::personData() const
309 {
310 return d->personData;
311 }
312
isContactTyping() const313 bool Conversation::isContactTyping() const
314 {
315 if (d->messages->textChannel()) {
316 return d->messages->textChannel()->chatState(d->targetContact) == Tp::ChannelChatStateComposing;
317 }
318
319 return false;
320 }
321
canSendMessages() const322 bool Conversation::canSendMessages() const
323 {
324 if (d->messages && d->messages->textChannel()) {
325 return true;
326 }
327
328 return false;
329 }
330