blob: ba66fd6b7a737411fe1e84b46aa5462824b081c6 [file] [log] [blame]
Sébastien Blin1f915762020-08-03 13:27:42 -04001/*
2 * Copyright (C) 2015-2020 by Savoir-faire Linux
3 * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
4 * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
5 * Author: Isa Nanic <isa.nanic@savoirfairelinux.com
6 * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
agsantos655d8e22020-08-10 17:36:47 -04007 * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
Sébastien Blin1f915762020-08-03 13:27:42 -04008 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23#include "utils.h"
24
25#ifdef Q_OS_WIN
26#include <lmcons.h>
27#include <shlguid.h>
28#include <shlobj.h>
29#include <shlwapi.h>
30#include <shobjidl.h>
31#include <windows.h>
32#endif
33
34#include "globalsystemtray.h"
35#include "jamiavatartheme.h"
36#include "lrcinstance.h"
37#include "pixbufmanipulator.h"
38#include "version.h"
39
40#include <globalinstances.h>
41#include <qrencode.h>
42
43#include <QApplication>
44#include <QBitmap>
45#include <QErrorMessage>
46#include <QFile>
47#include <QMessageBox>
48#include <QObject>
49#include <QPainter>
50#include <QPropertyAnimation>
51#include <QScreen>
52#include <QStackedWidget>
53#include <QSvgRenderer>
54#include <QTranslator>
55#include <QtConcurrent/QtConcurrent>
56
57bool
agsantos90a1dbc2020-09-01 18:19:22 -040058Utils::CreateStartupLink(const std::wstring& wstrAppName)
Sébastien Blin1f915762020-08-03 13:27:42 -040059{
60#ifdef Q_OS_WIN
61 TCHAR szPath[MAX_PATH];
62 GetModuleFileName(NULL, szPath, MAX_PATH);
63
64 std::wstring programPath(szPath);
65
66 TCHAR startupPath[MAX_PATH];
67 SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
68
69 std::wstring linkPath(startupPath);
70 linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));
71
72 return Utils::CreateLink(programPath.c_str(), linkPath.c_str());
73#else
74 return true;
75#endif
76}
77
78bool
79Utils::CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink)
80{
81#ifdef Q_OS_WIN
82 HRESULT hres;
agsantos90a1dbc2020-09-01 18:19:22 -040083 IShellLink* psl;
Sébastien Blin1f915762020-08-03 13:27:42 -040084
85 hres = CoCreateInstance(CLSID_ShellLink,
86 NULL,
87 CLSCTX_INPROC_SERVER,
88 IID_IShellLink,
agsantos90a1dbc2020-09-01 18:19:22 -040089 (LPVOID*) &psl);
Sébastien Blin1f915762020-08-03 13:27:42 -040090 if (SUCCEEDED(hres)) {
agsantos90a1dbc2020-09-01 18:19:22 -040091 IPersistFile* ppf;
Sébastien Blin1f915762020-08-03 13:27:42 -040092 psl->SetPath(lpszPathObj);
93 psl->SetArguments(TEXT("--minimized"));
94
agsantos90a1dbc2020-09-01 18:19:22 -040095 hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*) &ppf);
Sébastien Blin1f915762020-08-03 13:27:42 -040096 if (SUCCEEDED(hres)) {
97 hres = ppf->Save(lpszPathLink, TRUE);
98 ppf->Release();
99 }
100 psl->Release();
101 }
102 return hres;
103#else
104 Q_UNUSED(lpszPathObj)
105 Q_UNUSED(lpszPathLink)
106 return true;
107#endif
108}
109
110void
agsantos90a1dbc2020-09-01 18:19:22 -0400111Utils::DeleteStartupLink(const std::wstring& wstrAppName)
Sébastien Blin1f915762020-08-03 13:27:42 -0400112{
113#ifdef Q_OS_WIN
114 TCHAR startupPath[MAX_PATH];
115 SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
116
117 std::wstring linkPath(startupPath);
118 linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));
119
120 DeleteFile(linkPath.c_str());
121#endif
122}
123
124bool
agsantos90a1dbc2020-09-01 18:19:22 -0400125Utils::CheckStartupLink(const std::wstring& wstrAppName)
Sébastien Blin1f915762020-08-03 13:27:42 -0400126{
127#ifdef Q_OS_WIN
128 TCHAR startupPath[MAX_PATH];
129 SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);
130
131 std::wstring linkPath(startupPath);
132 linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));
133 return PathFileExists(linkPath.c_str());
134#else
135 return true;
136#endif
137}
138
agsantos90a1dbc2020-09-01 18:19:22 -0400139const char*
140Utils::WinGetEnv(const char* name)
Sébastien Blin1f915762020-08-03 13:27:42 -0400141{
142#ifdef Q_OS_WIN
143 const DWORD buffSize = 65535;
144 static char buffer[buffSize];
145 if (GetEnvironmentVariableA(name, buffer, buffSize)) {
146 return buffer;
147 } else {
148 return 0;
149 }
150#else
151 return 0;
152#endif
153}
154
155void
156Utils::removeOldVersions()
157{
158#ifdef Q_OS_WIN
159 /*
160 * As per: https://git.jami.net/savoirfairelinux/ring-client-windows/issues/429
161 * NB: As only the 64-bit version of this application is distributed, we will only
162 * remove 1. the configuration reg keys for Ring-x64, 2. the startup links for Ring,
163 * 3. the winsparkle reg keys. The NSIS uninstall reg keys for Jami-x64 are removed
164 * by the MSI installer.
165 * Uninstallation of Ring, either 32 or 64 bit, is left to the user.
166 * The current version of Jami will attempt to kill Ring.exe upon start if a startup
167 * link is found.
168 */
169 QString node64 = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node";
170 QString hkcuSoftwareKey = "HKEY_CURRENT_USER\\Software\\";
171 QString uninstKey = "\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\";
172 QString company = "Savoir-Faire Linux";
173
174 /*
175 * 1. Configuration reg keys for Ring-x64.
176 */
177 QSettings(hkcuSoftwareKey + "jami.net\\Ring", QSettings::NativeFormat).remove("");
178 QSettings(hkcuSoftwareKey + "ring.cx", QSettings::NativeFormat).remove("");
Andreas Traczyk84dec082020-09-01 14:31:31 -0400179
Sébastien Blin1f915762020-08-03 13:27:42 -0400180 /*
181 * 2. Unset Ring as a startup application.
182 */
183 if (Utils::CheckStartupLink(TEXT("Ring"))) {
184 qDebug() << "Found startup link for Ring. Removing it and killing Ring.exe.";
185 Utils::DeleteStartupLink(TEXT("Ring"));
Andreas Traczyk84dec082020-09-01 14:31:31 -0400186 QProcess process;
agsantos90a1dbc2020-09-01 18:19:22 -0400187 process.start("taskkill",
188 QStringList() << "/im"
189 << "Ring.exe"
190 << "/f");
Andreas Traczyk84dec082020-09-01 14:31:31 -0400191 process.waitForFinished();
Sébastien Blin1f915762020-08-03 13:27:42 -0400192 }
Andreas Traczyk84dec082020-09-01 14:31:31 -0400193
Sébastien Blin1f915762020-08-03 13:27:42 -0400194 /*
195 * 3. Remove registry entries for winsparkle(both Jami-x64 and Ring-x64).
196 */
197 QSettings(hkcuSoftwareKey + company, QSettings::NativeFormat).remove("");
198#else
199 return;
200#endif
201}
202
203QString
204Utils::GetRingtonePath()
205{
206#ifdef Q_OS_WIN
Ming Rui Zhangfcc2f412020-08-28 15:22:14 -0400207 return QCoreApplication::applicationDirPath() + "\\ringtones\\default.opus";
Sébastien Blin1f915762020-08-03 13:27:42 -0400208#else
209 return QString("/usr/local");
210#endif
211}
212
213QString
214Utils::GenGUID()
215{
216#ifdef Q_OS_WIN
217 GUID gidReference;
agsantos90a1dbc2020-09-01 18:19:22 -0400218 wchar_t* str;
Sébastien Blin1f915762020-08-03 13:27:42 -0400219 HRESULT hCreateGuid = CoCreateGuid(&gidReference);
220 if (hCreateGuid == S_OK) {
221 StringFromCLSID(gidReference, &str);
222 auto gStr = QString::fromWCharArray(str);
223 return gStr.remove("{").remove("}").toLower();
224 } else
225 return QString();
226#else
227 return QString("");
228#endif
229}
230
231QString
232Utils::GetISODate()
233{
234#ifdef Q_OS_WIN
235 SYSTEMTIME lt;
236 GetSystemTime(&lt);
237 return QString("%1-%2-%3T%4:%5:%6Z")
238 .arg(lt.wYear)
239 .arg(lt.wMonth, 2, 10, QChar('0'))
240 .arg(lt.wDay, 2, 10, QChar('0'))
241 .arg(lt.wHour, 2, 10, QChar('0'))
242 .arg(lt.wMinute, 2, 10, QChar('0'))
243 .arg(lt.wSecond, 2, 10, QChar('0'));
244#else
245 return QString();
246#endif
247}
248
249void
agsantos90a1dbc2020-09-01 18:19:22 -0400250Utils::InvokeMailto(const QString& subject, const QString& body, const QString& attachement)
Sébastien Blin1f915762020-08-03 13:27:42 -0400251{
252#ifdef Q_OS_WIN
253 HKEY hKey;
254 LONG lRes = RegOpenKeyExW(HKEY_CLASSES_ROOT, L"mailto", 0, KEY_READ, &hKey);
255 if (lRes != ERROR_FILE_NOT_FOUND) {
256 auto addr = QString("mailto:?subject=%1&body=%2").arg(subject).arg(body);
257 if (not attachement.isEmpty())
258 addr += QString("&attachement=%1").arg(attachement);
259 ShellExecute(nullptr, L"open", addr.toStdWString().c_str(), NULL, NULL, SW_SHOWNORMAL);
260 } else {
261 QErrorMessage errorMessage;
262 errorMessage.showMessage(QObject::tr("No default mail client found"));
263 }
264#endif
265}
266
267QString
agsantos90a1dbc2020-09-01 18:19:22 -0400268Utils::getContactImageString(const QString& accountId, const QString& uid)
Sébastien Blin1f915762020-08-03 13:27:42 -0400269{
270 return QString::fromLatin1(
271 Utils::QImageToByteArray(
272 Utils::conversationPhoto(uid, LRCInstance::getAccountInfo(accountId)))
273 .toBase64()
274 .data());
275}
276
277QImage
278Utils::getCirclePhoto(const QImage original, int sizePhoto)
279{
280 QImage target(sizePhoto, sizePhoto, QImage::Format_ARGB32_Premultiplied);
281 target.fill(Qt::transparent);
282
283 QPainter painter(&target);
284 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
285 painter.setBrush(QBrush(Qt::white));
286 auto scaledPhoto = original
287 .scaled(sizePhoto,
288 sizePhoto,
289 Qt::KeepAspectRatioByExpanding,
290 Qt::SmoothTransformation)
291 .convertToFormat(QImage::Format_ARGB32_Premultiplied);
292 int margin = 0;
293 if (scaledPhoto.width() > sizePhoto) {
294 margin = (scaledPhoto.width() - sizePhoto) / 2;
295 }
296 painter.drawEllipse(0, 0, sizePhoto, sizePhoto);
297 painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
298 painter.drawImage(0, 0, scaledPhoto, margin, 0);
299 return target;
300}
301
302void
agsantos90a1dbc2020-09-01 18:19:22 -0400303Utils::setStackWidget(QStackedWidget* stack, QWidget* widget)
Sébastien Blin1f915762020-08-03 13:27:42 -0400304{
305 if (stack->indexOf(widget) != -1 && stack->currentWidget() != widget) {
306 stack->setCurrentWidget(widget);
307 }
308}
309
310void
agsantos90a1dbc2020-09-01 18:19:22 -0400311Utils::showSystemNotification(QWidget* widget,
312 const QString& message,
Sébastien Blin1f915762020-08-03 13:27:42 -0400313 long delay,
agsantos90a1dbc2020-09-01 18:19:22 -0400314 const QString& triggeredAccountId)
Sébastien Blin1f915762020-08-03 13:27:42 -0400315{
Andreas Traczyk84dec082020-09-01 14:31:31 -0400316 if (!AppSettingsManager::getValue(Settings::Key::EnableNotifications).toBool()) {
317 qWarning() << "Notifications are disabled";
318 return;
Sébastien Blin1f915762020-08-03 13:27:42 -0400319 }
Andreas Traczyk84dec082020-09-01 14:31:31 -0400320 GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
321 GlobalSystemTray::instance().showMessage(message, "", QIcon(":images/jami.png"));
322 QApplication::alert(widget, delay);
Sébastien Blin1f915762020-08-03 13:27:42 -0400323}
324
325void
agsantos90a1dbc2020-09-01 18:19:22 -0400326Utils::showSystemNotification(QWidget* widget,
327 const QString& sender,
328 const QString& message,
Sébastien Blin1f915762020-08-03 13:27:42 -0400329 long delay,
agsantos90a1dbc2020-09-01 18:19:22 -0400330 const QString& triggeredAccountId)
Sébastien Blin1f915762020-08-03 13:27:42 -0400331{
Andreas Traczyk84dec082020-09-01 14:31:31 -0400332 if (!AppSettingsManager::getValue(Settings::Key::EnableNotifications).toBool()) {
333 qWarning() << "Notifications are disabled";
334 return;
Sébastien Blin1f915762020-08-03 13:27:42 -0400335 }
Andreas Traczyk84dec082020-09-01 14:31:31 -0400336 GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
337 GlobalSystemTray::instance().showMessage(sender, message, QIcon(":images/jami.png"));
338 QApplication::alert(widget, delay);
Sébastien Blin1f915762020-08-03 13:27:42 -0400339}
340
341QSize
agsantos90a1dbc2020-09-01 18:19:22 -0400342Utils::getRealSize(QScreen* screen)
Sébastien Blin1f915762020-08-03 13:27:42 -0400343{
344#ifdef Q_OS_WIN
345 DEVMODE dmThisScreen;
346 ZeroMemory(&dmThisScreen, sizeof(dmThisScreen));
agsantos90a1dbc2020-09-01 18:19:22 -0400347 EnumDisplaySettings((const wchar_t*) screen->name().utf16(),
Sébastien Blin1f915762020-08-03 13:27:42 -0400348 ENUM_CURRENT_SETTINGS,
agsantos90a1dbc2020-09-01 18:19:22 -0400349 (DEVMODE*) &dmThisScreen);
Sébastien Blin1f915762020-08-03 13:27:42 -0400350 return QSize(dmThisScreen.dmPelsWidth, dmThisScreen.dmPelsHeight);
351#else
352 return {};
353#endif
354}
355
356void
agsantos90a1dbc2020-09-01 18:19:22 -0400357Utils::forceDeleteAsync(const QString& path)
Sébastien Blin1f915762020-08-03 13:27:42 -0400358{
359 /*
360 * Keep deleting file until the process holding it let go,
361 * or the file itself does not exist anymore.
362 */
363 QtConcurrent::run([path] {
364 QFile file(path);
365 if (!QFile::exists(path))
366 return;
agsantos90a1dbc2020-09-01 18:19:22 -0400367 int retries {0};
Sébastien Blin1f915762020-08-03 13:27:42 -0400368 while (!file.remove() && retries < 5) {
369 qDebug().noquote() << "\n" << file.errorString() << "\n";
370 QThread::msleep(10);
371 ++retries;
372 }
373 });
374}
375
agsantos90a1dbc2020-09-01 18:19:22 -0400376UtilsAdapter&
Sébastien Blin1f915762020-08-03 13:27:42 -0400377UtilsAdapter::instance()
378{
379 static auto instance = new UtilsAdapter;
380 return *instance;
381}
382
383QString
384Utils::getChangeLog()
385{
386 QString logs;
387 QFile changeLogFile(":/changelog.html");
388 if (!changeLogFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
389 qDebug().noquote() << " Change log file failed to load";
390 return {};
391 }
392 QTextStream in(&changeLogFile);
393 in.setCodec("UTF-8");
394 while (!in.atEnd()) {
395 logs += in.readLine();
396 }
397 return logs;
398}
399
400QString
401Utils::getProjectCredits()
402{
403 QString credits;
404 QFile projectCreditsFile(":/projectcredits.html");
405 if (!projectCreditsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
406 qDebug().noquote() << " Project Credits failed to load";
407 return {};
408 }
409 QTextStream in(&projectCreditsFile);
410 in.setCodec("UTF-8");
411 while (!in.atEnd()) {
412 QString currentLine = in.readLine();
413 if (credits.isEmpty()) {
414 credits += "<h3 align=\"center\" style=\" margin-top:0px; "
415 + QString("margin-bottom:0px; margin-left:0px; margin-right:0px; ")
416 + "-qt-block-indent:0; text-indent:0px;\"><span style=\" font-weight:600;\">"
417 + UtilsAdapter::tr("Created by:") + "</span></h3>";
418 } else if (currentLine.contains("Marianne Forget")) {
419 credits
420 += "<h3 align=\"center\" style=\" margin-top:0px; margin-bottom:0px; "
421 + QString(
422 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">")
423 + "<span style=\" font-weight:600;\">" + UtilsAdapter::tr("Artwork by:")
424 + "</span></h3>";
425 }
426 credits += currentLine;
427 }
428 credits += "<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; "
429 + QString(
430 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">")
431 + UtilsAdapter::tr("Based on the SFLPhone project") + "</p>";
432
433 return credits;
434}
435
436void
437Utils::cleanUpdateFiles()
438{
439 /*
440 * Delete all logs and msi in the %TEMP% directory before launching.
441 */
442 QString dir = QString(Utils::WinGetEnv("TEMP"));
443 QDir log_dir(dir, {"jami*.log"});
agsantos90a1dbc2020-09-01 18:19:22 -0400444 for (const QString& filename : log_dir.entryList()) {
Sébastien Blin1f915762020-08-03 13:27:42 -0400445 log_dir.remove(filename);
446 }
447 QDir msi_dir(dir, {"jami*.msi"});
agsantos90a1dbc2020-09-01 18:19:22 -0400448 for (const QString& filename : msi_dir.entryList()) {
Sébastien Blin1f915762020-08-03 13:27:42 -0400449 msi_dir.remove(filename);
450 }
451 QDir version_dir(dir, {"version"});
agsantos90a1dbc2020-09-01 18:19:22 -0400452 for (const QString& filename : version_dir.entryList()) {
Sébastien Blin1f915762020-08-03 13:27:42 -0400453 version_dir.remove(filename);
454 }
455}
456
457void
agsantos90a1dbc2020-09-01 18:19:22 -0400458Utils::checkForUpdates(bool withUI, QWidget* parent)
Sébastien Blin1f915762020-08-03 13:27:42 -0400459{
460 Q_UNUSED(withUI)
461 Q_UNUSED(parent)
462 /*
463 * TODO: check update logic.
464 */
465}
466
467void
agsantos90a1dbc2020-09-01 18:19:22 -0400468Utils::applyUpdates(bool updateToBeta, QWidget* parent)
Sébastien Blin1f915762020-08-03 13:27:42 -0400469{
470 Q_UNUSED(updateToBeta)
471 Q_UNUSED(parent)
472 /*
473 * TODO: update logic.
474 */
475}
476
477inline QString
agsantos90a1dbc2020-09-01 18:19:22 -0400478removeEndlines(const QString& str)
Sébastien Blin1f915762020-08-03 13:27:42 -0400479{
480 QString trimmed(str);
481 trimmed.remove(QChar('\n'));
482 trimmed.remove(QChar('\r'));
483 return trimmed;
484}
485
486QString
agsantos90a1dbc2020-09-01 18:19:22 -0400487Utils::bestIdForConversation(const lrc::api::conversation::Info& conv,
488 const lrc::api::ConversationModel& model)
Sébastien Blin1f915762020-08-03 13:27:42 -0400489{
490 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
491 if (!contact.registeredName.isEmpty()) {
492 return removeEndlines(contact.registeredName);
493 }
494 return removeEndlines(contact.profileInfo.uri);
495}
496
497QString
agsantos90a1dbc2020-09-01 18:19:22 -0400498Utils::bestIdForAccount(const lrc::api::account::Info& account)
Sébastien Blin1f915762020-08-03 13:27:42 -0400499{
500 if (!account.registeredName.isEmpty()) {
501 return removeEndlines(account.registeredName);
502 }
503 return removeEndlines(account.profileInfo.uri);
504}
505
506QString
agsantos90a1dbc2020-09-01 18:19:22 -0400507Utils::bestNameForAccount(const lrc::api::account::Info& account)
Sébastien Blin1f915762020-08-03 13:27:42 -0400508{
509 if (account.profileInfo.alias.isEmpty()) {
510 return bestIdForAccount(account);
511 }
512 return account.profileInfo.alias;
513}
514
515QString
agsantos90a1dbc2020-09-01 18:19:22 -0400516Utils::bestIdForContact(const lrc::api::contact::Info& contact)
Sébastien Blin1f915762020-08-03 13:27:42 -0400517{
518 if (!contact.registeredName.isEmpty()) {
519 return removeEndlines(contact.registeredName);
520 }
521 return removeEndlines(contact.profileInfo.uri);
522}
523
524QString
agsantos90a1dbc2020-09-01 18:19:22 -0400525Utils::bestNameForContact(const lrc::api::contact::Info& contact)
Sébastien Blin1f915762020-08-03 13:27:42 -0400526{
527 auto alias = removeEndlines(contact.profileInfo.alias);
528 if (alias.length() == 0) {
529 return bestIdForContact(contact);
530 }
531 return alias;
532}
533
534QString
agsantos90a1dbc2020-09-01 18:19:22 -0400535Utils::bestNameForConversation(const lrc::api::conversation::Info& conv,
536 const lrc::api::ConversationModel& model)
Sébastien Blin1f915762020-08-03 13:27:42 -0400537{
538 try {
539 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
540 auto alias = removeEndlines(contact.profileInfo.alias);
541 if (alias.length() == 0) {
542 return bestIdForConversation(conv, model);
543 }
544 return alias;
545 } catch (...) {
546 }
547 return {};
548}
549
550/*
551 * Returns empty string if only infoHash is available, second best identifier otherwise.
552 */
553QString
agsantos90a1dbc2020-09-01 18:19:22 -0400554Utils::secondBestNameForAccount(const lrc::api::account::Info& account)
Sébastien Blin1f915762020-08-03 13:27:42 -0400555{
556 auto alias = removeEndlines(account.profileInfo.alias);
557 auto registeredName = removeEndlines(account.registeredName);
558 auto infoHash = account.profileInfo.uri;
559
560 if (!alias.length() == 0) {
561 /*
562 * If alias exists.
563 */
564 if (!registeredName.length() == 0) {
565 /*
566 * If registeredName exists.
567 */
568 return registeredName;
569 } else {
570 return infoHash;
571 }
572 } else {
573 if (!registeredName.length() == 0) {
574 /*
575 * If registeredName exists.
576 */
577 return infoHash;
578 } else {
579 return "";
580 }
581 }
582}
583
584lrc::api::profile::Type
agsantos90a1dbc2020-09-01 18:19:22 -0400585Utils::profileType(const lrc::api::conversation::Info& conv,
586 const lrc::api::ConversationModel& model)
Sébastien Blin1f915762020-08-03 13:27:42 -0400587{
588 try {
589 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
590 return contact.profileInfo.type;
591 } catch (...) {
592 return lrc::api::profile::Type::INVALID;
593 }
594}
595
596std::string
agsantos90a1dbc2020-09-01 18:19:22 -0400597Utils::formatTimeString(const std::time_t& timestamp)
Sébastien Blin1f915762020-08-03 13:27:42 -0400598{
599 std::time_t now = std::time(nullptr);
600 char interactionDay[64];
601 char nowDay[64];
602 std::strftime(interactionDay, sizeof(interactionDay), "%D", std::localtime(&timestamp));
603 std::strftime(nowDay, sizeof(nowDay), "%D", std::localtime(&now));
604 if (std::string(interactionDay) == std::string(nowDay)) {
605 char interactionTime[64];
606 std::strftime(interactionTime, sizeof(interactionTime), "%R", std::localtime(&timestamp));
607 return interactionTime;
608 } else {
609 return interactionDay;
610 }
611}
612
613bool
agsantos90a1dbc2020-09-01 18:19:22 -0400614Utils::isInteractionGenerated(const lrc::api::interaction::Type& type)
Sébastien Blin1f915762020-08-03 13:27:42 -0400615{
616 return type == lrc::api::interaction::Type::CALL
617 || type == lrc::api::interaction::Type::CONTACT;
618}
619
620bool
agsantos90a1dbc2020-09-01 18:19:22 -0400621Utils::isContactValid(const QString& contactUid, const lrc::api::ConversationModel& model)
Sébastien Blin1f915762020-08-03 13:27:42 -0400622{
ababi0b686642020-08-18 17:21:28 +0200623 const auto contact = model.owner.contactModel->getContact(contactUid);
Sébastien Blin1f915762020-08-03 13:27:42 -0400624 return (contact.profileInfo.type == lrc::api::profile::Type::PENDING
625 || contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY
626 || contact.profileInfo.type == lrc::api::profile::Type::RING
627 || contact.profileInfo.type == lrc::api::profile::Type::SIP)
628 && !contact.profileInfo.uri.isEmpty();
629}
630
631bool
agsantos90a1dbc2020-09-01 18:19:22 -0400632Utils::getReplyMessageBox(QWidget* widget, const QString& title, const QString& text)
Sébastien Blin1f915762020-08-03 13:27:42 -0400633{
634 if (QMessageBox::question(widget, title, text, QMessageBox::Yes | QMessageBox::No)
635 == QMessageBox::Yes)
636 return true;
637 return false;
638}
639
640QImage
agsantos90a1dbc2020-09-01 18:19:22 -0400641Utils::conversationPhoto(const QString& convUid,
642 const lrc::api::account::Info& accountInfo,
Sébastien Blin1f915762020-08-03 13:27:42 -0400643 bool filtered)
644{
ababi0b686642020-08-18 17:21:28 +0200645 auto* convModel = LRCInstance::getCurrentConversationModel();
646 const auto convInfo = convModel->getConversationForUID(convUid);
Sébastien Blin1f915762020-08-03 13:27:42 -0400647 if (!convInfo.uid.isEmpty()) {
648 return GlobalInstances::pixmapManipulator()
649 .decorationRole(convInfo, accountInfo)
650 .value<QImage>();
651 }
652 return QImage();
653}
654
655QColor
agsantos90a1dbc2020-09-01 18:19:22 -0400656Utils::getAvatarColor(const QString& canonicalUri)
Sébastien Blin1f915762020-08-03 13:27:42 -0400657{
658 if (canonicalUri.isEmpty()) {
659 return JamiAvatarTheme::defaultAvatarColor_;
660 }
661 auto h = QString(
662 QCryptographicHash::hash(canonicalUri.toLocal8Bit(), QCryptographicHash::Md5).toHex());
663 if (h.isEmpty() || h.isNull()) {
664 return JamiAvatarTheme::defaultAvatarColor_;
665 }
666 auto colorIndex = std::string("0123456789abcdef").find(h.at(0).toLatin1());
667 return JamiAvatarTheme::avatarColors_[colorIndex];
668}
669
670/* Generate a QImage representing a dummy user avatar, when user doesn't provide it.
671 * Current rendering is a flat colored circle with a centered letter.
agsantos90a1dbc2020-09-01 18:19:22 -0400672 * The color of the letter is computed from the circle color to be visible whaterver be the circle
673 * color.
Sébastien Blin1f915762020-08-03 13:27:42 -0400674 */
675QImage
agsantos90a1dbc2020-09-01 18:19:22 -0400676Utils::fallbackAvatar(const QSize size, const QString& canonicalUriStr, const QString& letterStr)
Sébastien Blin1f915762020-08-03 13:27:42 -0400677{
678 /*
679 * We start with a transparent avatar.
680 */
681 QImage avatar(size, QImage::Format_ARGB32);
682 avatar.fill(Qt::transparent);
683
684 /*
685 * We pick a color based on the passed character.
686 */
687 QColor avColor = getAvatarColor(canonicalUriStr);
688
689 /*
690 * We draw a circle with this color.
691 */
692 QPainter painter(&avatar);
693 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
694 painter.setPen(Qt::transparent);
695 painter.setBrush(avColor.lighter(110));
696 painter.drawEllipse(avatar.rect());
697
698 /*
699 * If a letter was passed, then we paint a letter in the circle,
700 * otherwise we draw the default avatar icon.
701 */
702 QString letterStrCleaned(letterStr);
703 letterStrCleaned.remove(QRegExp("[\\n\\t\\r]"));
704 if (!letterStr.isEmpty()) {
705 auto unicode = letterStr.toUcs4().at(0);
706 if (unicode >= 0x1F000 && unicode <= 0x1FFFF) {
707 /*
708 * Is Emoticon.
709 */
710 auto letter = QString::fromUcs4(&unicode, 1);
711 QFont font(QStringLiteral("Segoe UI Emoji"), avatar.height() / 2.66667, QFont::Medium);
712 painter.setFont(font);
713 QRect emojiRect(avatar.rect());
714 emojiRect.moveTop(-6);
715 painter.drawText(emojiRect, letter, QTextOption(Qt::AlignCenter));
716 } else if (unicode >= 0x0000 && unicode <= 0x00FF) {
717 /*
718 * Is Basic Latin.
719 */
720 auto letter = letterStr.at(0).toUpper();
721 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
722 painter.setFont(font);
723 painter.setPen(Qt::white);
724 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
725 } else {
726 auto letter = QString::fromUcs4(&unicode, 1);
727 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
728 painter.setFont(font);
729 painter.setPen(Qt::white);
730 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
731 }
732 } else {
733 QRect overlayRect = avatar.rect();
734 qreal margin = (0.05 * overlayRect.width());
735 overlayRect.moveLeft(overlayRect.left() + margin * 0.5);
736 overlayRect.moveTop(overlayRect.top() + margin * 0.5);
737 overlayRect.setWidth(overlayRect.width() - margin);
738 overlayRect.setHeight(overlayRect.height() - margin);
739 painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
740 }
741
742 return avatar;
743}
744
745QImage
agsantos90a1dbc2020-09-01 18:19:22 -0400746Utils::fallbackAvatar(const QSize size, const std::string& alias, const std::string& uri)
Sébastien Blin1f915762020-08-03 13:27:42 -0400747{
748 return fallbackAvatar(size, QString::fromStdString(uri), QString::fromStdString(alias));
749}
750
751QByteArray
752Utils::QImageToByteArray(QImage image)
753{
754 QByteArray ba;
755 QBuffer buffer(&ba);
756 buffer.open(QIODevice::WriteOnly);
757 image.save(&buffer, "PNG");
758 return ba;
759}
760
761QImage
agsantos90a1dbc2020-09-01 18:19:22 -0400762Utils::cropImage(const QImage& img)
Sébastien Blin1f915762020-08-03 13:27:42 -0400763{
764 QRect rect;
765 auto w = img.width();
766 auto h = img.height();
767 if (w > h) {
768 return img.copy({(w - h) / 2, 0, h, h});
769 }
770 return img.copy({0, (h - w) / 2, w, w});
771}
772
773QPixmap
agsantos90a1dbc2020-09-01 18:19:22 -0400774Utils::pixmapFromSvg(const QString& svg_resource, const QSize& size)
Sébastien Blin1f915762020-08-03 13:27:42 -0400775{
776 QSvgRenderer svgRenderer(svg_resource);
777 QPixmap pixmap(size);
778 pixmap.fill(Qt::transparent);
779 QPainter pixPainter(&pixmap);
780 svgRenderer.render(&pixPainter);
781 return pixmap;
782}
783
784QImage
785Utils::setupQRCode(QString ringID, int margin)
786{
787 auto rcode = QRcode_encodeString(ringID.toStdString().c_str(),
788 0, // Let the version be decided by libqrencode
789 QR_ECLEVEL_L, // Lowest level of error correction
790 QR_MODE_8, // 8-bit data mode
791 1);
792 if (not rcode) {
793 qWarning() << "Failed to generate QR code: " << strerror(errno);
794 return QImage();
795 }
796
797 int qrwidth = rcode->width + margin * 2;
798 QImage result(QSize(qrwidth, qrwidth), QImage::Format_Mono);
799 QPainter painter;
800 painter.begin(&result);
801 painter.setClipRect(QRect(0, 0, qrwidth, qrwidth));
802 painter.setPen(QPen(Qt::black, 0.1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
803 painter.setBrush(Qt::black);
804 painter.fillRect(QRect(0, 0, qrwidth, qrwidth), Qt::white);
agsantos90a1dbc2020-09-01 18:19:22 -0400805 unsigned char* p;
Sébastien Blin1f915762020-08-03 13:27:42 -0400806 p = rcode->data;
807 for (int y = 0; y < rcode->width; y++) {
agsantos90a1dbc2020-09-01 18:19:22 -0400808 unsigned char* row = (p + (y * rcode->width));
Sébastien Blin1f915762020-08-03 13:27:42 -0400809 for (int x = 0; x < rcode->width; x++) {
810 if (*(row + x) & 0x1) {
811 painter.drawRect(margin + x, margin + y, 1, 1);
812 }
813 }
814 }
815 painter.end();
816 QRcode_free(rcode);
817 return result;
818}
819
820float
821Utils::getCurrentScalingRatio()
822{
823 return CURRENT_SCALING_RATIO;
824}
825
826void
827Utils::setCurrentScalingRatio(float ratio)
828{
829 CURRENT_SCALING_RATIO = ratio;
830}
831
832QString
833Utils::formattedTime(int duration)
834{
835 if (duration == 0)
836 return {};
837 std::string formattedString;
838 auto minutes = duration / 60;
839 auto seconds = duration % 60;
840 if (minutes > 0) {
841 formattedString += std::to_string(minutes) + ":";
842 if (formattedString.length() == 2) {
843 formattedString = "0" + formattedString;
844 }
845 } else {
846 formattedString += "00:";
847 }
848 if (seconds < 10)
849 formattedString += "0";
850 formattedString += std::to_string(seconds);
851 return QString::fromStdString(formattedString);
852}
853
854QByteArray
agsantos90a1dbc2020-09-01 18:19:22 -0400855Utils::QByteArrayFromFile(const QString& filename)
Sébastien Blin1f915762020-08-03 13:27:42 -0400856{
857 QFile file(filename);
858 if (file.open(QIODevice::ReadOnly)) {
859 return file.readAll();
860 } else {
861 qDebug() << "can't open file";
862 return QByteArray();
863 }
864}
865
866QPixmap
agsantos90a1dbc2020-09-01 18:19:22 -0400867Utils::generateTintedPixmap(const QString& filename, QColor color)
Sébastien Blin1f915762020-08-03 13:27:42 -0400868{
869 QPixmap px(filename);
870 QImage tmpImage = px.toImage();
871 for (int y = 0; y < tmpImage.height(); y++) {
872 for (int x = 0; x < tmpImage.width(); x++) {
873 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
874 tmpImage.setPixelColor(x, y, color);
875 }
876 }
877 return QPixmap::fromImage(tmpImage);
878}
879
880QPixmap
agsantos90a1dbc2020-09-01 18:19:22 -0400881Utils::generateTintedPixmap(const QPixmap& pix, QColor color)
Sébastien Blin1f915762020-08-03 13:27:42 -0400882{
883 QPixmap px = pix;
884 QImage tmpImage = px.toImage();
885 for (int y = 0; y < tmpImage.height(); y++) {
886 for (int x = 0; x < tmpImage.width(); x++) {
887 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
888 tmpImage.setPixelColor(x, y, color);
889 }
890 }
891 return QPixmap::fromImage(tmpImage);
892}
893
894QImage
agsantos90a1dbc2020-09-01 18:19:22 -0400895Utils::scaleAndFrame(const QImage photo, const QSize& size)
Sébastien Blin1f915762020-08-03 13:27:42 -0400896{
897 return photo.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
898}
899
900QImage
agsantos90a1dbc2020-09-01 18:19:22 -0400901Utils::accountPhoto(const lrc::api::account::Info& accountInfo, const QSize& size)
Sébastien Blin1f915762020-08-03 13:27:42 -0400902{
903 QImage photo;
904 if (!accountInfo.profileInfo.avatar.isEmpty()) {
905 QByteArray ba = accountInfo.profileInfo.avatar.toLocal8Bit();
906 photo = GlobalInstances::pixmapManipulator().personPhoto(ba, nullptr).value<QImage>();
907 } else {
908 auto bestId = bestIdForAccount(accountInfo);
909 auto bestName = bestNameForAccount(accountInfo);
910 QString letterStr = bestId == bestName ? QString() : bestName;
911 QString prefix = accountInfo.profileInfo.type == lrc::api::profile::Type::RING ? "ring:"
912 : "sip:";
913 photo = fallbackAvatar(size, prefix + accountInfo.profileInfo.uri, letterStr);
914 }
915 return scaleAndFrame(photo, size);
916}
917
918QString
919Utils::humanFileSize(qint64 fileSize)
920{
921 float fileSizeF = static_cast<float>(fileSize);
922 float thresh = 1024;
923
924 if (abs(fileSizeF) < thresh) {
925 return QString::number(fileSizeF) + " B";
926 }
927 QString units[] = {"kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
928 int unit_position = -1;
929 do {
930 fileSizeF /= thresh;
931 ++unit_position;
932 } while (abs(fileSizeF) >= thresh && unit_position < units->size() - 1);
933 /*
934 * Round up to two decimal.
935 */
936 fileSizeF = roundf(fileSizeF * 100) / 100;
937 return QString::number(fileSizeF) + " " + units[unit_position];
938}
939
940const QString
agsantos90a1dbc2020-09-01 18:19:22 -0400941UtilsAdapter::getBestName(const QString& accountId, const QString& uid)
Sébastien Blin1f915762020-08-03 13:27:42 -0400942{
ababi0b686642020-08-18 17:21:28 +0200943 auto* convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
944 return Utils::bestNameForConversation(convModel->getConversationForUID(uid), *convModel);
Sébastien Blin1f915762020-08-03 13:27:42 -0400945}
946
947const QString
agsantos90a1dbc2020-09-01 18:19:22 -0400948UtilsAdapter::getBestId(const QString& accountId, const QString& uid)
Sébastien Blin1f915762020-08-03 13:27:42 -0400949{
ababi0b686642020-08-18 17:21:28 +0200950 auto* convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
951 return Utils::bestIdForConversation(convModel->getConversationForUID(uid), *convModel);
Sébastien Blin1f915762020-08-03 13:27:42 -0400952}
953
954int
955UtilsAdapter::getTotalUnreadMessages()
956{
agsantos90a1dbc2020-09-01 18:19:22 -0400957 int totalUnreadMessages {0};
Sébastien Blin1f915762020-08-03 13:27:42 -0400958 if (LRCInstance::getCurrentAccountInfo().profileInfo.type != lrc::api::profile::Type::SIP) {
ababi0b686642020-08-18 17:21:28 +0200959 auto* convModel = LRCInstance::getCurrentConversationModel();
Sébastien Blin1f915762020-08-03 13:27:42 -0400960 auto ringConversations = convModel->getFilteredConversations(lrc::api::profile::Type::RING);
961 std::for_each(ringConversations.begin(),
962 ringConversations.end(),
agsantos90a1dbc2020-09-01 18:19:22 -0400963 [&totalUnreadMessages](const auto& conversation) {
Sébastien Blin1f915762020-08-03 13:27:42 -0400964 totalUnreadMessages += conversation.unreadMessages;
965 });
966 }
967 return totalUnreadMessages;
968}
969
970int
971UtilsAdapter::getTotalPendingRequest()
972{
agsantos90a1dbc2020-09-01 18:19:22 -0400973 auto& accountInfo = LRCInstance::getCurrentAccountInfo();
Sébastien Blin1f915762020-08-03 13:27:42 -0400974 return accountInfo.contactModel->pendingRequestCount();
975}
976
977void
agsantos90a1dbc2020-09-01 18:19:22 -0400978UtilsAdapter::setConversationFilter(const QString& filter)
Sébastien Blin1f915762020-08-03 13:27:42 -0400979{
980 LRCInstance::getCurrentConversationModel()->setFilter(filter);
981}
982
983void
agsantos90a1dbc2020-09-01 18:19:22 -0400984UtilsAdapter::clearConversationHistory(const QString& accountId, const QString& uid)
Sébastien Blin1f915762020-08-03 13:27:42 -0400985{
986 LRCInstance::getAccountInfo(accountId).conversationModel->clearHistory(uid);
987}
988
989void
agsantos90a1dbc2020-09-01 18:19:22 -0400990UtilsAdapter::removeConversation(const QString& accountId, const QString& uid, bool banContact)
Sébastien Blin1f915762020-08-03 13:27:42 -0400991{
992 LRCInstance::getAccountInfo(accountId).conversationModel->removeConversation(uid, banContact);
993}
994
995const QString
996UtilsAdapter::getCurrAccId()
997{
998 return LRCInstance::getCurrAccId();
999}
1000
Sébastien Blin214d9ad2020-08-13 13:05:17 -04001001const QString
1002UtilsAdapter::getCurrConvId()
1003{
1004 return LRCInstance::getCurrentConvUid();
1005}
1006
1007void
1008UtilsAdapter::makePermanentCurrentConv()
1009{
1010 LRCInstance::getCurrentConversationModel()->makePermanent(LRCInstance::getCurrentConvUid());
1011}
1012
Sébastien Blin1f915762020-08-03 13:27:42 -04001013const QStringList
1014UtilsAdapter::getCurrAccList()
1015{
1016 return LRCInstance::accountModel().getAccountList();
1017}
1018
1019int
1020UtilsAdapter::getAccountListSize()
1021{
1022 return getCurrAccList().size();
1023}
1024
1025void
agsantos90a1dbc2020-09-01 18:19:22 -04001026UtilsAdapter::setCurrentCall(const QString& accountId, const QString& convUid)
Sébastien Blin1f915762020-08-03 13:27:42 -04001027{
agsantos90a1dbc2020-09-01 18:19:22 -04001028 auto& accInfo = LRCInstance::getAccountInfo(accountId);
ababi0b686642020-08-18 17:21:28 +02001029 const auto convInfo = accInfo.conversationModel->getConversationForUID(convUid);
Sébastien Blin1f915762020-08-03 13:27:42 -04001030 accInfo.callModel->setCurrentCall(convInfo.callId);
1031}
1032
1033void
1034UtilsAdapter::startPreviewing(bool force)
1035{
1036 LRCInstance::renderer()->startPreviewing(force);
1037}
1038
1039void
1040UtilsAdapter::stopPreviewing()
1041{
1042 if (!LRCInstance::hasVideoCall()) {
1043 LRCInstance::renderer()->stopPreviewing();
1044 }
1045}
1046
1047bool
1048UtilsAdapter::hasVideoCall()
1049{
1050 return LRCInstance::hasVideoCall();
1051}
1052
1053const QString
agsantos90a1dbc2020-09-01 18:19:22 -04001054UtilsAdapter::getCallId(const QString& accountId, const QString& convUid)
Sébastien Blin1f915762020-08-03 13:27:42 -04001055{
agsantos90a1dbc2020-09-01 18:19:22 -04001056 auto& accInfo = LRCInstance::getAccountInfo(accountId);
ababi0b686642020-08-18 17:21:28 +02001057 const auto convInfo = accInfo.conversationModel->getConversationForUID(convUid);
1058
Sébastien Blin1f915762020-08-03 13:27:42 -04001059 if (convInfo.uid.isEmpty()) {
1060 return "";
1061 }
1062
1063 auto call = LRCInstance::getCallInfoForConversation(convInfo, false);
1064 if (!call) {
1065 return "";
1066 }
1067
1068 return call->id;
1069}
1070
ababi76b94aa2020-08-24 17:46:30 +02001071const QString
1072UtilsAdapter::getCallStatusStr(int statusInt)
1073{
1074 const auto status = static_cast<lrc::api::call::Status>(statusInt);
1075 return lrc::api::call::to_string(status);
1076}
1077
Sébastien Blin1f915762020-08-03 13:27:42 -04001078// returns true if name is valid registered name
1079bool
agsantos90a1dbc2020-09-01 18:19:22 -04001080UtilsAdapter::validateRegNameForm(const QString& regName)
Sébastien Blin1f915762020-08-03 13:27:42 -04001081{
1082 QRegularExpression regExp(" ");
1083
1084 if (regName.size() > 2 && !regName.contains(regExp)) {
1085 return true;
1086
1087 } else {
1088 return false;
1089 }
1090}
1091
1092QString
1093UtilsAdapter::getStringUTF8(QString string)
1094{
1095 return string.toUtf8();
1096}
1097
1098QString
1099UtilsAdapter::getRecordQualityString(int value)
1100{
1101 return value ? QString::number(static_cast<float>(value) / 100, 'f', 1) + " Mbps" : "Default";
1102}
1103
1104QString
1105UtilsAdapter::getCurrentPath()
1106{
1107 return QDir::currentPath();
1108}
agsantos655d8e22020-08-10 17:36:47 -04001109
1110bool
1111UtilsAdapter::checkShowPluginsButton()
1112{
1113 return LRCInstance::pluginModel().getPluginsEnabled()
1114 && (LRCInstance::pluginModel().listLoadedPlugins().size() > 0);
Andreas Traczyk84dec082020-09-01 14:31:31 -04001115}
agsantos90a1dbc2020-09-01 18:19:22 -04001116
1117bool
1118UtilsAdapter::isImage(const QString& fileExt) const
1119{
1120 if (fileExt == "png" || fileExt == "jpg" || fileExt == "jpeg")
1121 return true;
1122 return false;
1123}