blob: f66e7062bdbd4a0c13d3610dbe06d0d18c89d2b4 [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
200 TCHAR workingDirectory[MAX_PATH];
201 GetCurrentDirectory(MAX_PATH, workingDirectory);
202
203 QString ringtonePath = QString::fromWCharArray(workingDirectory);
204 ringtonePath += QStringLiteral("\\ringtones\\default.opus");
205 return ringtonePath;
206#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{
314 QSettings settings("jami.net", "Jami");
315 if (settings.value(SettingsKey::enableNotifications).toBool()) {
316 GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
317 GlobalSystemTray::instance().showMessage(message, "", QIcon(":images/jami.png"));
318 QApplication::alert(widget, delay);
319 }
320}
321
322void
323Utils::showSystemNotification(QWidget *widget,
324 const QString &sender,
325 const QString &message,
326 long delay,
327 const QString &triggeredAccountId)
328{
329 QSettings settings("jami.net", "Jami");
330 if (settings.value(SettingsKey::enableNotifications).toBool()) {
331 GlobalSystemTray::instance().setTriggeredAccountId(triggeredAccountId);
332 GlobalSystemTray::instance().showMessage(sender, message, QIcon(":images/jami.png"));
333 QApplication::alert(widget, delay);
334 }
335}
336
337QSize
338Utils::getRealSize(QScreen *screen)
339{
340#ifdef Q_OS_WIN
341 DEVMODE dmThisScreen;
342 ZeroMemory(&dmThisScreen, sizeof(dmThisScreen));
343 EnumDisplaySettings((const wchar_t *) screen->name().utf16(),
344 ENUM_CURRENT_SETTINGS,
345 (DEVMODE *) &dmThisScreen);
346 return QSize(dmThisScreen.dmPelsWidth, dmThisScreen.dmPelsHeight);
347#else
348 return {};
349#endif
350}
351
352void
353Utils::forceDeleteAsync(const QString &path)
354{
355 /*
356 * Keep deleting file until the process holding it let go,
357 * or the file itself does not exist anymore.
358 */
359 QtConcurrent::run([path] {
360 QFile file(path);
361 if (!QFile::exists(path))
362 return;
363 int retries{0};
364 while (!file.remove() && retries < 5) {
365 qDebug().noquote() << "\n" << file.errorString() << "\n";
366 QThread::msleep(10);
367 ++retries;
368 }
369 });
370}
371
372UtilsAdapter &
373UtilsAdapter::instance()
374{
375 static auto instance = new UtilsAdapter;
376 return *instance;
377}
378
379QString
380Utils::getChangeLog()
381{
382 QString logs;
383 QFile changeLogFile(":/changelog.html");
384 if (!changeLogFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
385 qDebug().noquote() << " Change log file failed to load";
386 return {};
387 }
388 QTextStream in(&changeLogFile);
389 in.setCodec("UTF-8");
390 while (!in.atEnd()) {
391 logs += in.readLine();
392 }
393 return logs;
394}
395
396QString
397Utils::getProjectCredits()
398{
399 QString credits;
400 QFile projectCreditsFile(":/projectcredits.html");
401 if (!projectCreditsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
402 qDebug().noquote() << " Project Credits failed to load";
403 return {};
404 }
405 QTextStream in(&projectCreditsFile);
406 in.setCodec("UTF-8");
407 while (!in.atEnd()) {
408 QString currentLine = in.readLine();
409 if (credits.isEmpty()) {
410 credits += "<h3 align=\"center\" style=\" margin-top:0px; "
411 + QString("margin-bottom:0px; margin-left:0px; margin-right:0px; ")
412 + "-qt-block-indent:0; text-indent:0px;\"><span style=\" font-weight:600;\">"
413 + UtilsAdapter::tr("Created by:") + "</span></h3>";
414 } else if (currentLine.contains("Marianne Forget")) {
415 credits
416 += "<h3 align=\"center\" style=\" margin-top:0px; margin-bottom:0px; "
417 + QString(
418 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">")
419 + "<span style=\" font-weight:600;\">" + UtilsAdapter::tr("Artwork by:")
420 + "</span></h3>";
421 }
422 credits += currentLine;
423 }
424 credits += "<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; "
425 + QString(
426 "margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">")
427 + UtilsAdapter::tr("Based on the SFLPhone project") + "</p>";
428
429 return credits;
430}
431
432void
433Utils::cleanUpdateFiles()
434{
435 /*
436 * Delete all logs and msi in the %TEMP% directory before launching.
437 */
438 QString dir = QString(Utils::WinGetEnv("TEMP"));
439 QDir log_dir(dir, {"jami*.log"});
440 for (const QString &filename : log_dir.entryList()) {
441 log_dir.remove(filename);
442 }
443 QDir msi_dir(dir, {"jami*.msi"});
444 for (const QString &filename : msi_dir.entryList()) {
445 msi_dir.remove(filename);
446 }
447 QDir version_dir(dir, {"version"});
448 for (const QString &filename : version_dir.entryList()) {
449 version_dir.remove(filename);
450 }
451}
452
453void
454Utils::checkForUpdates(bool withUI, QWidget *parent)
455{
456 Q_UNUSED(withUI)
457 Q_UNUSED(parent)
458 /*
459 * TODO: check update logic.
460 */
461}
462
463void
464Utils::applyUpdates(bool updateToBeta, QWidget *parent)
465{
466 Q_UNUSED(updateToBeta)
467 Q_UNUSED(parent)
468 /*
469 * TODO: update logic.
470 */
471}
472
473inline QString
474removeEndlines(const QString &str)
475{
476 QString trimmed(str);
477 trimmed.remove(QChar('\n'));
478 trimmed.remove(QChar('\r'));
479 return trimmed;
480}
481
482QString
483Utils::bestIdForConversation(const lrc::api::conversation::Info &conv,
484 const lrc::api::ConversationModel &model)
485{
486 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
487 if (!contact.registeredName.isEmpty()) {
488 return removeEndlines(contact.registeredName);
489 }
490 return removeEndlines(contact.profileInfo.uri);
491}
492
493QString
494Utils::bestIdForAccount(const lrc::api::account::Info &account)
495{
496 if (!account.registeredName.isEmpty()) {
497 return removeEndlines(account.registeredName);
498 }
499 return removeEndlines(account.profileInfo.uri);
500}
501
502QString
503Utils::bestNameForAccount(const lrc::api::account::Info &account)
504{
505 if (account.profileInfo.alias.isEmpty()) {
506 return bestIdForAccount(account);
507 }
508 return account.profileInfo.alias;
509}
510
511QString
512Utils::bestIdForContact(const lrc::api::contact::Info &contact)
513{
514 if (!contact.registeredName.isEmpty()) {
515 return removeEndlines(contact.registeredName);
516 }
517 return removeEndlines(contact.profileInfo.uri);
518}
519
520QString
521Utils::bestNameForContact(const lrc::api::contact::Info &contact)
522{
523 auto alias = removeEndlines(contact.profileInfo.alias);
524 if (alias.length() == 0) {
525 return bestIdForContact(contact);
526 }
527 return alias;
528}
529
530QString
531Utils::bestNameForConversation(const lrc::api::conversation::Info &conv,
532 const lrc::api::ConversationModel &model)
533{
534 try {
535 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
536 auto alias = removeEndlines(contact.profileInfo.alias);
537 if (alias.length() == 0) {
538 return bestIdForConversation(conv, model);
539 }
540 return alias;
541 } catch (...) {
542 }
543 return {};
544}
545
546/*
547 * Returns empty string if only infoHash is available, second best identifier otherwise.
548 */
549QString
550Utils::secondBestNameForAccount(const lrc::api::account::Info &account)
551{
552 auto alias = removeEndlines(account.profileInfo.alias);
553 auto registeredName = removeEndlines(account.registeredName);
554 auto infoHash = account.profileInfo.uri;
555
556 if (!alias.length() == 0) {
557 /*
558 * If alias exists.
559 */
560 if (!registeredName.length() == 0) {
561 /*
562 * If registeredName exists.
563 */
564 return registeredName;
565 } else {
566 return infoHash;
567 }
568 } else {
569 if (!registeredName.length() == 0) {
570 /*
571 * If registeredName exists.
572 */
573 return infoHash;
574 } else {
575 return "";
576 }
577 }
578}
579
580lrc::api::profile::Type
581Utils::profileType(const lrc::api::conversation::Info &conv,
582 const lrc::api::ConversationModel &model)
583{
584 try {
585 auto contact = model.owner.contactModel->getContact(conv.participants[0]);
586 return contact.profileInfo.type;
587 } catch (...) {
588 return lrc::api::profile::Type::INVALID;
589 }
590}
591
592std::string
593Utils::formatTimeString(const std::time_t &timestamp)
594{
595 std::time_t now = std::time(nullptr);
596 char interactionDay[64];
597 char nowDay[64];
598 std::strftime(interactionDay, sizeof(interactionDay), "%D", std::localtime(&timestamp));
599 std::strftime(nowDay, sizeof(nowDay), "%D", std::localtime(&now));
600 if (std::string(interactionDay) == std::string(nowDay)) {
601 char interactionTime[64];
602 std::strftime(interactionTime, sizeof(interactionTime), "%R", std::localtime(&timestamp));
603 return interactionTime;
604 } else {
605 return interactionDay;
606 }
607}
608
609bool
610Utils::isInteractionGenerated(const lrc::api::interaction::Type &type)
611{
612 return type == lrc::api::interaction::Type::CALL
613 || type == lrc::api::interaction::Type::CONTACT;
614}
615
616bool
617Utils::isContactValid(const QString &contactUid, const lrc::api::ConversationModel &model)
618{
ababi0b686642020-08-18 17:21:28 +0200619 const auto contact = model.owner.contactModel->getContact(contactUid);
Sébastien Blin1f915762020-08-03 13:27:42 -0400620 return (contact.profileInfo.type == lrc::api::profile::Type::PENDING
621 || contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY
622 || contact.profileInfo.type == lrc::api::profile::Type::RING
623 || contact.profileInfo.type == lrc::api::profile::Type::SIP)
624 && !contact.profileInfo.uri.isEmpty();
625}
626
627bool
628Utils::getReplyMessageBox(QWidget *widget, const QString &title, const QString &text)
629{
630 if (QMessageBox::question(widget, title, text, QMessageBox::Yes | QMessageBox::No)
631 == QMessageBox::Yes)
632 return true;
633 return false;
634}
635
636QImage
637Utils::conversationPhoto(const QString &convUid,
638 const lrc::api::account::Info &accountInfo,
639 bool filtered)
640{
ababi0b686642020-08-18 17:21:28 +0200641 auto* convModel = LRCInstance::getCurrentConversationModel();
642 const auto convInfo = convModel->getConversationForUID(convUid);
Sébastien Blin1f915762020-08-03 13:27:42 -0400643 if (!convInfo.uid.isEmpty()) {
644 return GlobalInstances::pixmapManipulator()
645 .decorationRole(convInfo, accountInfo)
646 .value<QImage>();
647 }
648 return QImage();
649}
650
651QColor
652Utils::getAvatarColor(const QString &canonicalUri)
653{
654 if (canonicalUri.isEmpty()) {
655 return JamiAvatarTheme::defaultAvatarColor_;
656 }
657 auto h = QString(
658 QCryptographicHash::hash(canonicalUri.toLocal8Bit(), QCryptographicHash::Md5).toHex());
659 if (h.isEmpty() || h.isNull()) {
660 return JamiAvatarTheme::defaultAvatarColor_;
661 }
662 auto colorIndex = std::string("0123456789abcdef").find(h.at(0).toLatin1());
663 return JamiAvatarTheme::avatarColors_[colorIndex];
664}
665
666/* Generate a QImage representing a dummy user avatar, when user doesn't provide it.
667 * Current rendering is a flat colored circle with a centered letter.
668 * The color of the letter is computed from the circle color to be visible whaterver be the circle color.
669 */
670QImage
671Utils::fallbackAvatar(const QSize size, const QString &canonicalUriStr, const QString &letterStr)
672{
673 /*
674 * We start with a transparent avatar.
675 */
676 QImage avatar(size, QImage::Format_ARGB32);
677 avatar.fill(Qt::transparent);
678
679 /*
680 * We pick a color based on the passed character.
681 */
682 QColor avColor = getAvatarColor(canonicalUriStr);
683
684 /*
685 * We draw a circle with this color.
686 */
687 QPainter painter(&avatar);
688 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
689 painter.setPen(Qt::transparent);
690 painter.setBrush(avColor.lighter(110));
691 painter.drawEllipse(avatar.rect());
692
693 /*
694 * If a letter was passed, then we paint a letter in the circle,
695 * otherwise we draw the default avatar icon.
696 */
697 QString letterStrCleaned(letterStr);
698 letterStrCleaned.remove(QRegExp("[\\n\\t\\r]"));
699 if (!letterStr.isEmpty()) {
700 auto unicode = letterStr.toUcs4().at(0);
701 if (unicode >= 0x1F000 && unicode <= 0x1FFFF) {
702 /*
703 * Is Emoticon.
704 */
705 auto letter = QString::fromUcs4(&unicode, 1);
706 QFont font(QStringLiteral("Segoe UI Emoji"), avatar.height() / 2.66667, QFont::Medium);
707 painter.setFont(font);
708 QRect emojiRect(avatar.rect());
709 emojiRect.moveTop(-6);
710 painter.drawText(emojiRect, letter, QTextOption(Qt::AlignCenter));
711 } else if (unicode >= 0x0000 && unicode <= 0x00FF) {
712 /*
713 * Is Basic Latin.
714 */
715 auto letter = letterStr.at(0).toUpper();
716 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
717 painter.setFont(font);
718 painter.setPen(Qt::white);
719 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
720 } else {
721 auto letter = QString::fromUcs4(&unicode, 1);
722 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
723 painter.setFont(font);
724 painter.setPen(Qt::white);
725 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
726 }
727 } else {
728 QRect overlayRect = avatar.rect();
729 qreal margin = (0.05 * overlayRect.width());
730 overlayRect.moveLeft(overlayRect.left() + margin * 0.5);
731 overlayRect.moveTop(overlayRect.top() + margin * 0.5);
732 overlayRect.setWidth(overlayRect.width() - margin);
733 overlayRect.setHeight(overlayRect.height() - margin);
734 painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
735 }
736
737 return avatar;
738}
739
740QImage
741Utils::fallbackAvatar(const QSize size, const std::string &alias, const std::string &uri)
742{
743 return fallbackAvatar(size, QString::fromStdString(uri), QString::fromStdString(alias));
744}
745
746QByteArray
747Utils::QImageToByteArray(QImage image)
748{
749 QByteArray ba;
750 QBuffer buffer(&ba);
751 buffer.open(QIODevice::WriteOnly);
752 image.save(&buffer, "PNG");
753 return ba;
754}
755
756QImage
757Utils::cropImage(const QImage &img)
758{
759 QRect rect;
760 auto w = img.width();
761 auto h = img.height();
762 if (w > h) {
763 return img.copy({(w - h) / 2, 0, h, h});
764 }
765 return img.copy({0, (h - w) / 2, w, w});
766}
767
768QPixmap
769Utils::pixmapFromSvg(const QString &svg_resource, const QSize &size)
770{
771 QSvgRenderer svgRenderer(svg_resource);
772 QPixmap pixmap(size);
773 pixmap.fill(Qt::transparent);
774 QPainter pixPainter(&pixmap);
775 svgRenderer.render(&pixPainter);
776 return pixmap;
777}
778
779QImage
780Utils::setupQRCode(QString ringID, int margin)
781{
782 auto rcode = QRcode_encodeString(ringID.toStdString().c_str(),
783 0, // Let the version be decided by libqrencode
784 QR_ECLEVEL_L, // Lowest level of error correction
785 QR_MODE_8, // 8-bit data mode
786 1);
787 if (not rcode) {
788 qWarning() << "Failed to generate QR code: " << strerror(errno);
789 return QImage();
790 }
791
792 int qrwidth = rcode->width + margin * 2;
793 QImage result(QSize(qrwidth, qrwidth), QImage::Format_Mono);
794 QPainter painter;
795 painter.begin(&result);
796 painter.setClipRect(QRect(0, 0, qrwidth, qrwidth));
797 painter.setPen(QPen(Qt::black, 0.1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
798 painter.setBrush(Qt::black);
799 painter.fillRect(QRect(0, 0, qrwidth, qrwidth), Qt::white);
800 unsigned char *p;
801 p = rcode->data;
802 for (int y = 0; y < rcode->width; y++) {
803 unsigned char *row = (p + (y * rcode->width));
804 for (int x = 0; x < rcode->width; x++) {
805 if (*(row + x) & 0x1) {
806 painter.drawRect(margin + x, margin + y, 1, 1);
807 }
808 }
809 }
810 painter.end();
811 QRcode_free(rcode);
812 return result;
813}
814
815float
816Utils::getCurrentScalingRatio()
817{
818 return CURRENT_SCALING_RATIO;
819}
820
821void
822Utils::setCurrentScalingRatio(float ratio)
823{
824 CURRENT_SCALING_RATIO = ratio;
825}
826
827QString
828Utils::formattedTime(int duration)
829{
830 if (duration == 0)
831 return {};
832 std::string formattedString;
833 auto minutes = duration / 60;
834 auto seconds = duration % 60;
835 if (minutes > 0) {
836 formattedString += std::to_string(minutes) + ":";
837 if (formattedString.length() == 2) {
838 formattedString = "0" + formattedString;
839 }
840 } else {
841 formattedString += "00:";
842 }
843 if (seconds < 10)
844 formattedString += "0";
845 formattedString += std::to_string(seconds);
846 return QString::fromStdString(formattedString);
847}
848
849QByteArray
850Utils::QByteArrayFromFile(const QString &filename)
851{
852 QFile file(filename);
853 if (file.open(QIODevice::ReadOnly)) {
854 return file.readAll();
855 } else {
856 qDebug() << "can't open file";
857 return QByteArray();
858 }
859}
860
861QPixmap
862Utils::generateTintedPixmap(const QString &filename, QColor color)
863{
864 QPixmap px(filename);
865 QImage tmpImage = px.toImage();
866 for (int y = 0; y < tmpImage.height(); y++) {
867 for (int x = 0; x < tmpImage.width(); x++) {
868 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
869 tmpImage.setPixelColor(x, y, color);
870 }
871 }
872 return QPixmap::fromImage(tmpImage);
873}
874
875QPixmap
876Utils::generateTintedPixmap(const QPixmap &pix, QColor color)
877{
878 QPixmap px = pix;
879 QImage tmpImage = px.toImage();
880 for (int y = 0; y < tmpImage.height(); y++) {
881 for (int x = 0; x < tmpImage.width(); x++) {
882 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
883 tmpImage.setPixelColor(x, y, color);
884 }
885 }
886 return QPixmap::fromImage(tmpImage);
887}
888
889QImage
890Utils::scaleAndFrame(const QImage photo, const QSize &size)
891{
892 return photo.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
893}
894
895QImage
896Utils::accountPhoto(const lrc::api::account::Info &accountInfo, const QSize &size)
897{
898 QImage photo;
899 if (!accountInfo.profileInfo.avatar.isEmpty()) {
900 QByteArray ba = accountInfo.profileInfo.avatar.toLocal8Bit();
901 photo = GlobalInstances::pixmapManipulator().personPhoto(ba, nullptr).value<QImage>();
902 } else {
903 auto bestId = bestIdForAccount(accountInfo);
904 auto bestName = bestNameForAccount(accountInfo);
905 QString letterStr = bestId == bestName ? QString() : bestName;
906 QString prefix = accountInfo.profileInfo.type == lrc::api::profile::Type::RING ? "ring:"
907 : "sip:";
908 photo = fallbackAvatar(size, prefix + accountInfo.profileInfo.uri, letterStr);
909 }
910 return scaleAndFrame(photo, size);
911}
912
913QString
914Utils::humanFileSize(qint64 fileSize)
915{
916 float fileSizeF = static_cast<float>(fileSize);
917 float thresh = 1024;
918
919 if (abs(fileSizeF) < thresh) {
920 return QString::number(fileSizeF) + " B";
921 }
922 QString units[] = {"kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
923 int unit_position = -1;
924 do {
925 fileSizeF /= thresh;
926 ++unit_position;
927 } while (abs(fileSizeF) >= thresh && unit_position < units->size() - 1);
928 /*
929 * Round up to two decimal.
930 */
931 fileSizeF = roundf(fileSizeF * 100) / 100;
932 return QString::number(fileSizeF) + " " + units[unit_position];
933}
934
935const QString
936UtilsAdapter::getBestName(const QString &accountId, const QString &uid)
937{
ababi0b686642020-08-18 17:21:28 +0200938 auto* convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
939 return Utils::bestNameForConversation(convModel->getConversationForUID(uid), *convModel);
Sébastien Blin1f915762020-08-03 13:27:42 -0400940}
941
942const QString
943UtilsAdapter::getBestId(const QString &accountId, const QString &uid)
944{
ababi0b686642020-08-18 17:21:28 +0200945 auto* convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
946 return Utils::bestIdForConversation(convModel->getConversationForUID(uid), *convModel);
Sébastien Blin1f915762020-08-03 13:27:42 -0400947}
948
949int
950UtilsAdapter::getTotalUnreadMessages()
951{
952 int totalUnreadMessages{0};
953 if (LRCInstance::getCurrentAccountInfo().profileInfo.type != lrc::api::profile::Type::SIP) {
ababi0b686642020-08-18 17:21:28 +0200954 auto* convModel = LRCInstance::getCurrentConversationModel();
Sébastien Blin1f915762020-08-03 13:27:42 -0400955 auto ringConversations = convModel->getFilteredConversations(lrc::api::profile::Type::RING);
956 std::for_each(ringConversations.begin(),
957 ringConversations.end(),
ababi0b686642020-08-18 17:21:28 +0200958 [&totalUnreadMessages](const auto &conversation) {
Sébastien Blin1f915762020-08-03 13:27:42 -0400959 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{
Sébastien Blin1f915762020-08-03 13:27:42 -04001011 auto &accInfo = LRCInstance::getAccountInfo(accountId);
ababi0b686642020-08-18 17:21:28 +02001012 const auto convInfo = accInfo.conversationModel->getConversationForUID(convUid);
Sébastien Blin1f915762020-08-03 13:27:42 -04001013 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{
ababi0b686642020-08-18 17:21:28 +02001039 auto &accInfo = LRCInstance::getAccountInfo(accountId);
1040 const auto convInfo = accInfo.conversationModel->getConversationForUID(convUid);
1041
Sébastien Blin1f915762020-08-03 13:27:42 -04001042 if (convInfo.uid.isEmpty()) {
1043 return "";
1044 }
1045
1046 auto call = LRCInstance::getCallInfoForConversation(convInfo, false);
1047 if (!call) {
1048 return "";
1049 }
1050
1051 return call->id;
1052}
1053
1054// returns true if name is valid registered name
1055bool
1056UtilsAdapter::validateRegNameForm(const QString &regName)
1057{
1058 QRegularExpression regExp(" ");
1059
1060 if (regName.size() > 2 && !regName.contains(regExp)) {
1061 return true;
1062
1063 } else {
1064 return false;
1065 }
1066}
1067
1068QString
1069UtilsAdapter::getStringUTF8(QString string)
1070{
1071 return string.toUtf8();
1072}
1073
1074QString
1075UtilsAdapter::getRecordQualityString(int value)
1076{
1077 return value ? QString::number(static_cast<float>(value) / 100, 'f', 1) + " Mbps" : "Default";
1078}
1079
1080QString
1081UtilsAdapter::getCurrentPath()
1082{
1083 return QDir::currentPath();
1084}
agsantos655d8e22020-08-10 17:36:47 -04001085
1086bool
1087UtilsAdapter::checkShowPluginsButton()
1088{
1089 return LRCInstance::pluginModel().getPluginsEnabled()
1090 && (LRCInstance::pluginModel().listLoadedPlugins().size() > 0);
1091}