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