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