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