1 /* Ricochet - https://ricochet.im/
2  * Copyright (C) 2014, John Brooks <john.brooks@dereferenced.net>
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  *      notice, this list of conditions and the following disclaimer.
10  *
11  *    * Redistributions in binary form must reproduce the above
12  *      copyright notice, this list of conditions and the following disclaimer
13  *      in the documentation and/or other materials provided with the
14  *      distribution.
15  *
16  *    * Neither the names of the copyright owners nor the names of its
17  *      contributors may be used to endorse or promote products derived from
18  *      this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 #include "ConversationModel.h"
34 #include "protocol/Connection.h"
35 #include "protocol/ChatChannel.h"
36 #include <QDebug>
37 
ConversationModel(QObject * parent)38 ConversationModel::ConversationModel(QObject *parent)
39     : QAbstractListModel(parent)
40     , m_contact(0)
41     , m_unreadCount(0)
42 {
43 }
44 
setContact(ContactUser * contact)45 void ConversationModel::setContact(ContactUser *contact)
46 {
47     if (contact == m_contact)
48         return;
49 
50     beginResetModel();
51     messages.clear();
52 
53     if (m_contact)
54         disconnect(m_contact, 0, this, 0);
55     m_contact = contact;
56     if (m_contact) {
57         auto connectChannel = [this](Protocol::Channel *channel) {
58             if (Protocol::ChatChannel *chat = qobject_cast<Protocol::ChatChannel*>(channel)) {
59                 connect(chat, &Protocol::ChatChannel::messageReceived, this, &ConversationModel::messageReceived);
60                 connect(chat, &Protocol::ChatChannel::messageAcknowledged, this, &ConversationModel::messageAcknowledged);
61 
62                 if (chat->direction() == Protocol::Channel::Outbound) {
63                     connect(chat, &Protocol::Channel::invalidated, this, &ConversationModel::outboundChannelClosed);
64                     sendQueuedMessages();
65                 }
66             }
67         };
68 
69         auto connectConnection = [this,connectChannel]() {
70             if (m_contact->connection()) {
71                 connect(m_contact->connection().data(), &Protocol::Connection::channelOpened, this, connectChannel);
72                 foreach (auto channel, m_contact->connection()->findChannels<Protocol::ChatChannel>())
73                     connectChannel(channel);
74                 sendQueuedMessages();
75             }
76         };
77 
78         connect(m_contact, &ContactUser::connected, this, connectConnection);
79         connectConnection();
80         connect(m_contact, &ContactUser::statusChanged,
81                 this, &ConversationModel::onContactStatusChanged);
82     }
83 
84     endResetModel();
85     emit contactChanged();
86 }
87 
sendMessage(const QString & text)88 void ConversationModel::sendMessage(const QString &text)
89 {
90     if (text.isEmpty())
91         return;
92 
93     MessageData message(text, QDateTime::currentDateTime(), 0, Queued);
94 
95     if (m_contact->connection()) {
96         auto channel = m_contact->connection()->findChannel<Protocol::ChatChannel>(Protocol::Channel::Outbound);
97         if (!channel) {
98             channel = new Protocol::ChatChannel(Protocol::Channel::Outbound, m_contact->connection().data());
99             if (!channel->openChannel()) {
100                 message.status = Error;
101                 delete channel;
102                 channel = 0;
103             }
104         }
105 
106         if (channel && channel->isOpened()) {
107             MessageId id = 0;
108             if (channel->sendChatMessage(text, QDateTime(), id))
109                 message.status = Sending;
110             else
111                 message.status = Error;
112             message.identifier = id;
113             message.attemptCount++;
114         }
115     }
116 
117     beginInsertRows(QModelIndex(), 0, 0);
118     messages.prepend(message);
119     endInsertRows();
120 }
121 
sendQueuedMessages()122 void ConversationModel::sendQueuedMessages()
123 {
124     if (!m_contact->connection())
125         return;
126 
127     // Quickly scan to see if we have any queued messages
128     bool haveQueued = false;
129     foreach (const MessageData &data, messages) {
130         if (data.status == Queued) {
131             haveQueued = true;
132             break;
133         }
134     }
135 
136     if (!haveQueued)
137         return;
138 
139     auto channel = m_contact->connection()->findChannel<Protocol::ChatChannel>(Protocol::Channel::Outbound);
140     if (!channel) {
141         channel = new Protocol::ChatChannel(Protocol::Channel::Outbound, m_contact->connection().data());
142         if (!channel->openChannel()) {
143             delete channel;
144             return;
145         }
146     }
147 
148     // sendQueuedMessages is called at channelOpened
149     if (!channel->isOpened())
150         return;
151 
152     // Iterate backwards, from oldest to newest messages
153     for (int i = messages.size() - 1; i >= 0; i--) {
154         if (messages[i].status == Queued) {
155             qDebug() << "Sending queued chat message";
156             bool ok = false;
157             if (messages[i].identifier)
158                 ok = channel->sendChatMessageWithId(messages[i].text, messages[i].time, messages[i].identifier);
159             else
160                 ok = channel->sendChatMessage(messages[i].text, messages[i].time, messages[i].identifier);
161             if (ok)
162                 messages[i].status = Sending;
163             else
164                 messages[i].status = Error;
165             messages[i].attemptCount++;
166             emit dataChanged(index(i, 0), index(i, 0));
167         }
168     }
169 }
170 
messageReceived(const QString & text,const QDateTime & time,MessageId id)171 void ConversationModel::messageReceived(const QString &text, const QDateTime &time, MessageId id)
172 {
173     // To preserve conversation flow despite potentially high latency, incoming messages
174     // are positioned above the last unacknowledged messages to the peer. We assume that
175     // the peer hadn't seen any unacknowledged message when this message was sent.
176     int row = 0;
177     for (int i = 0; i < messages.size() && i < 5; i++) {
178         if (messages[i].status != Sending && messages[i].status != Queued) {
179             row = i;
180             break;
181         }
182     }
183 
184     beginInsertRows(QModelIndex(), row, row);
185     MessageData message(text, time, id, Received);
186     messages.insert(row, message);
187     endInsertRows();
188 
189     m_unreadCount++;
190     emit unreadCountChanged();
191 }
192 
messageAcknowledged(MessageId id,bool accepted)193 void ConversationModel::messageAcknowledged(MessageId id, bool accepted)
194 {
195     int row = indexOfIdentifier(id, true);
196     if (row < 0)
197         return;
198 
199     MessageData &data = messages[row];
200     data.status = accepted ? Delivered : Error;
201     emit dataChanged(index(row, 0), index(row, 0));
202 }
203 
outboundChannelClosed()204 void ConversationModel::outboundChannelClosed()
205 {
206     // Any messages that are Sending are moved back to Queued, so they
207     // will be re-sent when we reconnect.
208     for (int i = 0; i < messages.size(); i++) {
209         if (messages[i].status != Sending)
210             continue;
211         if (messages[i].attemptCount >= 2) {
212             qDebug() << "Outbound chat channel closed, and unacknowledged message has been tried twice already. Marking as error.";
213             messages[i].status = Error;
214         } else {
215             qDebug() << "Outbound chat channel closed, putting unacknowledged chat message back in queue";
216             messages[i].status = Queued;
217         }
218         emit dataChanged(index(i, 0), index(i, 0));
219     }
220 
221     // Try to reopen the channel if we're still connected
222     if (m_contact && m_contact->connection() && m_contact->connection()->isConnected()) {
223         metaObject()->invokeMethod(this, "sendQueuedMessages", Qt::QueuedConnection);
224     }
225 }
226 
clear()227 void ConversationModel::clear()
228 {
229     if (messages.isEmpty())
230         return;
231 
232     beginRemoveRows(QModelIndex(), 0, messages.size()-1);
233     messages.clear();
234     endRemoveRows();
235 
236     resetUnreadCount();
237 }
238 
resetUnreadCount()239 void ConversationModel::resetUnreadCount()
240 {
241     if (m_unreadCount == 0)
242         return;
243     m_unreadCount = 0;
244     emit unreadCountChanged();
245 }
246 
onContactStatusChanged()247 void ConversationModel::onContactStatusChanged()
248 {
249     // Update in case section has changed
250     emit dataChanged(index(0, 0), index(rowCount()-1, 0), QVector<int>() << SectionRole);
251 }
252 
roleNames() const253 QHash<int,QByteArray> ConversationModel::roleNames() const
254 {
255     QHash<int, QByteArray> roles;
256     roles[Qt::DisplayRole] = "text";
257     roles[TimestampRole] = "timestamp";
258     roles[IsOutgoingRole] = "isOutgoing";
259     roles[StatusRole] = "status";
260     roles[SectionRole] = "section";
261     roles[TimespanRole] = "timespan";
262     return roles;
263 }
264 
rowCount(const QModelIndex & parent) const265 int ConversationModel::rowCount(const QModelIndex &parent) const
266 {
267     if (parent.isValid())
268         return 0;
269     return messages.size();
270 }
271 
data(const QModelIndex & index,int role) const272 QVariant ConversationModel::data(const QModelIndex &index, int role) const
273 {
274     if (!index.isValid() || index.row() >= messages.size())
275         return QVariant();
276 
277     const MessageData &message = messages[index.row()];
278 
279     switch (role) {
280         case Qt::DisplayRole: return message.text;
281         case TimestampRole: return message.time;
282         case IsOutgoingRole: return message.status != Received;
283         case StatusRole: return message.status;
284 
285         case SectionRole: {
286             if (m_contact->status() == ContactUser::Online)
287                 return QString();
288             if (index.row() < messages.size() - 1) {
289                 const MessageData &next = messages[index.row()+1];
290                 if (next.status != Received && next.status != Delivered)
291                     return QString();
292             }
293             for (int i = 0; i <= index.row(); i++) {
294                 if (messages[i].status == Received || messages[i].status == Delivered)
295                     return QString();
296             }
297             return QStringLiteral("offline");
298         }
299         case TimespanRole: {
300             if (index.row() < messages.size() - 1)
301                 return messages[index.row() + 1].time.secsTo(messages[index.row()].time);
302             else
303                 return -1;
304         }
305     }
306 
307     return QVariant();
308 }
309 
indexOfIdentifier(MessageId identifier,bool isOutgoing) const310 int ConversationModel::indexOfIdentifier(MessageId identifier, bool isOutgoing) const
311 {
312     for (int i = 0; i < messages.size(); i++) {
313         if (messages[i].identifier == identifier && (messages[i].status != Received) == isOutgoing)
314             return i;
315     }
316     return -1;
317 }
318 
319