blob: f54318a5075d90c15e61a9df1d720ad5c841825c [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
58Utils::CreateStartupLink(const std::wstring &wstrAppName)
59{
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;
83 IShellLink *psl;
84
85 hres = CoCreateInstance(CLSID_ShellLink,
86 NULL,
87 CLSCTX_INPROC_SERVER,
88 IID_IShellLink,
89 (LPVOID *) &psl);
90 if (SUCCEEDED(hres)) {
91 IPersistFile *ppf;
92 psl->SetPath(lpszPathObj);
93 psl->SetArguments(TEXT("--minimized"));
94
95 hres = psl->QueryInterface(IID_IPersistFile, (LPVOID *) &ppf);
96 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
111Utils::DeleteStartupLink(const std::wstring &wstrAppName)
112{
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
125Utils::CheckStartupLink(const std::wstring &wstrAppName)
126{
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
139const char *
140Utils::WinGetEnv(const char *name)
141{
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;
187 process.start("taskkill", QStringList()
188 << "/im" << "Ring.exe" << "/f");
189 process.waitForFinished();
Sébastien Blin1f915762020-08-03 13:27:42 -0400190 }
Andreas Traczyk84dec082020-09-01 14:31:31 -0400191
Sébastien Blin1f915762020-08-03 13:27:42 -0400192 /*
193 * 3. Remove registry entries for winsparkle(both Jami-x64 and Ring-x64).
194 */
195 QSettings(hkcuSoftwareKey + company, QSettings::NativeFormat).remove("");
196#else
197 return;
198#endif
199}
200
201QString
202Utils::GetRingtonePath()
203{
204#ifdef Q_OS_WIN
Ming Rui Zhangfcc2f412020-08-28 15:22:14 -0400205 return QCoreApplication::applicationDirPath() + "\\ringtones\\default.opus";
Sébastien Blin1f915762020-08-03 13:27:42 -0400206#else
207 return QString("/usr/local");
208#endif
209}
210
211QString
212Utils::GenGUID()
213{
214#ifdef Q_OS_WIN
215 GUID gidReference;
216 wchar_t *str;
217 HRESULT hCreateGuid = CoCreateGuid(&gidReference);
218 if (hCreateGuid == S_OK) {
219 StringFromCLSID(gidReference, &str);
220 auto gStr = QString::fromWCharArray(str);
221 return gStr.remove("{").remove("}").toLower();
222 } else
223 return QString();
224#else
225 return QString("");
226#endif
227}
228
229QString
230Utils::GetISODate()
231{
232#ifdef Q_OS_WIN
233 SYSTEMTIME lt;
234 GetSystemTime(&lt);
235 return QString("%1-%2-%3T%4:%5:%6Z")
236 .arg(lt.wYear)
237 .arg(lt.wMonth, 2, 10, QChar('0'))
238 .arg(lt.wDay, 2, 10, QChar('0'))
239 .arg(lt.wHour, 2, 10, QChar('0'))
240 .arg(lt.wMinute, 2, 10, QChar('0'))
241 .arg(lt.wSecond, 2, 10, QChar('0'));
242#else
243 return QString();
244#endif
245}
246
247void
248Utils::InvokeMailto(const QString &subject, const QString &body, const QString &attachement)
249{
250#ifdef Q_OS_WIN
251 HKEY hKey;
252 LONG lRes = RegOpenKeyExW(HKEY_CLASSES_ROOT, L"mailto", 0, KEY_READ, &hKey);
253 if (lRes != ERROR_FILE_NOT_FOUND) {
254 auto addr = QString("mailto:?subject=%1&body=%2").arg(subject).arg(body);
255 if (not attachement.isEmpty())
256 addr += QString("&attachement=%1").arg(attachement);
257 ShellExecute(nullptr, L"open", addr.toStdWString().c_str(), NULL, NULL, SW_SHOWNORMAL);
258 } else {
259 QErrorMessage errorMessage;
260 errorMessage.showMessage(QObject::tr("No default mail client found"));
261 }
262#endif
263}
264
265QString
266Utils::getContactImageString(const QString &accountId, const QString &uid)
267{
268 return QString::fromLatin1(
269 Utils::QImageToByteArray(
270 Utils::conversationPhoto(uid, LRCInstance::getAccountInfo(accountId)))
271 .toBase64()
272 .data());
273}
274
275QImage
276Utils::getCirclePhoto(const QImage original, int sizePhoto)
277{
278 QImage target(sizePhoto, sizePhoto, QImage::Format_ARGB32_Premultiplied);
279 target.fill(Qt::transparent);
280
281 QPainter painter(&target);
282 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
283 painter.setBrush(QBrush(Qt::white));
284 auto scaledPhoto = original
285 .scaled(sizePhoto,
286 sizePhoto,
287 Qt::KeepAspectRatioByExpanding,
288 Qt::SmoothTransformation)
289 .convertToFormat(QImage::Format_ARGB32_Premultiplied);
290 int margin = 0;
291 if (scaledPhoto.width() > sizePhoto) {
292 margin = (scaledPhoto.width() - sizePhoto) / 2;
293 }
294 painter.drawEllipse(0, 0, sizePhoto, sizePhoto);
295 painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
296 painter.drawImage(0, 0, scaledPhoto, margin, 0);
297 return target;
298}
299
300void
301Utils::setStackWidget(QStackedWidget *stack, QWidget *widget)
302{
303 if (stack->indexOf(widget) != -1 && stack->currentWidget() != widget) {
304 stack->setCurrentWidget(widget);
305 }
306}
307
308void
309Utils::showSystemNotification(QWidget *widget,
310 const QString &message,
311 long delay,
312 const QString &triggeredAccountId)
313{
Andreas Traczyk84dec082020-09-01 14:31:31 -0400314 if (!AppSettingsManager::getValue(Settings::Key::EnableNotifications).toBool()) {
315 qWarning() << "Notifications are disabled";
316 return;
Sébastien Blin1f915762020-08-03 13:27:42 -0400317 }
Andreas Traczyk84dec082020-09-01 14:31:31 -0400318 GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
319 GlobalSystemTray::instance().showMessage(message, "", QIcon(":images/jami.png"));
320 QApplication::alert(widget, delay);
Sébastien Blin1f915762020-08-03 13:27:42 -0400321}
322
323void
324Utils::showSystemNotification(QWidget *widget,
325 const QString &sender,
326 const QString &message,
327 long delay,
328 const QString &triggeredAccountId)
329{
Andreas Traczyk84dec082020-09-01 14:31:31 -0400330 if (!AppSettingsManager::getValue(Settings::Key::EnableNotifications).toBool()) {
331 qWarning() << "Notifications are disabled";
332 return;
Sébastien Blin1f915762020-08-03 13:27:42 -0400333 }
Andreas Traczyk84dec082020-09-01 14:31:31 -0400334 GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
335 GlobalSystemTray::instance().showMessage(sender, message, QIcon(":images/jami.png"));
336 QApplication::alert(widget, delay);
Sébastien Blin1f915762020-08-03 13:27:42 -0400337}
338
339QSize
340Utils::getRealSize(QScreen *screen)
341{
342#ifdef Q_OS_WIN
343 DEVMODE dmThisScreen;
344 ZeroMemory(&dmThisScreen, sizeof(dmThisScreen));
345 EnumDisplaySettings((const wchar_t *) screen->name().utf16(),
346 ENUM_CURRENT_SETTINGS,
347 (DEVMODE *) &dmThisScreen);
348 return QSize(dmThisScreen.dmPelsWidth, dmThisScreen.dmPelsHeight);
349#else
350 return {};
351#endif
352}
353
354void
355Utils::forceDeleteAsync(const QString &path)
356{
357 /*
358 * Keep deleting file until the process holding it let go,
359 * or the file itself does not exist anymore.
360 */
361 QtConcurrent::run([path] {
362 QFile file(path);
363 if (!QFile::exists(path))
364 return;
365 int retries{0};
366 while (!file.remove() && retries < 5) {
367 qDebug().noquote() << "\n" << file.errorString() << "\n";
368 QThread::msleep(10);
369 ++retries;
370 }
371 });
372}
373
374UtilsAdapter &
375UtilsAdapter::instance()
376{
377 static auto instance = new UtilsAdapter;
378 return *instance;
379}
380
381QString
382Utils::getChangeLog()
383{
384 QString logs;
385 QFile changeLogFile(":/changelog.html");
386 if (!changeLogFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
387 qDebug().noquote() << " Change log file failed to load";
388 return {};
389 }
390 QTextStream in(&changeLogFile);
391 in.setCodec("UTF-8");
392 while (!in.atEnd()) {
393 logs += in.readLine();
394 }
395 return logs;
396}
397
398QString
399Utils::getProjectCredits()
400{
401 QString credits;
402 QFile projectCreditsFile(":/projectcredits.html");
403 if (!projectCreditsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
404 qDebug().noquote() << " Project Credits failed to load";
405 return {};
406 }
407 QTextStream in(&projectCreditsFile);
408 in.setCodec("UTF-8");
409 while (!in.atEnd()) {
410 QString currentLine = in.readLine();
411 if (credits.isEmpty()) {
412 credits += "<h3 align=\"center\" style=\" margin-top:0px; "
413 + QString("margin-bottom:0px; margin-left:0px; margin-right:0px; ")
414 + "-qt-block-indent:0; text-indent:0px;\"><span style=\" font-weight:600;\">"
415 + UtilsAdapter::tr("Created by:") + "</span></h3>";
416 } else if (currentLine.contains("Marianne Forget")) {
417 credits
418 += "<h3 align=\"center\" style=\" margin-top:0px; margin-bottom:0px; "
419 + QString(
420 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">")
421 + "<span style=\" font-weight:600;\">" + UtilsAdapter::tr("Artwork by:")
422 + "</span></h3>";
423 }
424 credits += currentLine;
425 }
426 credits += "<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; "
427 + QString(
428 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">")
429 + UtilsAdapter::tr("Based on the SFLPhone project") + "</p>";
430
431 return credits;
432}
433
434void
435Utils::cleanUpdateFiles()
436{
437 /*
438 * Delete all logs and msi in the %TEMP% directory before launching.
439 */
440 QString dir = QString(Utils::WinGetEnv("TEMP"));
441 QDir log_dir(dir, {"jami*.log"});
442 for (const QString &filename : log_dir.entryList()) {
443 log_dir.remove(filename);
444 }
445 QDir msi_dir(dir, {"jami*.msi"});
446 for (const QString &filename : msi_dir.entryList()) {
447 msi_dir.remove(filename);
448 }
449 QDir version_dir(dir, {"version"});
450 for (const QString &filename : version_dir.entryList()) {
451 version_dir.remove(filename);
452 }
453}
454
455void
456Utils::checkForUpdates(bool withUI, QWidget *parent)
457{
458 Q_UNUSED(withUI)
459 Q_UNUSED(parent)
460 /*
461 * TODO: check update logic.
462 */
463}
464
465void
466Utils::applyUpdates(bool updateToBeta, QWidget *parent)
467{
468 Q_UNUSED(updateToBeta)
469 Q_UNUSED(parent)
470 /*
471 * TODO: update logic.
472 */
473}
474
475inline QString
476removeEndlines(const QString &str)
477{
478 QString trimmed(str);
479 trimmed.remove(QChar('\n'));
480 trimmed.remove(QChar('\r'));
481 return trimmed;
482}
483
484QString
485Utils::bestIdForConversation(const lrc::api::conversation::Info &conv,
486 const lrc::api::ConversationModel &model)
487{
488 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
489 if (!contact.registeredName.isEmpty()) {
490 return removeEndlines(contact.registeredName);
491 }
492 return removeEndlines(contact.profileInfo.uri);
493}
494
495QString
496Utils::bestIdForAccount(const lrc::api::account::Info &account)
497{
498 if (!account.registeredName.isEmpty()) {
499 return removeEndlines(account.registeredName);
500 }
501 return removeEndlines(account.profileInfo.uri);
502}
503
504QString
505Utils::bestNameForAccount(const lrc::api::account::Info &account)
506{
507 if (account.profileInfo.alias.isEmpty()) {
508 return bestIdForAccount(account);
509 }
510 return account.profileInfo.alias;
511}
512
513QString
514Utils::bestIdForContact(const lrc::api::contact::Info &contact)
515{
516 if (!contact.registeredName.isEmpty()) {
517 return removeEndlines(contact.registeredName);
518 }
519 return removeEndlines(contact.profileInfo.uri);
520}
521
522QString
523Utils::bestNameForContact(const lrc::api::contact::Info &contact)
524{
525 auto alias = removeEndlines(contact.profileInfo.alias);
526 if (alias.length() == 0) {
527 return bestIdForContact(contact);
528 }
529 return alias;
530}
531
532QString
533Utils::bestNameForConversation(const lrc::api::conversation::Info &conv,
534 const lrc::api::ConversationModel &model)
535{
536 try {
537 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
538 auto alias = removeEndlines(contact.profileInfo.alias);
539 if (alias.length() == 0) {
540 return bestIdForConversation(conv, model);
541 }
542 return alias;
543 } catch (...) {
544 }
545 return {};
546}
547
548/*
549 * Returns empty string if only infoHash is available, second best identifier otherwise.
550 */
551QString
552Utils::secondBestNameForAccount(const lrc::api::account::Info &account)
553{
554 auto alias = removeEndlines(account.profileInfo.alias);
555 auto registeredName = removeEndlines(account.registeredName);
556 auto infoHash = account.profileInfo.uri;
557
558 if (!alias.length() == 0) {
559 /*
560 * If alias exists.
561 */
562 if (!registeredName.length() == 0) {
563 /*
564 * If registeredName exists.
565 */
566 return registeredName;
567 } else {
568 return infoHash;
569 }
570 } else {
571 if (!registeredName.length() == 0) {
572 /*
573 * If registeredName exists.
574 */
575 return infoHash;
576 } else {
577 return "";
578 }
579 }
580}
581
582lrc::api::profile::Type
583Utils::profileType(const lrc::api::conversation::Info &conv,
584 const lrc::api::ConversationModel &model)
585{
586 try {
587 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
588 return contact.profileInfo.type;
589 } catch (...) {
590 return lrc::api::profile::Type::INVALID;
591 }
592}
593
594std::string
595Utils::formatTimeString(const std::time_t &timestamp)
596{
597 std::time_t now = std::time(nullptr);
598 char interactionDay[64];
599 char nowDay[64];
600 std::strftime(interactionDay, sizeof(interactionDay), "%D", std::localtime(&timestamp));
601 std::strftime(nowDay, sizeof(nowDay), "%D", std::localtime(&now));
602 if (std::string(interactionDay) == std::string(nowDay)) {
603 char interactionTime[64];
604 std::strftime(interactionTime, sizeof(interactionTime), "%R", std::localtime(&timestamp));
605 return interactionTime;
606 } else {
607 return interactionDay;
608 }
609}
610
611bool
612Utils::isInteractionGenerated(const lrc::api::interaction::Type &type)
613{
614 return type == lrc::api::interaction::Type::CALL
615 || type == lrc::api::interaction::Type::CONTACT;
616}
617
618bool
619Utils::isContactValid(const QString &contactUid, const lrc::api::ConversationModel &model)
620{
ababi0b686642020-08-18 17:21:28 +0200621 const auto contact = model.owner.contactModel->getContact(contactUid);
Sébastien Blin1f915762020-08-03 13:27:42 -0400622 return (contact.profileInfo.type == lrc::api::profile::Type::PENDING
623 || contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY
624 || contact.profileInfo.type == lrc::api::profile::Type::RING
625 || contact.profileInfo.type == lrc::api::profile::Type::SIP)
626 && !contact.profileInfo.uri.isEmpty();
627}
628
629bool
630Utils::getReplyMessageBox(QWidget *widget, const QString &title, const QString &text)
631{
632 if (QMessageBox::question(widget, title, text, QMessageBox::Yes | QMessageBox::No)
633 == QMessageBox::Yes)
634 return true;
635 return false;
636}
637
638QImage
639Utils::conversationPhoto(const QString &convUid,
640 const lrc::api::account::Info &accountInfo,
641 bool filtered)
642{
ababi0b686642020-08-18 17:21:28 +0200643 auto* convModel = LRCInstance::getCurrentConversationModel();
644 const auto convInfo = convModel->getConversationForUID(convUid);
Sébastien Blin1f915762020-08-03 13:27:42 -0400645 if (!convInfo.uid.isEmpty()) {
646 return GlobalInstances::pixmapManipulator()
647 .decorationRole(convInfo, accountInfo)
648 .value<QImage>();
649 }
650 return QImage();
651}
652
653QColor
654Utils::getAvatarColor(const QString &canonicalUri)
655{
656 if (canonicalUri.isEmpty()) {
657 return JamiAvatarTheme::defaultAvatarColor_;
658 }
659 auto h = QString(
660 QCryptographicHash::hash(canonicalUri.toLocal8Bit(), QCryptographicHash::Md5).toHex());
661 if (h.isEmpty() || h.isNull()) {
662 return JamiAvatarTheme::defaultAvatarColor_;
663 }
664 auto colorIndex = std::string("0123456789abcdef").find(h.at(0).toLatin1());
665 return JamiAvatarTheme::avatarColors_[colorIndex];
666}
667
668/* Generate a QImage representing a dummy user avatar, when user doesn't provide it.
669 * Current rendering is a flat colored circle with a centered letter.
670 * The color of the letter is computed from the circle color to be visible whaterver be the circle color.
671 */
672QImage
673Utils::fallbackAvatar(const QSize size, const QString &canonicalUriStr, const QString &letterStr)
674{
675 /*
676 * We start with a transparent avatar.
677 */
678 QImage avatar(size, QImage::Format_ARGB32);
679 avatar.fill(Qt::transparent);
680
681 /*
682 * We pick a color based on the passed character.
683 */
684 QColor avColor = getAvatarColor(canonicalUriStr);
685
686 /*
687 * We draw a circle with this color.
688 */
689 QPainter painter(&avatar);
690 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
691 painter.setPen(Qt::transparent);
692 painter.setBrush(avColor.lighter(110));
693 painter.drawEllipse(avatar.rect());
694
695 /*
696 * If a letter was passed, then we paint a letter in the circle,
697 * otherwise we draw the default avatar icon.
698 */
699 QString letterStrCleaned(letterStr);
700 letterStrCleaned.remove(QRegExp("[\\n\\t\\r]"));
701 if (!letterStr.isEmpty()) {
702 auto unicode = letterStr.toUcs4().at(0);
703 if (unicode >= 0x1F000 && unicode <= 0x1FFFF) {
704 /*
705 * Is Emoticon.
706 */
707 auto letter = QString::fromUcs4(&unicode, 1);
708 QFont font(QStringLiteral("Segoe UI Emoji"), avatar.height() / 2.66667, QFont::Medium);
709 painter.setFont(font);
710 QRect emojiRect(avatar.rect());
711 emojiRect.moveTop(-6);
712 painter.drawText(emojiRect, letter, QTextOption(Qt::AlignCenter));
713 } else if (unicode >= 0x0000 && unicode <= 0x00FF) {
714 /*
715 * Is Basic Latin.
716 */
717 auto letter = letterStr.at(0).toUpper();
718 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
719 painter.setFont(font);
720 painter.setPen(Qt::white);
721 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
722 } else {
723 auto letter = QString::fromUcs4(&unicode, 1);
724 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
725 painter.setFont(font);
726 painter.setPen(Qt::white);
727 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
728 }
729 } else {
730 QRect overlayRect = avatar.rect();
731 qreal margin = (0.05 * overlayRect.width());
732 overlayRect.moveLeft(overlayRect.left() + margin * 0.5);
733 overlayRect.moveTop(overlayRect.top() + margin * 0.5);
734 overlayRect.setWidth(overlayRect.width() - margin);
735 overlayRect.setHeight(overlayRect.height() - margin);
736 painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
737 }
738
739 return avatar;
740}
741
742QImage
743Utils::fallbackAvatar(const QSize size, const std::string &alias, const std::string &uri)
744{
745 return fallbackAvatar(size, QString::fromStdString(uri), QString::fromStdString(alias));
746}
747
748QByteArray
749Utils::QImageToByteArray(QImage image)
750{
751 QByteArray ba;
752 QBuffer buffer(&ba);
753 buffer.open(QIODevice::WriteOnly);
754 image.save(&buffer, "PNG");
755 return ba;
756}
757
758QImage
759Utils::cropImage(const QImage &img)
760{
761 QRect rect;
762 auto w = img.width();
763 auto h = img.height();
764 if (w > h) {
765 return img.copy({(w - h) / 2, 0, h, h});
766 }
767 return img.copy({0, (h - w) / 2, w, w});
768}
769
770QPixmap
771Utils::pixmapFromSvg(const QString &svg_resource, const QSize &size)
772{
773 QSvgRenderer svgRenderer(svg_resource);
774 QPixmap pixmap(size);
775 pixmap.fill(Qt::transparent);
776 QPainter pixPainter(&pixmap);
777 svgRenderer.render(&pixPainter);
778 return pixmap;
779}
780
781QImage
782Utils::setupQRCode(QString ringID, int margin)
783{
784 auto rcode = QRcode_encodeString(ringID.toStdString().c_str(),
785 0, // Let the version be decided by libqrencode
786 QR_ECLEVEL_L, // Lowest level of error correction
787 QR_MODE_8, // 8-bit data mode
788 1);
789 if (not rcode) {
790 qWarning() << "Failed to generate QR code: " << strerror(errno);
791 return QImage();
792 }
793
794 int qrwidth = rcode->width + margin * 2;
795 QImage result(QSize(qrwidth, qrwidth), QImage::Format_Mono);
796 QPainter painter;
797 painter.begin(&result);
798 painter.setClipRect(QRect(0, 0, qrwidth, qrwidth));
799 painter.setPen(QPen(Qt::black, 0.1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
800 painter.setBrush(Qt::black);
801 painter.fillRect(QRect(0, 0, qrwidth, qrwidth), Qt::white);
802 unsigned char *p;
803 p = rcode->data;
804 for (int y = 0; y < rcode->width; y++) {
805 unsigned char *row = (p + (y * rcode->width));
806 for (int x = 0; x < rcode->width; x++) {
807 if (*(row + x) & 0x1) {
808 painter.drawRect(margin + x, margin + y, 1, 1);
809 }
810 }
811 }
812 painter.end();
813 QRcode_free(rcode);
814 return result;
815}
816
817float
818Utils::getCurrentScalingRatio()
819{
820 return CURRENT_SCALING_RATIO;
821}
822
823void
824Utils::setCurrentScalingRatio(float ratio)
825{
826 CURRENT_SCALING_RATIO = ratio;
827}
828
829QString
830Utils::formattedTime(int duration)
831{
832 if (duration == 0)
833 return {};
834 std::string formattedString;
835 auto minutes = duration / 60;
836 auto seconds = duration % 60;
837 if (minutes > 0) {
838 formattedString += std::to_string(minutes) + ":";
839 if (formattedString.length() == 2) {
840 formattedString = "0" + formattedString;
841 }
842 } else {
843 formattedString += "00:";
844 }
845 if (seconds < 10)
846 formattedString += "0";
847 formattedString += std::to_string(seconds);
848 return QString::fromStdString(formattedString);
849}
850
851QByteArray
852Utils::QByteArrayFromFile(const QString &filename)
853{
854 QFile file(filename);
855 if (file.open(QIODevice::ReadOnly)) {
856 return file.readAll();
857 } else {
858 qDebug() << "can't open file";
859 return QByteArray();
860 }
861}
862
863QPixmap
864Utils::generateTintedPixmap(const QString &filename, QColor color)
865{
866 QPixmap px(filename);
867 QImage tmpImage = px.toImage();
868 for (int y = 0; y < tmpImage.height(); y++) {
869 for (int x = 0; x < tmpImage.width(); x++) {
870 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
871 tmpImage.setPixelColor(x, y, color);
872 }
873 }
874 return QPixmap::fromImage(tmpImage);
875}
876
877QPixmap
878Utils::generateTintedPixmap(const QPixmap &pix, QColor color)
879{
880 QPixmap px = pix;
881 QImage tmpImage = px.toImage();
882 for (int y = 0; y < tmpImage.height(); y++) {
883 for (int x = 0; x < tmpImage.width(); x++) {
884 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
885 tmpImage.setPixelColor(x, y, color);
886 }
887 }
888 return QPixmap::fromImage(tmpImage);
889}
890
891QImage
892Utils::scaleAndFrame(const QImage photo, const QSize &size)
893{
894 return photo.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
895}
896
897QImage
898Utils::accountPhoto(const lrc::api::account::Info &accountInfo, const QSize &size)
899{
900 QImage photo;
901 if (!accountInfo.profileInfo.avatar.isEmpty()) {
902 QByteArray ba = accountInfo.profileInfo.avatar.toLocal8Bit();
903 photo = GlobalInstances::pixmapManipulator().personPhoto(ba, nullptr).value<QImage>();
904 } else {
905 auto bestId = bestIdForAccount(accountInfo);
906 auto bestName = bestNameForAccount(accountInfo);
907 QString letterStr = bestId == bestName ? QString() : bestName;
908 QString prefix = accountInfo.profileInfo.type == lrc::api::profile::Type::RING ? "ring:"
909 : "sip:";
910 photo = fallbackAvatar(size, prefix + accountInfo.profileInfo.uri, letterStr);
911 }
912 return scaleAndFrame(photo, size);
913}
914
915QString
916Utils::humanFileSize(qint64 fileSize)
917{
918 float fileSizeF = static_cast<float>(fileSize);
919 float thresh = 1024;
920
921 if (abs(fileSizeF) < thresh) {
922 return QString::number(fileSizeF) + " B";
923 }
924 QString units[] = {"kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
925 int unit_position = -1;
926 do {
927 fileSizeF /= thresh;
928 ++unit_position;
929 } while (abs(fileSizeF) >= thresh && unit_position < units->size() - 1);
930 /*
931 * Round up to two decimal.
932 */
933 fileSizeF = roundf(fileSizeF * 100) / 100;
934 return QString::number(fileSizeF) + " " + units[unit_position];
935}
936
937const QString
938UtilsAdapter::getBestName(const QString &accountId, const QString &uid)
939{
ababi0b686642020-08-18 17:21:28 +0200940 auto* convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
941 return Utils::bestNameForConversation(convModel->getConversationForUID(uid), *convModel);
Sébastien Blin1f915762020-08-03 13:27:42 -0400942}
943
944const QString
945UtilsAdapter::getBestId(const QString &accountId, const QString &uid)
946{
ababi0b686642020-08-18 17:21:28 +0200947 auto* convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
948 return Utils::bestIdForConversation(convModel->getConversationForUID(uid), *convModel);
Sébastien Blin1f915762020-08-03 13:27:42 -0400949}
950
951int
952UtilsAdapter::getTotalUnreadMessages()
953{
954 int totalUnreadMessages{0};
955 if (LRCInstance::getCurrentAccountInfo().profileInfo.type != lrc::api::profile::Type::SIP) {
ababi0b686642020-08-18 17:21:28 +0200956 auto* convModel = LRCInstance::getCurrentConversationModel();
Sébastien Blin1f915762020-08-03 13:27:42 -0400957 auto ringConversations = convModel->getFilteredConversations(lrc::api::profile::Type::RING);
958 std::for_each(ringConversations.begin(),
959 ringConversations.end(),
ababi0b686642020-08-18 17:21:28 +0200960 [&totalUnreadMessages](const auto &conversation) {
Sébastien Blin1f915762020-08-03 13:27:42 -0400961 totalUnreadMessages += conversation.unreadMessages;
962 });
963 }
964 return totalUnreadMessages;
965}
966
967int
968UtilsAdapter::getTotalPendingRequest()
969{
970 auto &accountInfo = LRCInstance::getCurrentAccountInfo();
971 return accountInfo.contactModel->pendingRequestCount();
972}
973
974void
975UtilsAdapter::setConversationFilter(const QString &filter)
976{
977 LRCInstance::getCurrentConversationModel()->setFilter(filter);
978}
979
980void
981UtilsAdapter::clearConversationHistory(const QString &accountId, const QString &uid)
982{
983 LRCInstance::getAccountInfo(accountId).conversationModel->clearHistory(uid);
984}
985
986void
987UtilsAdapter::removeConversation(const QString &accountId, const QString &uid, bool banContact)
988{
989 LRCInstance::getAccountInfo(accountId).conversationModel->removeConversation(uid, banContact);
990}
991
992const QString
993UtilsAdapter::getCurrAccId()
994{
995 return LRCInstance::getCurrAccId();
996}
997
Sébastien Blin214d9ad2020-08-13 13:05:17 -0400998const QString
999UtilsAdapter::getCurrConvId()
1000{
1001 return LRCInstance::getCurrentConvUid();
1002}
1003
1004void
1005UtilsAdapter::makePermanentCurrentConv()
1006{
1007 LRCInstance::getCurrentConversationModel()->makePermanent(LRCInstance::getCurrentConvUid());
1008}
1009
Sébastien Blin1f915762020-08-03 13:27:42 -04001010const QStringList
1011UtilsAdapter::getCurrAccList()
1012{
1013 return LRCInstance::accountModel().getAccountList();
1014}
1015
1016int
1017UtilsAdapter::getAccountListSize()
1018{
1019 return getCurrAccList().size();
1020}
1021
1022void
1023UtilsAdapter::setCurrentCall(const QString &accountId, const QString &convUid)
1024{
Sébastien Blin1f915762020-08-03 13:27:42 -04001025 auto &accInfo = LRCInstance::getAccountInfo(accountId);
ababi0b686642020-08-18 17:21:28 +02001026 const auto convInfo = accInfo.conversationModel->getConversationForUID(convUid);
Sébastien Blin1f915762020-08-03 13:27:42 -04001027 accInfo.callModel->setCurrentCall(convInfo.callId);
1028}
1029
1030void
1031UtilsAdapter::startPreviewing(bool force)
1032{
1033 LRCInstance::renderer()->startPreviewing(force);
1034}
1035
1036void
1037UtilsAdapter::stopPreviewing()
1038{
1039 if (!LRCInstance::hasVideoCall()) {
1040 LRCInstance::renderer()->stopPreviewing();
1041 }
1042}
1043
1044bool
1045UtilsAdapter::hasVideoCall()
1046{
1047 return LRCInstance::hasVideoCall();
1048}
1049
1050const QString
1051UtilsAdapter::getCallId(const QString &accountId, const QString &convUid)
1052{
ababi0b686642020-08-18 17:21:28 +02001053 auto &accInfo = LRCInstance::getAccountInfo(accountId);
1054 const auto convInfo = accInfo.conversationModel->getConversationForUID(convUid);
1055
Sébastien Blin1f915762020-08-03 13:27:42 -04001056 if (convInfo.uid.isEmpty()) {
1057 return "";
1058 }
1059
1060 auto call = LRCInstance::getCallInfoForConversation(convInfo, false);
1061 if (!call) {
1062 return "";
1063 }
1064
1065 return call->id;
1066}
1067
ababi76b94aa2020-08-24 17:46:30 +02001068const QString
1069UtilsAdapter::getCallStatusStr(int statusInt)
1070{
1071 const auto status = static_cast<lrc::api::call::Status>(statusInt);
1072 return lrc::api::call::to_string(status);
1073}
1074
1075
Sébastien Blin1f915762020-08-03 13:27:42 -04001076// returns true if name is valid registered name
1077bool
1078UtilsAdapter::validateRegNameForm(const QString &regName)
1079{
1080 QRegularExpression regExp(" ");
1081
1082 if (regName.size() > 2 && !regName.contains(regExp)) {
1083 return true;
1084
1085 } else {
1086 return false;
1087 }
1088}
1089
1090QString
1091UtilsAdapter::getStringUTF8(QString string)
1092{
1093 return string.toUtf8();
1094}
1095
1096QString
1097UtilsAdapter::getRecordQualityString(int value)
1098{
1099 return value ? QString::number(static_cast<float>(value) / 100, 'f', 1) + " Mbps" : "Default";
1100}
1101
1102QString
1103UtilsAdapter::getCurrentPath()
1104{
1105 return QDir::currentPath();
1106}
agsantos655d8e22020-08-10 17:36:47 -04001107
1108bool
1109UtilsAdapter::checkShowPluginsButton()
1110{
1111 return LRCInstance::pluginModel().getPluginsEnabled()
1112 && (LRCInstance::pluginModel().listLoadedPlugins().size() > 0);
Andreas Traczyk84dec082020-09-01 14:31:31 -04001113}