blob: 1556f9da6cf301c92c67621c64193c28e8668ec6 [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{
619 auto contact = model.owner.contactModel->getContact(contactUid);
620 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{
641 auto convInfo = LRCInstance::getConversationFromConvUid(convUid, accountInfo.id, filtered);
642 if (!convInfo.uid.isEmpty()) {
643 return GlobalInstances::pixmapManipulator()
644 .decorationRole(convInfo, accountInfo)
645 .value<QImage>();
646 }
647 return QImage();
648}
649
650QColor
651Utils::getAvatarColor(const QString &canonicalUri)
652{
653 if (canonicalUri.isEmpty()) {
654 return JamiAvatarTheme::defaultAvatarColor_;
655 }
656 auto h = QString(
657 QCryptographicHash::hash(canonicalUri.toLocal8Bit(), QCryptographicHash::Md5).toHex());
658 if (h.isEmpty() || h.isNull()) {
659 return JamiAvatarTheme::defaultAvatarColor_;
660 }
661 auto colorIndex = std::string("0123456789abcdef").find(h.at(0).toLatin1());
662 return JamiAvatarTheme::avatarColors_[colorIndex];
663}
664
665/* Generate a QImage representing a dummy user avatar, when user doesn't provide it.
666 * Current rendering is a flat colored circle with a centered letter.
667 * The color of the letter is computed from the circle color to be visible whaterver be the circle color.
668 */
669QImage
670Utils::fallbackAvatar(const QSize size, const QString &canonicalUriStr, const QString &letterStr)
671{
672 /*
673 * We start with a transparent avatar.
674 */
675 QImage avatar(size, QImage::Format_ARGB32);
676 avatar.fill(Qt::transparent);
677
678 /*
679 * We pick a color based on the passed character.
680 */
681 QColor avColor = getAvatarColor(canonicalUriStr);
682
683 /*
684 * We draw a circle with this color.
685 */
686 QPainter painter(&avatar);
687 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
688 painter.setPen(Qt::transparent);
689 painter.setBrush(avColor.lighter(110));
690 painter.drawEllipse(avatar.rect());
691
692 /*
693 * If a letter was passed, then we paint a letter in the circle,
694 * otherwise we draw the default avatar icon.
695 */
696 QString letterStrCleaned(letterStr);
697 letterStrCleaned.remove(QRegExp("[\\n\\t\\r]"));
698 if (!letterStr.isEmpty()) {
699 auto unicode = letterStr.toUcs4().at(0);
700 if (unicode >= 0x1F000 && unicode <= 0x1FFFF) {
701 /*
702 * Is Emoticon.
703 */
704 auto letter = QString::fromUcs4(&unicode, 1);
705 QFont font(QStringLiteral("Segoe UI Emoji"), avatar.height() / 2.66667, QFont::Medium);
706 painter.setFont(font);
707 QRect emojiRect(avatar.rect());
708 emojiRect.moveTop(-6);
709 painter.drawText(emojiRect, letter, QTextOption(Qt::AlignCenter));
710 } else if (unicode >= 0x0000 && unicode <= 0x00FF) {
711 /*
712 * Is Basic Latin.
713 */
714 auto letter = letterStr.at(0).toUpper();
715 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
716 painter.setFont(font);
717 painter.setPen(Qt::white);
718 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
719 } else {
720 auto letter = QString::fromUcs4(&unicode, 1);
721 QFont font("Arial", avatar.height() / 2.66667, QFont::Medium);
722 painter.setFont(font);
723 painter.setPen(Qt::white);
724 painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
725 }
726 } else {
727 QRect overlayRect = avatar.rect();
728 qreal margin = (0.05 * overlayRect.width());
729 overlayRect.moveLeft(overlayRect.left() + margin * 0.5);
730 overlayRect.moveTop(overlayRect.top() + margin * 0.5);
731 overlayRect.setWidth(overlayRect.width() - margin);
732 overlayRect.setHeight(overlayRect.height() - margin);
733 painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
734 }
735
736 return avatar;
737}
738
739QImage
740Utils::fallbackAvatar(const QSize size, const std::string &alias, const std::string &uri)
741{
742 return fallbackAvatar(size, QString::fromStdString(uri), QString::fromStdString(alias));
743}
744
745QByteArray
746Utils::QImageToByteArray(QImage image)
747{
748 QByteArray ba;
749 QBuffer buffer(&ba);
750 buffer.open(QIODevice::WriteOnly);
751 image.save(&buffer, "PNG");
752 return ba;
753}
754
755QImage
756Utils::cropImage(const QImage &img)
757{
758 QRect rect;
759 auto w = img.width();
760 auto h = img.height();
761 if (w > h) {
762 return img.copy({(w - h) / 2, 0, h, h});
763 }
764 return img.copy({0, (h - w) / 2, w, w});
765}
766
767QPixmap
768Utils::pixmapFromSvg(const QString &svg_resource, const QSize &size)
769{
770 QSvgRenderer svgRenderer(svg_resource);
771 QPixmap pixmap(size);
772 pixmap.fill(Qt::transparent);
773 QPainter pixPainter(&pixmap);
774 svgRenderer.render(&pixPainter);
775 return pixmap;
776}
777
778QImage
779Utils::setupQRCode(QString ringID, int margin)
780{
781 auto rcode = QRcode_encodeString(ringID.toStdString().c_str(),
782 0, // Let the version be decided by libqrencode
783 QR_ECLEVEL_L, // Lowest level of error correction
784 QR_MODE_8, // 8-bit data mode
785 1);
786 if (not rcode) {
787 qWarning() << "Failed to generate QR code: " << strerror(errno);
788 return QImage();
789 }
790
791 int qrwidth = rcode->width + margin * 2;
792 QImage result(QSize(qrwidth, qrwidth), QImage::Format_Mono);
793 QPainter painter;
794 painter.begin(&result);
795 painter.setClipRect(QRect(0, 0, qrwidth, qrwidth));
796 painter.setPen(QPen(Qt::black, 0.1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
797 painter.setBrush(Qt::black);
798 painter.fillRect(QRect(0, 0, qrwidth, qrwidth), Qt::white);
799 unsigned char *p;
800 p = rcode->data;
801 for (int y = 0; y < rcode->width; y++) {
802 unsigned char *row = (p + (y * rcode->width));
803 for (int x = 0; x < rcode->width; x++) {
804 if (*(row + x) & 0x1) {
805 painter.drawRect(margin + x, margin + y, 1, 1);
806 }
807 }
808 }
809 painter.end();
810 QRcode_free(rcode);
811 return result;
812}
813
814float
815Utils::getCurrentScalingRatio()
816{
817 return CURRENT_SCALING_RATIO;
818}
819
820void
821Utils::setCurrentScalingRatio(float ratio)
822{
823 CURRENT_SCALING_RATIO = ratio;
824}
825
826QString
827Utils::formattedTime(int duration)
828{
829 if (duration == 0)
830 return {};
831 std::string formattedString;
832 auto minutes = duration / 60;
833 auto seconds = duration % 60;
834 if (minutes > 0) {
835 formattedString += std::to_string(minutes) + ":";
836 if (formattedString.length() == 2) {
837 formattedString = "0" + formattedString;
838 }
839 } else {
840 formattedString += "00:";
841 }
842 if (seconds < 10)
843 formattedString += "0";
844 formattedString += std::to_string(seconds);
845 return QString::fromStdString(formattedString);
846}
847
848QByteArray
849Utils::QByteArrayFromFile(const QString &filename)
850{
851 QFile file(filename);
852 if (file.open(QIODevice::ReadOnly)) {
853 return file.readAll();
854 } else {
855 qDebug() << "can't open file";
856 return QByteArray();
857 }
858}
859
860QPixmap
861Utils::generateTintedPixmap(const QString &filename, QColor color)
862{
863 QPixmap px(filename);
864 QImage tmpImage = px.toImage();
865 for (int y = 0; y < tmpImage.height(); y++) {
866 for (int x = 0; x < tmpImage.width(); x++) {
867 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
868 tmpImage.setPixelColor(x, y, color);
869 }
870 }
871 return QPixmap::fromImage(tmpImage);
872}
873
874QPixmap
875Utils::generateTintedPixmap(const QPixmap &pix, QColor color)
876{
877 QPixmap px = pix;
878 QImage tmpImage = px.toImage();
879 for (int y = 0; y < tmpImage.height(); y++) {
880 for (int x = 0; x < tmpImage.width(); x++) {
881 color.setAlpha(tmpImage.pixelColor(x, y).alpha());
882 tmpImage.setPixelColor(x, y, color);
883 }
884 }
885 return QPixmap::fromImage(tmpImage);
886}
887
888QImage
889Utils::scaleAndFrame(const QImage photo, const QSize &size)
890{
891 return photo.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
892}
893
894QImage
895Utils::accountPhoto(const lrc::api::account::Info &accountInfo, const QSize &size)
896{
897 QImage photo;
898 if (!accountInfo.profileInfo.avatar.isEmpty()) {
899 QByteArray ba = accountInfo.profileInfo.avatar.toLocal8Bit();
900 photo = GlobalInstances::pixmapManipulator().personPhoto(ba, nullptr).value<QImage>();
901 } else {
902 auto bestId = bestIdForAccount(accountInfo);
903 auto bestName = bestNameForAccount(accountInfo);
904 QString letterStr = bestId == bestName ? QString() : bestName;
905 QString prefix = accountInfo.profileInfo.type == lrc::api::profile::Type::RING ? "ring:"
906 : "sip:";
907 photo = fallbackAvatar(size, prefix + accountInfo.profileInfo.uri, letterStr);
908 }
909 return scaleAndFrame(photo, size);
910}
911
912QString
913Utils::humanFileSize(qint64 fileSize)
914{
915 float fileSizeF = static_cast<float>(fileSize);
916 float thresh = 1024;
917
918 if (abs(fileSizeF) < thresh) {
919 return QString::number(fileSizeF) + " B";
920 }
921 QString units[] = {"kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
922 int unit_position = -1;
923 do {
924 fileSizeF /= thresh;
925 ++unit_position;
926 } while (abs(fileSizeF) >= thresh && unit_position < units->size() - 1);
927 /*
928 * Round up to two decimal.
929 */
930 fileSizeF = roundf(fileSizeF * 100) / 100;
931 return QString::number(fileSizeF) + " " + units[unit_position];
932}
933
934const QString
935UtilsAdapter::getBestName(const QString &accountId, const QString &uid)
936{
937 auto convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
938 return Utils::bestNameForConversation(LRCInstance::getConversationFromConvUid(uid, accountId),
939 *convModel);
940}
941
942const QString
943UtilsAdapter::getBestId(const QString &accountId, const QString &uid)
944{
945 auto convModel = LRCInstance::getAccountInfo(accountId).conversationModel.get();
946 return Utils::bestIdForConversation(LRCInstance::getConversationFromConvUid(uid, accountId),
947 *convModel);
948}
949
950int
951UtilsAdapter::getTotalUnreadMessages()
952{
953 int totalUnreadMessages{0};
954 if (LRCInstance::getCurrentAccountInfo().profileInfo.type != lrc::api::profile::Type::SIP) {
955 auto convModel = LRCInstance::getCurrentConversationModel();
956 auto ringConversations = convModel->getFilteredConversations(lrc::api::profile::Type::RING);
957 std::for_each(ringConversations.begin(),
958 ringConversations.end(),
959 [&totalUnreadMessages, convModel](const auto &conversation) {
960 totalUnreadMessages += conversation.unreadMessages;
961 });
962 }
963 return totalUnreadMessages;
964}
965
966int
967UtilsAdapter::getTotalPendingRequest()
968{
969 auto &accountInfo = LRCInstance::getCurrentAccountInfo();
970 return accountInfo.contactModel->pendingRequestCount();
971}
972
973void
974UtilsAdapter::setConversationFilter(const QString &filter)
975{
976 LRCInstance::getCurrentConversationModel()->setFilter(filter);
977}
978
979void
980UtilsAdapter::clearConversationHistory(const QString &accountId, const QString &uid)
981{
982 LRCInstance::getAccountInfo(accountId).conversationModel->clearHistory(uid);
983}
984
985void
986UtilsAdapter::removeConversation(const QString &accountId, const QString &uid, bool banContact)
987{
988 LRCInstance::getAccountInfo(accountId).conversationModel->removeConversation(uid, banContact);
989}
990
991const QString
992UtilsAdapter::getCurrAccId()
993{
994 return LRCInstance::getCurrAccId();
995}
996
997const QStringList
998UtilsAdapter::getCurrAccList()
999{
1000 return LRCInstance::accountModel().getAccountList();
1001}
1002
1003int
1004UtilsAdapter::getAccountListSize()
1005{
1006 return getCurrAccList().size();
1007}
1008
1009void
1010UtilsAdapter::setCurrentCall(const QString &accountId, const QString &convUid)
1011{
1012 auto convInfo = LRCInstance::getConversationFromConvUid(convUid, accountId);
1013 auto &accInfo = LRCInstance::getAccountInfo(accountId);
1014 accInfo.callModel->setCurrentCall(convInfo.callId);
1015}
1016
1017void
1018UtilsAdapter::startPreviewing(bool force)
1019{
1020 LRCInstance::renderer()->startPreviewing(force);
1021}
1022
1023void
1024UtilsAdapter::stopPreviewing()
1025{
1026 if (!LRCInstance::hasVideoCall()) {
1027 LRCInstance::renderer()->stopPreviewing();
1028 }
1029}
1030
1031bool
1032UtilsAdapter::hasVideoCall()
1033{
1034 return LRCInstance::hasVideoCall();
1035}
1036
1037const QString
1038UtilsAdapter::getCallId(const QString &accountId, const QString &convUid)
1039{
1040 auto convInfo = LRCInstance::getConversationFromConvUid(convUid, accountId);
1041 if (convInfo.uid.isEmpty()) {
1042 return "";
1043 }
1044
1045 auto call = LRCInstance::getCallInfoForConversation(convInfo, false);
1046 if (!call) {
1047 return "";
1048 }
1049
1050 return call->id;
1051}
1052
1053// returns true if name is valid registered name
1054bool
1055UtilsAdapter::validateRegNameForm(const QString &regName)
1056{
1057 QRegularExpression regExp(" ");
1058
1059 if (regName.size() > 2 && !regName.contains(regExp)) {
1060 return true;
1061
1062 } else {
1063 return false;
1064 }
1065}
1066
1067QString
1068UtilsAdapter::getStringUTF8(QString string)
1069{
1070 return string.toUtf8();
1071}
1072
1073QString
1074UtilsAdapter::getRecordQualityString(int value)
1075{
1076 return value ? QString::number(static_cast<float>(value) / 100, 'f', 1) + " Mbps" : "Default";
1077}
1078
1079QString
1080UtilsAdapter::getCurrentPath()
1081{
1082 return QDir::currentPath();
1083}
agsantos655d8e22020-08-10 17:36:47 -04001084
1085bool
1086UtilsAdapter::checkShowPluginsButton()
1087{
1088 return LRCInstance::pluginModel().getPluginsEnabled()
1089 && (LRCInstance::pluginModel().listLoadedPlugins().size() > 0);
1090}