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