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