1 // Copyright 2005-2019 The Mumble Developers. All rights reserved.
2 // Use of this source code is governed by a BSD-style license
3 // that can be found in the LICENSE file at the root of the
4 // Mumble source tree or at <https://www.mumble.info/LICENSE>.
5
6 #include "mumble_pch.hpp"
7
8 #include "UserListModel.h"
9 #include "Channel.h"
10 #include "Message.h"
11
12 #include <vector>
13 #include <algorithm>
14
UserListModel(const MumbleProto::UserList & userList,QObject * parent_)15 UserListModel::UserListModel(const MumbleProto::UserList& userList, QObject *parent_)
16 : QAbstractTableModel(parent_)
17 , m_legacyMode(false) {
18
19 m_userList.reserve(userList.users_size());
20 for (int i = 0; i < userList.users_size(); ++i) {
21 m_userList.append(userList.users(i));
22 }
23
24 if (!m_userList.empty()) {
25 const MumbleProto::UserList_User& user = m_userList.back();
26 m_legacyMode = !user.has_last_seen() || !user.has_last_channel();
27 }
28 }
29
rowCount(const QModelIndex & parentIndex) const30 int UserListModel::rowCount(const QModelIndex &parentIndex) const {
31 if (parentIndex.isValid())
32 return 0;
33
34 return m_userList.size();
35 }
36
columnCount(const QModelIndex & parentIndex) const37 int UserListModel::columnCount(const QModelIndex &parentIndex) const {
38 if (parentIndex.isValid())
39 return 0;
40
41 if (m_legacyMode) {
42 // Only COL_NICK
43 return 1;
44 }
45
46 return COUNT_COL;
47 }
48
headerData(int section,Qt::Orientation orientation,int role) const49 QVariant UserListModel::headerData(int section, Qt::Orientation orientation, int role) const {
50 if (orientation != Qt::Horizontal)
51 return QVariant();
52
53 if (section < 0 || section >= columnCount())
54 return QVariant();
55
56 if (role == Qt::DisplayRole) {
57 switch (section) {
58 case COL_NICK: return tr("Nick");
59 case COL_INACTIVEDAYS: return tr("Inactive days");
60 case COL_LASTCHANNEL: return tr("Last channel");
61 default: return QVariant();
62 }
63 }
64
65 return QVariant();
66 }
67
data(const QModelIndex & dataIndex,int role) const68 QVariant UserListModel::data(const QModelIndex &dataIndex, int role) const {
69 if (!dataIndex.isValid())
70 return QVariant();
71
72 if (dataIndex.row() < 0 || dataIndex.row() >= m_userList.size())
73 return QVariant();
74
75 if (dataIndex.column() >= columnCount())
76 return QVariant();
77
78 const MumbleProto::UserList_User& user = m_userList[dataIndex.row()];
79
80 if (role == Qt::DisplayRole) {
81 switch (dataIndex.column()) {
82 case COL_NICK: return u8(user.name());
83 case COL_INACTIVEDAYS: return lastSeenToTodayDayCount(user.last_seen());
84 case COL_LASTCHANNEL: return pathForChannelId(user.last_channel());
85 default: return QVariant();
86 }
87 } else if (role == Qt::ToolTipRole) {
88 switch (dataIndex.column()) {
89 case COL_INACTIVEDAYS: return tr("Last seen: %1").arg(user.last_seen().empty() ?
90 tr("Never")
91 : Qt::escape(u8(user.last_seen())));
92 case COL_LASTCHANNEL: return tr("Channel ID: %1").arg(user.last_channel());
93 default: return QVariant();
94 }
95 } else if (role == Qt::UserRole) {
96 switch (dataIndex.column()) {
97 case COL_INACTIVEDAYS: return isoUTCToDateTime(user.last_seen());
98 case COL_LASTCHANNEL: return user.last_channel();
99 default: return QVariant();
100 }
101 } else if (role == Qt::EditRole) {
102 if (dataIndex.column() == COL_NICK) {
103 return u8(user.name());
104 }
105 }
106
107 return QVariant();
108 }
109
setData(const QModelIndex & dataIndex,const QVariant & value,int role)110 bool UserListModel::setData(const QModelIndex &dataIndex, const QVariant &value, int role) {
111 if (!dataIndex.isValid())
112 return false;
113
114 if (dataIndex.column() != COL_NICK || role != Qt::EditRole)
115 return false;
116
117 if (dataIndex.row() < 0 || dataIndex.row() >= m_userList.size())
118 return false;
119
120 const std::string newNick = u8(value.toString());
121 if (newNick.empty()) {
122 // Empty nick is not valid
123 return false;
124 }
125
126 MumbleProto::UserList_User& user = m_userList[dataIndex.row()];
127 if (newNick != user.name()) {
128 foreach (const MumbleProto::UserList_User& otherUser, m_userList) {
129 if (otherUser.name() == newNick) {
130 // Duplicate is not valid
131 return false;
132 }
133 }
134
135 user.set_name(newNick);
136 m_changes[user.user_id()] = user;
137
138 emit dataChanged(dataIndex, dataIndex);
139 }
140
141 return true;
142 }
143
flags(const QModelIndex & flagIndex) const144 Qt::ItemFlags UserListModel::flags(const QModelIndex &flagIndex) const {
145 const Qt::ItemFlags original = QAbstractTableModel::flags(flagIndex);
146
147 if (flagIndex.column() == COL_NICK) {
148 return original | Qt::ItemIsEditable;
149 }
150
151 return original;
152 }
153
removeRows(int row,int count,const QModelIndex & parentIndex)154 bool UserListModel::removeRows(int row, int count, const QModelIndex &parentIndex) {
155 if (row + count > m_userList.size())
156 return false;
157
158 beginRemoveRows(parentIndex, row, row + count - 1);
159
160 ModelUserList::Iterator startIt = m_userList.begin() + row;
161 ModelUserList::Iterator endIt = startIt + count;
162
163 for (ModelUserList::Iterator it = startIt;
164 it != endIt;
165 ++it) {
166 it->clear_name();
167 m_changes[it->user_id()] = *it;
168 }
169
170 m_userList.erase(startIt, endIt);
171
172 endRemoveRows();
173 return true;
174 }
175
removeRowsInSelection(const QItemSelection & selection)176 void UserListModel::removeRowsInSelection(const QItemSelection &selection) {
177 QModelIndexList indices = selection.indexes();
178
179 std::vector<int> rows;
180 rows.reserve(indices.size());
181
182 foreach (const QModelIndex& idx, indices) {
183 if (idx.column() != COL_NICK)
184 continue;
185
186 rows.push_back(idx.row());
187 }
188
189 // Make sure to presort the rows so we work from back (high) to front (low).
190 // This prevents the row numbers from becoming invalid after removing a group.
191 // The basic idea is to take a number of sorted rows (e.g. 10,9,5,3,2,1) and
192 // delete them with the minimum number of removeRows calls. This means grouping
193 // adjacent rows (e.g. (10,9),(5),(3,2,1)) and using a removeRows call for each group.
194 std::sort(rows.begin(), rows.end(), std::greater<int>());
195
196 int nextRow = -2;
197 int groupRowCount = 0;
198
199 for (size_t i = 0; i < rows.size(); ++i) {
200 if (rows[i] == nextRow) {
201 ++groupRowCount;
202 --nextRow;
203 } else {
204 if (groupRowCount > 0) {
205 // Remove previous group
206 const int lastRowInGroup = nextRow + 1;
207 removeRows(lastRowInGroup, groupRowCount);
208 }
209
210 // Start next group
211 nextRow = rows[i] - 1;
212 groupRowCount = 1;
213 }
214 }
215
216 if (groupRowCount > 0) {
217 // Remove leftover group
218 const int lastRowInGroup = nextRow + 1;
219 removeRows(lastRowInGroup, groupRowCount);
220 }
221 }
222
getUserListUpdate() const223 MumbleProto::UserList UserListModel::getUserListUpdate() const {
224 MumbleProto::UserList updateList;
225
226 for (ModelUserListChangeMap::ConstIterator it = m_changes.constBegin();
227 it != m_changes.constEnd();
228 ++it) {
229 MumbleProto::UserList_User* user = updateList.add_users();
230 *user = it.value();
231 }
232
233 return updateList;
234 }
235
isUserListDirty() const236 bool UserListModel::isUserListDirty() const {
237 return !m_changes.empty();
238 }
239
isLegacy() const240 bool UserListModel::isLegacy() const {
241 return m_legacyMode;
242 }
243
lastSeenToTodayDayCount(const std::string & lastSeenDate) const244 QVariant UserListModel::lastSeenToTodayDayCount(const std::string &lastSeenDate) const {
245 if (lastSeenDate.empty())
246 return QVariant();
247
248 QVariant count = m_stringToLastSeenToTodayCount.value(u8(lastSeenDate));
249 if (count.isNull()) {
250 QDateTime dt = isoUTCToDateTime(lastSeenDate);
251 if (!dt.isValid()) {
252 // Not convertable to int
253 return QVariant();
254 }
255 count = dt.daysTo(QDateTime::currentDateTime().toUTC());
256 m_stringToLastSeenToTodayCount.insert(u8(lastSeenDate), count);
257 }
258 return count;
259 }
260
pathForChannelId(const int channelId) const261 QString UserListModel::pathForChannelId(const int channelId) const {
262 QString path = m_channelIdToPathMap.value(channelId);
263 if (path.isNull()) {
264 path = QLatin1String("-");
265
266 Channel *channel = Channel::get(channelId);
267 if (channel != NULL) {
268 QStringList pathParts;
269
270 while (channel->cParent != NULL) {
271 pathParts.prepend(channel->qsName);
272 channel = channel->cParent;
273 }
274
275 pathParts.append(QString());
276
277 path = QLatin1String("/ ") + pathParts.join(QLatin1String(" / "));
278 }
279
280 m_channelIdToPathMap.insert(channelId, path);
281 }
282 return path;
283 }
284
isoUTCToDateTime(const std::string & isoTime) const285 QDateTime UserListModel::isoUTCToDateTime(const std::string &isoTime) const {
286 QDateTime dt = QDateTime::fromString(u8(isoTime), Qt::ISODate);
287 dt.setTimeSpec(Qt::UTC);
288 return dt;
289 }
290