diff --git a/Telegram/DeployLinux.sh b/Telegram/DeployLinux.sh index 6b7c5e1dcb..8e3d2b69ac 100755 --- a/Telegram/DeployLinux.sh +++ b/Telegram/DeployLinux.sh @@ -1,5 +1,5 @@ -AppVersionStr=0.6.11 -AppVersion=6011 +AppVersionStr=0.6.12 +AppVersion=6012 if [ ! -f "./../Linux/Release/deploy/$AppVersionStr/tlinuxupd$AppVersion" ]; then echo "tlinuxupd$AppVersion not found!"; diff --git a/Telegram/DeployLinux32.sh b/Telegram/DeployLinux32.sh index a854aed8e7..653b71f940 100755 --- a/Telegram/DeployLinux32.sh +++ b/Telegram/DeployLinux32.sh @@ -1,5 +1,5 @@ -AppVersionStr=0.6.11 -AppVersion=6011 +AppVersionStr=0.6.12 +AppVersion=6012 if [ ! -f "./../Linux/Release/deploy/$AppVersionStr/tlinux32upd$AppVersion" ]; then echo "tlinux32upd$AppVersion not found!" diff --git a/Telegram/DeployMacWin.sh b/Telegram/DeployMacWin.sh index acf1d13112..a62c5b23a1 100755 --- a/Telegram/DeployMacWin.sh +++ b/Telegram/DeployMacWin.sh @@ -1,5 +1,5 @@ -AppVersionStr=0.6.11 -AppVersion=6011 +AppVersionStr=0.6.12 +AppVersion=6012 if [ ! -f "./../Mac/Release/deploy/$AppVersionStr/tmacupd$AppVersion" ]; then echo "tmacupd$AppVersion not found!" diff --git a/Telegram/DeployWin.sh b/Telegram/DeployWin.sh index 490a2257b1..f020dfad1a 100644 --- a/Telegram/DeployWin.sh +++ b/Telegram/DeployWin.sh @@ -1,5 +1,5 @@ -AppVersionStr=0.6.11 -AppVersion=6011 +AppVersionStr=0.6.12 +AppVersion=6012 if [ ! -f "./../Win32/Deploy/deploy/$AppVersionStr/tupdate$AppVersion" ]; then echo "tupdate$AppVersion not found!" diff --git a/Telegram/PrepareLinux.sh b/Telegram/PrepareLinux.sh index 9c459b8b78..c23dec9f78 100755 --- a/Telegram/PrepareLinux.sh +++ b/Telegram/PrepareLinux.sh @@ -1,5 +1,5 @@ -AppVersionStr=0.6.11 -AppVersion=6011 +AppVersionStr=0.6.12 +AppVersion=6012 if [ -d "./../Linux/Release/deploy/$AppVersionStr" ]; then echo "Deploy folder for version $AppVersionStr already exists!" diff --git a/Telegram/PrepareLinux32.sh b/Telegram/PrepareLinux32.sh index 273c89e132..5141fbf8ee 100755 --- a/Telegram/PrepareLinux32.sh +++ b/Telegram/PrepareLinux32.sh @@ -1,5 +1,5 @@ -AppVersionStr=0.6.11 -AppVersion=6011 +AppVersionStr=0.6.12 +AppVersion=6012 if [ -d "./../Linux/Release/deploy/$AppVersionStr" ]; then echo "Deploy folder for version $AppVersionStr already exists!" diff --git a/Telegram/PrepareMac.sh b/Telegram/PrepareMac.sh index 3b29a36192..5bc207d648 100755 --- a/Telegram/PrepareMac.sh +++ b/Telegram/PrepareMac.sh @@ -1,5 +1,5 @@ -AppVersionStr=0.6.11 -AppVersion=6011 +AppVersionStr=0.6.12 +AppVersion=6012 echo "" echo "Preparing version $AppVersionStr.." diff --git a/Telegram/PrepareWin.bat b/Telegram/PrepareWin.bat index 4e508f9d3d..1bebd5525e 100644 --- a/Telegram/PrepareWin.bat +++ b/Telegram/PrepareWin.bat @@ -1,6 +1,6 @@ @echo OFF -set "AppVersionStr=0.6.11" +set "AppVersionStr=0.6.12" echo. echo Preparing version %AppVersionStr%.. echo. diff --git a/Telegram/Resources/lang.txt b/Telegram/Resources/lang.txt index 4b5e00eca1..ed7728c456 100644 --- a/Telegram/Resources/lang.txt +++ b/Telegram/Resources/lang.txt @@ -62,6 +62,7 @@ lng_connecting: "Connecting.."; lng_reconnecting: "Reconnect in %1 s.."; lng_reconnecting_try_now: "Try now"; +lng_status_service_notifications: "service notifications"; lng_status_offline: "last seen a long time ago"; lng_status_recently: "last seen recently"; lng_status_last_week: "last seen within a week"; @@ -219,6 +220,15 @@ lng_download_path_clearing: "Clearing.."; lng_download_path_cleared: "Cleared!"; lng_download_path_clear_failed: "Clear failed :("; +lng_settings_section_cache: "Local storage"; +lng_settings_no_images_cached: "No cached images found!"; +lng_settings_image_cached: "Cached: {count} image, {size}"; +lng_settings_images_cached: "Cached: {count} images, {size}"; +lng_local_images_clear: "Clear All"; +lng_local_images_clearing: "Clearing.."; +lng_local_images_cleared: "Cleared!"; +lng_local_images_clear_failed: "Clear failed :("; + lng_settings_section_advanced: "Advanced"; lng_connection_type: "Connection type:"; lng_connection_auto_connecting: "Default (connecting..)"; diff --git a/Telegram/Setup.iss b/Telegram/Setup.iss index a2e31eca87..7ea4ed9353 100644 --- a/Telegram/Setup.iss +++ b/Telegram/Setup.iss @@ -3,9 +3,9 @@ #define MyAppShortName "Telegram" #define MyAppName "Telegram Desktop" -#define MyAppVersion "0.6.11" -#define MyAppVersionZero "0.6.11" -#define MyAppFullVersion "0.6.11.0" +#define MyAppVersion "0.6.12" +#define MyAppVersionZero "0.6.12" +#define MyAppFullVersion "0.6.12.0" #define MyAppPublisher "Telegram Messenger LLP" #define MyAppURL "https://tdesktop.com" #define MyAppExeName "Telegram.exe" diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index 9db1120c5a..23484b6762 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -26,6 +26,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "mainwidget.h" #include +#include "localstorage.h" + namespace { bool quiting = false; @@ -114,7 +116,7 @@ namespace App { bool loggedOut() { Window *w(wnd()); if (w) { - w->tempDirDelete(); + w->tempDirDelete(Local::ClearManagerAll); w->notifyClearFast(); w->setupIntro(true); } @@ -170,7 +172,7 @@ namespace App { switch (online) { case -2: { QDate yesterday(date(now).date()); - yesterday.addDays(-1); + yesterday.addDays(-3); return int32(QDateTime(yesterday).toTime_t()); } break; @@ -207,7 +209,11 @@ namespace App { return dNow.secsTo(dTomorrow); } - QString onlineText(int32 online, int32 now, bool precise) { + QString onlineText(UserData *user, int32 now, bool precise) { + if (isServiceUser(user->id)) { + return lang(lng_status_service_notifications); + } + int32 online = user->onlineTill; if (online <= 0) { switch (online) { case 0: return lang(lng_status_offline); @@ -337,7 +343,7 @@ namespace App { data->input = MTP_inputPeerForeign(d.vid, d.vaccess_hash); data->inputUser = MTP_inputUserForeign(d.vid, d.vaccess_hash); data->setPhone(qs(d.vphone)); - data->setName(textOneLine(qs(d.vfirst_name)), textOneLine(qs(d.vlast_name)), (data->id != 333000 && !data->phone.isEmpty()) ? formatPhone(data->phone) : QString(), textOneLine(qs(d.vusername))); + data->setName(textOneLine(qs(d.vfirst_name)), textOneLine(qs(d.vlast_name)), (!isServiceUser(data->id) && !data->phone.isEmpty()) ? formatPhone(data->phone) : QString(), textOneLine(qs(d.vusername))); data->setPhoto(d.vphoto); data->access = d.vaccess_hash.v; wasContact = (data->contact > 0); @@ -683,7 +689,7 @@ namespace App { App::main()->removeContact(user); } } - user->setName(textOneLine(user->firstName), textOneLine(user->lastName), (user->contact || user->id == 333000 || user->phone.isEmpty()) ? QString() : App::formatPhone(user->phone), textOneLine(user->username)); + user->setName(textOneLine(user->firstName), textOneLine(user->lastName), (user->contact || isServiceUser(user->id) || user->phone.isEmpty()) ? QString() : App::formatPhone(user->phone), textOneLine(user->username)); if (App::main()) App::main()->peerUpdated(user); } } @@ -1103,7 +1109,6 @@ namespace App { result = new ImageLinkData(imageLink); imageLinksData.insert(imageLink, result); result->type = type; - result->openl = TextLinkPtr(new TextLink(url)); } else { result = i.value(); } @@ -1655,7 +1660,7 @@ namespace App { } QByteArray encrypted(16 + fullSize, Qt::Uninitialized); // 128bit of sha1 - key128, sizeof(data), data hashSha1(toEncrypt.constData(), toEncrypt.size(), encrypted.data()); - aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 16, fullSize, &MTP::localKey(), encrypted.constData()); + aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 16, fullSize, &Local::oldKey(), encrypted.constData()); DEBUG_LOG(("App Info: writing user config file")); QDataStream configStream(&configFile); @@ -1706,7 +1711,7 @@ namespace App { } cSetLocalSalt(salt); - MTP::createLocalKey(QByteArray(), &salt); + Local::createOldKey(&salt); if (data.size() <= 16 || (data.size() & 0x0F)) { LOG(("App Error: bad encrypted part size: %1").arg(data.size())); @@ -1715,7 +1720,7 @@ namespace App { uint32 fullDataLen = data.size() - 16; decrypted.resize(fullDataLen); const char *dataKey = data.constData(), *encrypted = data.constData() + 16; - aesDecryptLocal(encrypted, decrypted.data(), fullDataLen, &MTP::localKey(), dataKey); + aesDecryptLocal(encrypted, decrypted.data(), fullDataLen, &Local::oldKey(), dataKey); uchar sha1Buffer[20]; if (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), dataKey, 16)) { LOG(("App Error: bad decrypt key, data from user-config not decrypted")); @@ -1942,7 +1947,6 @@ namespace App { ::quiting = true; } - QImage readImage(QByteArray data, QByteArray *format) { QByteArray tmpFormat; QImage result; diff --git a/Telegram/SourceFiles/app.h b/Telegram/SourceFiles/app.h index 587656aee5..0b5fa18a1d 100644 --- a/Telegram/SourceFiles/app.h +++ b/Telegram/SourceFiles/app.h @@ -65,7 +65,7 @@ namespace App { int32 onlineForSort(int32 online, int32 now); int32 onlineWillChangeIn(int32 onlineOnServer, int32 nowOnServer); - QString onlineText(int32 onlineOnServer, int32 nowOnServer, bool precise = false); + QString onlineText(UserData *user, int32 nowOnServer, bool precise = false); void feedUsers(const MTPVector &users); void feedChats(const MTPVector &chats); diff --git a/Telegram/SourceFiles/application.cpp b/Telegram/SourceFiles/application.cpp index 2b2306fdb4..c47573876d 100644 --- a/Telegram/SourceFiles/application.cpp +++ b/Telegram/SourceFiles/application.cpp @@ -28,6 +28,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "boxes/confirmbox.h" #include "langloaderplain.h" +#include "localstorage.h" + namespace { Application *mainApp = 0; FileUploader *uploader = 0; @@ -131,6 +133,7 @@ Application::Application(int &argc, char **argv) : PsApplication(argc, argv), } } + Local::start(); style::startManager(); anim::startManager(); historyInit(); @@ -617,9 +620,11 @@ void Application::socketError(QLocalSocket::LocalSocketError e) { } void Application::startApp() { + Local::ReadMapState state = Local::readMap(QByteArray()); + App::readUserConfig(); - if (!MTP::localKey().created()) { - MTP::createLocalKey(QByteArray()); + if (!Local::oldKey().created()) { + Local::createOldKey(); cSetNeedConfigResave(true); } if (cNeedConfigResave()) { @@ -765,6 +770,7 @@ Application::~Application() { delete window; style::stopManager(); + Local::stop(); } Application *Application::app() { diff --git a/Telegram/SourceFiles/application.h b/Telegram/SourceFiles/application.h index 05266a4218..84f8051a58 100644 --- a/Telegram/SourceFiles/application.h +++ b/Telegram/SourceFiles/application.h @@ -80,6 +80,8 @@ signals: void peerPhotoDone(PeerId peer); void peerPhotoFail(PeerId peer); + void adjustSingleTimers(); + public slots: void startUpdateCheck(bool forceWait = false); diff --git a/Telegram/SourceFiles/boxes/addparticipantbox.cpp b/Telegram/SourceFiles/boxes/addparticipantbox.cpp index c1e02dffe3..9dd608367d 100644 --- a/Telegram/SourceFiles/boxes/addparticipantbox.cpp +++ b/Telegram/SourceFiles/boxes/addparticipantbox.cpp @@ -118,7 +118,7 @@ AddParticipantInner::ContactData *AddParticipantInner::contactData(DialogRow *ro data->inchat = _chat->participants.constFind(user) != _chat->participants.cend(); data->check = false; data->name.setText(st::profileListNameFont, user->name, _textNameOptions); - data->online = App::onlineText(user->onlineTill, _time); + data->online = App::onlineText(user, _time); } else { data = i.value(); } diff --git a/Telegram/SourceFiles/boxes/contactsbox.cpp b/Telegram/SourceFiles/boxes/contactsbox.cpp index f7a31bfce0..c98c83c4e7 100644 --- a/Telegram/SourceFiles/boxes/contactsbox.cpp +++ b/Telegram/SourceFiles/boxes/contactsbox.cpp @@ -98,7 +98,7 @@ ContactsInner::ContactData *ContactsInner::contactData(DialogRow *row) { if (i == _contactsData.cend()) { _contactsData.insert(user, data = new ContactData()); data->name.setText(st::profileListNameFont, user->name, _textNameOptions); - data->online = App::onlineText(user->onlineTill, _time); + data->online = App::onlineText(user, _time); } else { data = i.value(); } diff --git a/Telegram/SourceFiles/boxes/newgroupbox.cpp b/Telegram/SourceFiles/boxes/newgroupbox.cpp index 72c68509c0..ad6674371c 100644 --- a/Telegram/SourceFiles/boxes/newgroupbox.cpp +++ b/Telegram/SourceFiles/boxes/newgroupbox.cpp @@ -99,7 +99,7 @@ NewGroupInner::ContactData *NewGroupInner::contactData(DialogRow *row) { _contactsData.insert(user, data = new ContactData()); data->check = false; data->name.setText(st::profileListNameFont, user->name, _textNameOptions); - data->online = App::onlineText(user->onlineTill, _time); + data->online = App::onlineText(user, _time); } else { data = i.value(); } diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index 6fa2f9959d..66b74e607d 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -17,8 +17,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com */ #pragma once -static const int32 AppVersion = 6011; -static const wchar_t *AppVersionStr = L"0.6.11"; +static const int32 AppVersion = 6012; +static const wchar_t *AppVersionStr = L"0.6.12"; static const wchar_t *AppNameOld = L"Telegram Win (Unofficial)"; static const wchar_t *AppName = L"Telegram Desktop"; @@ -101,8 +101,16 @@ enum { MaxMessageSize = 4096, MaxHttpRedirects = 5, // when getting external data/images + + WriteMapTimeout = 1000, + SaveDraftTimeout = 1000, // save draft after 1 secs of not changing text + SaveDraftAnywayTimeout = 5000, // or save anyway each 5 secs }; +inline bool isServiceUser(uint64 id) { + return (id == 333000) || (id == 777000); +} + #ifdef Q_OS_WIN inline const GUID &cGUID() { static const GUID gGuid = { 0x87a94ab0, 0xe370, 0x4cde, { 0x98, 0xd3, 0xac, 0xc1, 0x10, 0xc5, 0x96, 0x7d } }; diff --git a/Telegram/SourceFiles/gui/images.cpp b/Telegram/SourceFiles/gui/images.cpp index 59fb5d235d..f2f6a8e834 100644 --- a/Telegram/SourceFiles/gui/images.cpp +++ b/Telegram/SourceFiles/gui/images.cpp @@ -29,18 +29,9 @@ namespace { return img; } - typedef QMap StorageImages; + typedef QMap StorageImages; StorageImages storageImages; - QByteArray storageKey(int32 dc, const int64 &volume, int32 local, const int64 &secret) { - QByteArray result(24, Qt::Uninitialized); - memcpy(result.data(), &dc, 4); - memcpy(result.data() + 4, &volume, 8); - memcpy(result.data() + 12, &local, 4); - memcpy(result.data() + 16, &secret, 8); - return result; - } - int64 globalAquiredSize = 0; } @@ -480,7 +471,7 @@ bool StorageImage::loaded() const { } StorageImage *getImage(int32 width, int32 height, int32 dc, const int64 &volume, int32 local, const int64 &secret, int32 size) { - QByteArray key(storageKey(dc, volume, local, secret)); + StorageKey key(storageKey(dc, volume, local)); StorageImages::const_iterator i = storageImages.constFind(key); if (i == storageImages.cend()) { i = storageImages.insert(key, new StorageImage(width, height, dc, volume, local, secret, size)); @@ -489,7 +480,7 @@ StorageImage *getImage(int32 width, int32 height, int32 dc, const int64 &volume, } StorageImage *getImage(int32 width, int32 height, int32 dc, const int64 &volume, int32 local, const int64 &secret, const QByteArray &bytes) { - QByteArray key(storageKey(dc, volume, local, secret)); + StorageKey key(storageKey(dc, volume, local)); StorageImages::const_iterator i = storageImages.constFind(key); if (i == storageImages.cend()) { QByteArray bytesArr(bytes); diff --git a/Telegram/SourceFiles/gui/images.h b/Telegram/SourceFiles/gui/images.h index edddac9ad1..4d5c275576 100644 --- a/Telegram/SourceFiles/gui/images.h +++ b/Telegram/SourceFiles/gui/images.h @@ -53,6 +53,13 @@ public: void forget() const; void restore() const; + QByteArray savedFormat() const { + return format; + } + QByteArray savedData() const { + return saved; + } + virtual ~Image() { invalidateSizeCache(); } @@ -106,6 +113,24 @@ private: LocalImage *getImage(const QString &file); LocalImage *getImage(const QPixmap &pixmap, QByteArray format); +typedef QPair StorageKey; +inline uint64 storageMix32To64(int32 a, int32 b) { + return (uint64(*reinterpret_cast(&a)) << 32) | uint64(*reinterpret_cast(&b)); +} +inline StorageKey storageKey(int32 dc, const int64 &volume, int32 local) { + return StorageKey(storageMix32To64(dc, local), volume); +} +inline StorageKey storageKey(const MTPDfileLocation &location) { + return storageKey(location.vdc_id.v, location.vvolume_id.v, location.vlocal_id.v); +} +struct StorageImageSaved { + StorageImageSaved() : type(mtpc_storage_fileUnknown) { + } + StorageImageSaved(mtpTypeId type, const QByteArray &data) : type(type), data(data) { + } + mtpTypeId type; + QByteArray data; +}; class StorageImage : public Image { public: @@ -123,7 +148,7 @@ public: void load(bool loadFirst = false, bool prior = true) { if (loader) { loader->start(loadFirst, prior); - check(); + if (loader) check(); } } void checkload() const { @@ -131,7 +156,7 @@ public: if (!loader->loading()) { loader->start(true); } - check(); + if (loader) check(); } } diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 516a071954..3018a97219 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -27,6 +27,7 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "gui/filedialog.h" #include "audio.h" +#include "localstorage.h" TextParseOptions _textNameOptions = { 0, // flags @@ -2132,6 +2133,10 @@ const QString HistoryPhoto::inDialogsText() const { return lang(lng_in_dlg_photo); } +const QString HistoryPhoto::inHistoryText() const { + return qsl("[ ") + lang(lng_in_dlg_photo) + qsl(" ]"); +} + bool HistoryPhoto::hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width) const { if (width < 0) width = w; return (x >= 0 && y >= 0 && x < width && y < _height); @@ -2149,6 +2154,40 @@ HistoryMedia *HistoryPhoto::clone() const { return new HistoryPhoto(*this); } +void HistoryPhoto::updateFrom(const MTPMessageMedia &media) { + if (media.type() == mtpc_messageMediaPhoto) { + const MTPPhoto &photo(media.c_messageMediaPhoto().vphoto); + if (photo.type() == mtpc_photo) { + const QVector &sizes(photo.c_photo().vsizes.c_vector().v); + for (QVector::const_iterator i = sizes.cbegin(), e = sizes.cend(); i != e; ++i) { + char size = 0; + const MTPFileLocation *loc = 0; + switch (i->type()) { + case mtpc_photoSize: { + const string &s(i->c_photoSize().vtype.c_string().v); + loc = &i->c_photoSize().vlocation; + if (s.size()) size = s[0]; + } break; + + case mtpc_photoCachedSize: { + const string &s(i->c_photoCachedSize().vtype.c_string().v); + loc = &i->c_photoSize().vlocation; + if (s.size()) size = s[0]; + } break; + } + if (!loc || loc->type() != mtpc_fileLocation) continue; + if (size == 's') { + Local::writeImage(storageKey(loc->c_fileLocation()), data->thumb); + } else if (size == 'm') { + Local::writeImage(storageKey(loc->c_fileLocation()), data->medium); + } else if (size == 'x') { + Local::writeImage(storageKey(loc->c_fileLocation()), data->full); + } + } + } + } +} + void HistoryPhoto::draw(QPainter &p, const HistoryItem *parent, bool selected, int32 width) const { if (width < 0) width = w; data->full->load(false, false); @@ -2314,6 +2353,10 @@ const QString HistoryVideo::inDialogsText() const { return lang(lng_in_dlg_video); } +const QString HistoryVideo::inHistoryText() const { + return qsl("[ ") + lang(lng_in_dlg_video) + qsl(" ]"); +} + bool HistoryVideo::hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width) const { if (width < 0) width = w; if (width >= _maxw) { @@ -2611,6 +2654,10 @@ const QString HistoryAudio::inDialogsText() const { return lang(lng_in_dlg_audio); } +const QString HistoryAudio::inHistoryText() const { + return qsl("[ ") + lang(lng_in_dlg_audio) + qsl(" ]"); +} + bool HistoryAudio::hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width) const { if (width < 0) width = w; if (width >= _maxw) { @@ -2862,6 +2909,10 @@ const QString HistoryDocument::inDialogsText() const { return data->name.isEmpty() ? lang(lng_in_dlg_document) : data->name; } +const QString HistoryDocument::inHistoryText() const { + return qsl("[ ") + lang(lng_in_dlg_document) + (data->name.isEmpty() ? QString() : (qsl(" : ") + data->name)) + qsl(" ]"); +} + bool HistoryDocument::hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width) const { if (width < 0) width = w; if (width >= _maxw) { @@ -2962,6 +3013,10 @@ const QString HistoryContact::inDialogsText() const { return lang(lng_in_dlg_contact); } +const QString HistoryContact::inHistoryText() const { + return qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" : ") + name.original(0, 0xFFFF, false) + qsl(", ") + phone + qsl(" ]"); +} + bool HistoryContact::hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width) const { if (width < 0) width = w; return (x >= 0 && y <= 0 && x < w && y < _height); @@ -3060,7 +3115,7 @@ void HistoryContact::updateFrom(const MTPMessageMedia &media) { } namespace { - QRegularExpression reYouTube1(qsl("^(https?://)?(www\\.)?youtube\\.com/watch\\?v=([a-z0-9_-]+)(&|$)"), QRegularExpression::CaseInsensitiveOption); + QRegularExpression reYouTube1(qsl("^(https?://)?(www\\.|m\\.)?youtube\\.com/watch\\?([^#]+&)?v=([a-z0-9_-]+)(&|$)"), QRegularExpression::CaseInsensitiveOption); QRegularExpression reYouTube2(qsl("^(https?://)?(www\\.)?youtu\\.be/([a-z0-9_-]+)(\\?|$)"), QRegularExpression::CaseInsensitiveOption); QRegularExpression reInstagram(qsl("^(https?://)?(www\\.)?instagram\\.com/p/([a-z0-9_-]+)(/|$)"), QRegularExpression::CaseInsensitiveOption); @@ -3373,15 +3428,23 @@ void ImageLinkData::load() { HistoryImageLink::HistoryImageLink(const QString &url, int32 width) : HistoryMedia(width) { if (url.startsWith(qsl("location:"))) { - data = App::imageLink(url, GoogleMapsLink, qsl("https://maps.google.com/maps?q=") + url.mid(9) + qsl("&ll=") + url.mid(9) + qsl("&z=17")); + QString lnk = qsl("https://maps.google.com/maps?q=") + url.mid(9) + qsl("&ll=") + url.mid(9) + qsl("&z=17"); + link.reset(new TextLink(lnk)); + data = App::imageLink(url, GoogleMapsLink, lnk); } else { + int matchIndex = 4; QRegularExpressionMatch m = reYouTube1.match(url); - if (!m.hasMatch()) m = reYouTube2.match(url); + if (!m.hasMatch()) { + m = reYouTube2.match(url); + matchIndex = 3; + } if (m.hasMatch()) { - data = App::imageLink(qsl("youtube:") + m.captured(3), YouTubeLink, url); + link.reset(new TextLink(url)); + data = App::imageLink(qsl("youtube:") + m.captured(matchIndex), YouTubeLink, url); } else { m = reInstagram.match(url); if (m.hasMatch()) { + link.reset(new TextLink(url)); data = App::imageLink(qsl("instagram:") + m.captured(3), InstagramLink, url); data->title = qsl("instagram.com/p/") + m.captured(3); } else { @@ -3552,6 +3615,17 @@ const QString HistoryImageLink::inDialogsText() const { return QString(); } +const QString HistoryImageLink::inHistoryText() const { + if (data) { + switch (data->type) { + case YouTubeLink: return qsl("[ YouTube Video : ") + link->text() + qsl(" ]"); + case InstagramLink: return qsl("[ Instagram Link : ") + link->text() + qsl(" ]"); + case GoogleMapsLink: return qsl("[ ") + lang(lng_maps_point) + qsl(" : ") + link->text() + qsl(" ]"); + } + } + return qsl("[ Link : ") + link->text() + qsl(" ]"); +} + bool HistoryImageLink::hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width) const { if (width < 0) width = w; return (x >= 0 && y >= 0 && x < width && y < _height); @@ -3560,7 +3634,7 @@ bool HistoryImageLink::hasPoint(int32 x, int32 y, const HistoryItem *parent, int TextLinkPtr HistoryImageLink::getLink(int32 x, int32 y, const HistoryItem *parent, int32 width) const { if (width < 0) width = w; if (x >= 0 && y >= 0 && x < width && y < _height && data) { - return data->openl; + return link; } return TextLinkPtr(); } @@ -3705,7 +3779,7 @@ bool HistoryMessage::uploading() const { QString HistoryMessage::selectedText(uint32 selection) const { if (_media && selection == FullItemSel) { - return _text.original(0, 0xFFFF) + '[' + _media->inDialogsText() + ']'; + return _text.original(0, 0xFFFF) + _media->inHistoryText(); } uint16 selectedFrom = (selection == FullItemSel) ? 0 : (selection >> 16) & 0xFFFF; uint16 selectedTo = (selection == FullItemSel) ? 0xFFFF : (selection & 0xFFFF); diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index 0675e8c61e..ff268a74a7 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -629,6 +629,34 @@ inline MTPMessagesFilter typeToMediaFilter(MediaOverviewType &type) { return MTPMessagesFilter(); } +struct MessageCursor { + MessageCursor() : position(0), anchor(0), scroll(QFIXED_MAX) { + } + MessageCursor(int position, int anchor, int scroll) : position(position), anchor(anchor), scroll(scroll) { + } + MessageCursor(const QTextEdit &edit) { + fillFrom(edit); + } + void fillFrom(const QTextEdit &edit) { + QTextCursor c = edit.textCursor(); + position = c.position(); + anchor = c.anchor(); + QScrollBar *s = edit.verticalScrollBar(); + scroll = s ? s->value() : QFIXED_MAX; + } + void applyTo(QTextEdit &edit, bool *lock = 0) { + if (lock) *lock = true; + QTextCursor c = edit.textCursor(); + c.setPosition(anchor, QTextCursor::MoveAnchor); + c.setPosition(position, QTextCursor::KeepAnchor); + edit.setTextCursor(c); + QScrollBar *s = edit.verticalScrollBar(); + if (s) s->setValue(scroll); + if (lock) *lock = false; + } + int position, anchor, scroll; +}; + class HistoryMedia; class HistoryMessage; class HistoryUnreadBar; @@ -732,7 +760,7 @@ struct History : public QList { } QString draft; - QTextCursor draftCur; + MessageCursor draftCursor; int32 lastWidth, lastScrollTop; bool mute; @@ -1223,6 +1251,7 @@ public: virtual HistoryMediaType type() const = 0; virtual const QString inDialogsText() const = 0; + virtual const QString inHistoryText() const = 0; virtual bool hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const = 0; virtual int32 countHeight(const HistoryItem *parent, int32 width = -1) const { return height(); @@ -1276,6 +1305,7 @@ public: return MediaTypePhoto; } const QString inDialogsText() const; + const QString inHistoryText() const; bool hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; TextLinkPtr getLink(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; HistoryMedia *clone() const; @@ -1284,6 +1314,8 @@ public: return data; } + void updateFrom(const MTPMessageMedia &media); + TextLinkPtr lnk() const { return openl; } @@ -1312,6 +1344,7 @@ public: return MediaTypeVideo; } const QString inDialogsText() const; + const QString inHistoryText() const; bool hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; TextLinkPtr getLink(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; bool uploading() const { @@ -1344,6 +1377,7 @@ public: return MediaTypeAudio; } const QString inDialogsText() const; + const QString inHistoryText() const; bool hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; TextLinkPtr getLink(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; bool uploading() const { @@ -1376,6 +1410,7 @@ public: return MediaTypeDocument; } const QString inDialogsText() const; + const QString inHistoryText() const; bool hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; int32 countHeight(const HistoryItem *parent, int32 width = -1) const; bool uploading() const { @@ -1418,6 +1453,7 @@ public: return MediaTypeContact; } const QString inDialogsText() const; + const QString inHistoryText() const; bool hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width) const; TextLinkPtr getLink(int32 x, int32 y, const HistoryItem *parent, int32 width) const; HistoryMedia *clone() const; @@ -1426,7 +1462,7 @@ public: private: int32 userId; - int32 w, phonew; + int32 phonew; Text name; QString phone; UserData *contact; @@ -1449,7 +1485,6 @@ struct ImageLinkData { QString id; QString title, duration; ImagePtr thumb; - TextLinkPtr openl; ImageLinkType type; bool loading; @@ -1498,12 +1533,14 @@ public: return MediaTypeImageLink; } const QString inDialogsText() const; + const QString inHistoryText() const; bool hasPoint(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; TextLinkPtr getLink(int32 x, int32 y, const HistoryItem *parent, int32 width = -1) const; HistoryMedia *clone() const; private: ImageLinkData *data; + TextLinkPtr link; }; diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 587cc88590..45be7fde4f 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -28,6 +28,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "fileuploader.h" #include "supporttl.h" +#include "localstorage.h" + // flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html HistoryList::HistoryList(HistoryWidget *historyWidget, ScrollArea *scroll, History *history) : QWidget(0) @@ -633,106 +635,98 @@ void HistoryList::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } _contextMenuLnk = textlnkOver(); - if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { + PhotoLink *lnkPhoto = dynamic_cast(_contextMenuLnk.data()); + VideoLink *lnkVideo = dynamic_cast(_contextMenuLnk.data()); + AudioLink *lnkAudio = dynamic_cast(_contextMenuLnk.data()); + DocumentLink *lnkDocument = dynamic_cast(_contextMenuLnk.data()); + if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument) { _menu = new ContextMenu(historyWidget); if (isUponSelected > 0) { _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); } - _menu->addAction(lang(lng_context_open_link), this, SLOT(openContextUrl()))->setEnabled(true); - _menu->addAction(lang(lng_context_copy_link), this, SLOT(copyContextUrl()))->setEnabled(true); - } else if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { - _menu = new ContextMenu(historyWidget); - if (isUponSelected > 0) { - _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); - } - _menu->addAction(lang(lng_context_open_email), this, SLOT(openContextUrl()))->setEnabled(true); - _menu->addAction(lang(lng_context_copy_email), this, SLOT(copyContextUrl()))->setEnabled(true); - } else if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { - _menu = new ContextMenu(historyWidget); - if (isUponSelected > 0) { - _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); - } - _menu->addAction(lang(lng_context_open_hashtag), this, SLOT(openContextUrl()))->setEnabled(true); - _menu->addAction(lang(lng_context_copy_hashtag), this, SLOT(copyContextUrl()))->setEnabled(true); - } else { - PhotoLink *lnkPhoto = dynamic_cast(_contextMenuLnk.data()); - VideoLink *lnkVideo = dynamic_cast(_contextMenuLnk.data()); - AudioLink *lnkAudio = dynamic_cast(_contextMenuLnk.data()); - DocumentLink *lnkDocument = dynamic_cast(_contextMenuLnk.data()); - if (lnkPhoto || lnkVideo || lnkAudio || lnkDocument) { - _menu = new ContextMenu(historyWidget); - if (isUponSelected > 0) { - _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); - } - if (lnkPhoto) { - _menu->addAction(lang(lng_context_open_image), this, SLOT(openContextUrl()))->setEnabled(true); - _menu->addAction(lang(lng_context_save_image), this, SLOT(saveContextImage()))->setEnabled(true); - _menu->addAction(lang(lng_context_copy_image), this, SLOT(copyContextImage()))->setEnabled(true); + if (lnkPhoto) { + _menu->addAction(lang(lng_context_open_image), this, SLOT(openContextUrl()))->setEnabled(true); + _menu->addAction(lang(lng_context_save_image), this, SLOT(saveContextImage()))->setEnabled(true); + _menu->addAction(lang(lng_context_copy_image), this, SLOT(copyContextImage()))->setEnabled(true); + } else { + if ((lnkVideo && lnkVideo->video()->loader) || (lnkAudio && lnkAudio->audio()->loader) || (lnkDocument && lnkDocument->document()->loader)) { + _menu->addAction(lang(lng_context_cancel_download), this, SLOT(cancelContextDownload()))->setEnabled(true); } else { - if ((lnkVideo && lnkVideo->video()->loader) || (lnkAudio && lnkAudio->audio()->loader) || (lnkDocument && lnkDocument->document()->loader)) { - _menu->addAction(lang(lng_context_cancel_download), this, SLOT(cancelContextDownload()))->setEnabled(true); - } else { - if ((lnkVideo && !lnkVideo->video()->already(true).isEmpty()) || (lnkAudio && !lnkAudio->audio()->already(true).isEmpty()) || (lnkDocument && !lnkDocument->document()->already(true).isEmpty())) { - _menu->addAction(lang(cPlatform() == dbipMac ? lng_context_show_in_finder : lng_context_show_in_folder), this, SLOT(showContextInFolder()))->setEnabled(true); - } - _menu->addAction(lang(lnkVideo ? lng_context_open_video : (lnkAudio ? lng_context_open_audio : lng_context_open_document)), this, SLOT(openContextFile()))->setEnabled(true); - _menu->addAction(lang(lnkVideo ? lng_context_save_video : (lnkAudio ? lng_context_save_audio : lng_context_save_document)), this, SLOT(saveContextFile()))->setEnabled(true); + if ((lnkVideo && !lnkVideo->video()->already(true).isEmpty()) || (lnkAudio && !lnkAudio->audio()->already(true).isEmpty()) || (lnkDocument && !lnkDocument->document()->already(true).isEmpty())) { + _menu->addAction(lang(cPlatform() == dbipMac ? lng_context_show_in_finder : lng_context_show_in_folder), this, SLOT(showContextInFolder()))->setEnabled(true); } + _menu->addAction(lang(lnkVideo ? lng_context_open_video : (lnkAudio ? lng_context_open_audio : lng_context_open_document)), this, SLOT(openContextFile()))->setEnabled(true); + _menu->addAction(lang(lnkVideo ? lng_context_save_video : (lnkAudio ? lng_context_save_audio : lng_context_save_document)), this, SLOT(saveContextFile()))->setEnabled(true); } - if (isUponSelected > 1) { - _menu->addAction(lang(lng_context_forward_selected), historyWidget, SLOT(onForwardSelected())); - _menu->addAction(lang(lng_context_delete_selected), historyWidget, SLOT(onDeleteSelected())); - _menu->addAction(lang(lng_context_clear_selection), historyWidget, SLOT(onClearSelected())); - } else if (App::hoveredLinkItem()) { - if (isUponSelected != -2) { - if (dynamic_cast(App::hoveredLinkItem())) { - _menu->addAction(lang(lng_context_forward_msg), historyWidget, SLOT(forwardMessage()))->setEnabled(true); - } - _menu->addAction(lang(lng_context_delete_msg), historyWidget, SLOT(deleteMessage()))->setEnabled(true); - } - _menu->addAction(lang(lng_context_select_msg), historyWidget, SLOT(selectMessage()))->setEnabled(true); - App::contextItem(App::hoveredLinkItem()); - } - } else { // maybe cursor on some text history item? - HistoryItem *item = App::hoveredItem() ? App::hoveredItem() : App::hoveredLinkItem(); - bool canDelete = (item && item->itemType() == HistoryItem::MsgType); - bool canForward = canDelete && (item->id > 0) && !item->serviceMsg(); - - HistoryMessage *msg = dynamic_cast(item); - HistoryServiceMsg *srv = dynamic_cast(item); - - if (isUponSelected > 0) { - if (!_menu) _menu = new ContextMenu(this); - _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); - } else if (item && !isUponSelected && !_contextMenuLnk) { - QString contextMenuText = item->selectedText(FullItemSel); - if (!contextMenuText.isEmpty()) { - if (!_menu) _menu = new ContextMenu(this); - _menu->addAction(lang(lng_context_copy_text), this, SLOT(copyContextText()))->setEnabled(true); - } - } - - if (isUponSelected > 1) { - if (!_menu) _menu = new ContextMenu(this); - _menu->addAction(lang(lng_context_forward_selected), historyWidget, SLOT(onForwardSelected())); - _menu->addAction(lang(lng_context_delete_selected), historyWidget, SLOT(onDeleteSelected())); - _menu->addAction(lang(lng_context_clear_selection), historyWidget, SLOT(onClearSelected())); - } else { - if (!_menu) _menu = new ContextMenu(this); - if (isUponSelected != -2) { - if (canForward) { - _menu->addAction(lang(lng_context_forward_msg), historyWidget, SLOT(forwardMessage()))->setEnabled(true); - } - - if (canDelete) { - _menu->addAction(lang((msg && msg->uploading()) ? lng_context_cancel_upload : lng_context_delete_msg), historyWidget, SLOT(deleteMessage()))->setEnabled(true); - } - } - _menu->addAction(lang(lng_context_select_msg), historyWidget, SLOT(selectMessage()))->setEnabled(true); - } - App::contextItem(item); } + if (isUponSelected > 1) { + _menu->addAction(lang(lng_context_forward_selected), historyWidget, SLOT(onForwardSelected())); + _menu->addAction(lang(lng_context_delete_selected), historyWidget, SLOT(onDeleteSelected())); + _menu->addAction(lang(lng_context_clear_selection), historyWidget, SLOT(onClearSelected())); + } else if (App::hoveredLinkItem()) { + if (isUponSelected != -2) { + if (dynamic_cast(App::hoveredLinkItem())) { + _menu->addAction(lang(lng_context_forward_msg), historyWidget, SLOT(forwardMessage()))->setEnabled(true); + } + _menu->addAction(lang(lng_context_delete_msg), historyWidget, SLOT(deleteMessage()))->setEnabled(true); + } + _menu->addAction(lang(lng_context_select_msg), historyWidget, SLOT(selectMessage()))->setEnabled(true); + App::contextItem(App::hoveredLinkItem()); + } + } else { // maybe cursor on some text history item? + HistoryItem *item = App::hoveredItem() ? App::hoveredItem() : App::hoveredLinkItem(); + bool canDelete = (item && item->itemType() == HistoryItem::MsgType); + bool canForward = canDelete && (item->id > 0) && !item->serviceMsg(); + + HistoryMessage *msg = dynamic_cast(item); + HistoryServiceMsg *srv = dynamic_cast(item); + + if (isUponSelected > 0) { + if (!_menu) _menu = new ContextMenu(this); + _menu->addAction(lang(lng_context_copy_selected), this, SLOT(copySelectedText()))->setEnabled(true); + } else if (item && !isUponSelected && !_contextMenuLnk) { + QString contextMenuText = item->selectedText(FullItemSel); + if (!contextMenuText.isEmpty()) { + if (!_menu) _menu = new ContextMenu(this); + _menu->addAction(lang(lng_context_copy_text), this, SLOT(copyContextText()))->setEnabled(true); + } + } + + if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { + if (!_menu) _menu = new ContextMenu(historyWidget); + _menu->addAction(lang(lng_context_open_link), this, SLOT(openContextUrl()))->setEnabled(true); + _menu->addAction(lang(lng_context_copy_link), this, SLOT(copyContextUrl()))->setEnabled(true); + } else if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { + if (!_menu) _menu = new ContextMenu(historyWidget); + _menu->addAction(lang(lng_context_open_email), this, SLOT(openContextUrl()))->setEnabled(true); + _menu->addAction(lang(lng_context_copy_email), this, SLOT(copyContextUrl()))->setEnabled(true); + } else if (_contextMenuLnk && dynamic_cast(_contextMenuLnk.data())) { + if (!_menu) _menu = new ContextMenu(historyWidget); + _menu->addAction(lang(lng_context_open_hashtag), this, SLOT(openContextUrl()))->setEnabled(true); + _menu->addAction(lang(lng_context_copy_hashtag), this, SLOT(copyContextUrl()))->setEnabled(true); + } else { + } + if (isUponSelected > 1) { + if (!_menu) _menu = new ContextMenu(this); + _menu->addAction(lang(lng_context_forward_selected), historyWidget, SLOT(onForwardSelected())); + _menu->addAction(lang(lng_context_delete_selected), historyWidget, SLOT(onDeleteSelected())); + _menu->addAction(lang(lng_context_clear_selection), historyWidget, SLOT(onClearSelected())); + } else { + if (!_menu) _menu = new ContextMenu(this); + if (isUponSelected != -2) { + if (canForward) { + _menu->addAction(lang(lng_context_forward_msg), historyWidget, SLOT(forwardMessage()))->setEnabled(true); + } + + if (canDelete) { + _menu->addAction(lang((msg && msg->uploading()) ? lng_context_cancel_upload : lng_context_delete_msg), historyWidget, SLOT(deleteMessage()))->setEnabled(true); + } + } + _menu->addAction(lang(lng_context_select_msg), historyWidget, SLOT(selectMessage()))->setEnabled(true); + } + App::contextItem(item); } + if (_menu) { _menu->deleteOnHide(); connect(_menu, SIGNAL(destroyed(QObject*)), this, SLOT(onMenuDestroy(QObject*))); @@ -1535,7 +1529,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) , _attachDragDocument(this) , _attachDragPhoto(this) , imageLoader(this) - , noTypingUpdate(false) + , _synthedTextUpdate(false) , loadingChatId(0) , loadingRequestId(0) , serviceImageCacheSize(0) @@ -1545,7 +1539,9 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) , bg(st::msgBG) , hiderOffered(false) , _scrollDelta(0) - , _typingRequest(0) { + , _typingRequest(0) + , _saveDraftText(false) + , _saveDraftStart(0) { _scroll.setFocusPolicy(Qt::NoFocus); setAcceptDrops(true); @@ -1575,6 +1571,11 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) _animActiveTimer.setSingleShot(false); connect(&_animActiveTimer, SIGNAL(timeout()), this, SLOT(onAnimActiveStep())); + _saveDraftTimer.setSingleShot(true); + connect(&_saveDraftTimer, SIGNAL(timeout()), this, SLOT(onDraftSave())); + connect(_field.verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onDraftSaveDelayed())); + connect(&_field, SIGNAL(cursorPositionChanged()), this, SLOT(onDraftSaveDelayed())); + _scroll.hide(); _scroll.move(0, 0); @@ -1605,6 +1606,43 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) void HistoryWidget::onTextChange() { updateTyping(); + + if (!hist || _synthedTextUpdate) return; + _saveDraftText = true; + onDraftSave(true); +} + +void HistoryWidget::onDraftSaveDelayed() { + if (!hist || _synthedTextUpdate) return; + if (!_field.textCursor().anchor() && !_field.textCursor().position() && !_field.verticalScrollBar()->value()) { + if (!Local::hasDraftPositions(hist->peer->id)) return; + } + onDraftSave(true); +} + +void HistoryWidget::onDraftSave(bool delayed) { + if (!hist) return; + if (delayed) { + uint64 ms = getms(); + if (!_saveDraftStart) { + _saveDraftStart = ms; + return _saveDraftTimer.start(SaveDraftTimeout); + } else if (ms - _saveDraftStart < SaveDraftAnywayTimeout) { + return _saveDraftTimer.start(SaveDraftTimeout); + } + } + writeDraft(); +} + +void HistoryWidget::writeDraft(const QString *text, const MessageCursor *cursor) { + bool save = hist && (_saveDraftStart > 0); + _saveDraftStart = 0; + _saveDraftTimer.stop(); + if (_saveDraftText) { + if (save) Local::writeDraft(hist->peer->id, text ? (*text) : _field.getText()); + _saveDraftText = false; + } + if (save) Local::writeDraftPositions(hist->peer->id, cursor ? (*cursor) : MessageCursor(_field)); } void HistoryWidget::cancelTyping() { @@ -1616,7 +1654,7 @@ void HistoryWidget::cancelTyping() { void HistoryWidget::updateTyping(bool typing) { uint64 ms = getms(true) + 10000; - if (noTypingUpdate || !hist || (typing && (hist->myTyping + 5000 > ms)) || (!typing && (hist->myTyping + 5000 <= ms))) return; + if (_synthedTextUpdate || !hist || (typing && (hist->myTyping + 5000 > ms)) || (!typing && (hist->myTyping + 5000 <= ms))) return; hist->myTyping = typing ? ms : 0; cancelTyping(); @@ -1725,7 +1763,10 @@ void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool l } if (hist) { hist->draft = _field.getText(); - hist->draftCur = _field.textCursor(); + hist->draftCursor.fillFrom(_field); + + writeDraft(&hist->draft, &hist->draftCursor); + if (hist->readyForWork() && _scroll.scrollTop() + 1 <= _scroll.scrollTopMax()) { hist->lastWidth = _list->width(); } else { @@ -1803,13 +1844,19 @@ void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool l App::main()->peerUpdated(histPeer); - noTypingUpdate = true; - setFieldText(hist->draft); - _field.setFocus(); if (!hist->draft.isEmpty()) { - _field.setTextCursor(hist->draftCur); + setFieldText(hist->draft); + _field.setFocus(); + hist->draftCursor.applyTo(_field, &_synthedTextUpdate); + } else { + QString draft = Local::readDraft(hist->peer->id); + setFieldText(draft); + _field.setFocus(); + if (!draft.isEmpty()) { + MessageCursor cur = Local::readDraftPositions(hist->peer->id); + cur.applyTo(_field, &_synthedTextUpdate); + } } - noTypingUpdate = false; connect(&_scroll, SIGNAL(geometryChanged()), _list, SLOT(onParentGeometryChanged())); connect(&_scroll, SIGNAL(scrolled()), _list, SLOT(onUpdateSelected())); @@ -2223,6 +2270,10 @@ void HistoryWidget::onSend(bool ctrlShiftEnter) { App::main()->sendPreparedText(hist, prepareMessage(_field.getText())); setFieldText(QString()); + _saveDraftText = true; + _saveDraftStart = getms(); + onDraftSave(); + if (!_attachType.isHidden()) _attachType.hideStart(); if (!_emojiPan.isHidden()) _emojiPan.hideStart(); } @@ -2688,7 +2739,7 @@ void HistoryWidget::updateOnlineDisplay(int32 x, int32 w) { } } } else { - text = App::onlineText(histPeer->asUser()->onlineTill, t); + text = App::onlineText(histPeer->asUser(), t); } if (titlePeerText != text) { titlePeerText = text; @@ -3164,9 +3215,9 @@ void HistoryWidget::onFieldTabbed() { } void HistoryWidget::setFieldText(const QString &text) { - noTypingUpdate = true; + _synthedTextUpdate = true; _field.setPlainText(text); - noTypingUpdate = false; + _synthedTextUpdate = false; } void HistoryWidget::onCancel() { diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 58c9f3d147..f1adad33b2 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -396,6 +396,9 @@ public slots: void onAnimActiveStep(); + void onDraftSaveDelayed(); + void onDraftSave(bool delayed = false); + private: bool messagesFailed(const RPCError &error, mtpRequestId requestId); @@ -404,6 +407,7 @@ private: void addMessagesToBack(const QVector &messages); void chatLoaded(const MTPmessages_ChatFull &res); + void writeDraft(const QString *text = 0, const MessageCursor *cursor = 0); void setFieldText(const QString &text); QStringList getMediasFromMime(const QMimeData *d); @@ -440,7 +444,7 @@ private: int32 _selCount; // < 0 - text selected, focus list, not _field LocalImageLoader imageLoader; - bool noTypingUpdate; + bool _synthedTextUpdate; PeerId loadingChatId; mtpRequestId loadingRequestId; @@ -470,5 +474,9 @@ private: mtpRequestId _typingRequest; QTimer _typingStopTimer; + uint64 _saveDraftStart; + bool _saveDraftText; + QTimer _saveDraftTimer; + }; diff --git a/Telegram/SourceFiles/localstorage.cpp b/Telegram/SourceFiles/localstorage.cpp new file mode 100644 index 0000000000..c10775434e --- /dev/null +++ b/Telegram/SourceFiles/localstorage.cpp @@ -0,0 +1,988 @@ +/* +This file is part of Telegram Desktop, +an unofficial desktop messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014 John Preston, https://tdesktop.com +*/ +#include "stdafx.h" +#include "localstorage.h" + +namespace { + + typedef quint64 FileKey; + + static const char tdfMagic[] = { 'T', 'D', 'F', '$' }; + static const int32 tdfMagicLen = sizeof(tdfMagic); + + QString toFilePart(FileKey val) { + QString result; + result.reserve(0x10); + for (int32 i = 0; i < 0x10; ++i) { + uchar v = (val & 0x0F); + result.push_back((v < 0x0A) ? ('0' + v) : ('A' + (v - 0x0A))); + val >>= 4; + } + return result; + } + + FileKey fromFilePart(const QString &val) { + FileKey result = 0; + int32 i = val.size(); + if (i != 0x10) return 0; + + while (i > 0) { + --i; + result <<= 4; + + uint16 ch = val.at(i).unicode(); + if (ch >= 'A' && ch <= 'F') { + result |= (ch - 'A') + 0x0A; + } else if (ch >= '0' && ch <= '9') { + result |= (ch - '0'); + } else { + return 0; + } + } + return result; + } + + QString _basePath; + + bool _started = false; + _local_inner::Manager *_manager = 0; + + bool _working() { + return _manager && !_basePath.isEmpty(); + } + + bool keyAlreadyUsed(QString &name) { + name += '0'; + if (QFileInfo(name).exists()) return true; + name[name.size() - 1] = '1'; + return QFileInfo(name).exists(); + } + + FileKey genKey() { + if (!_working()) return 0; + + FileKey result; + QString path; + path.reserve(_basePath.size() + 0x11); + path += _basePath; + do { + result = MTP::nonce(); + path.resize(_basePath.size()); + path += toFilePart(result); + } while (keyAlreadyUsed(path)); + + return result; + } + + void clearKey(const FileKey &key, bool safe = true) { + if (!_working()) return; + + QString name; + name.reserve(_basePath.size() + 0x11); + name += _basePath; + name += toFilePart(key); + name += '0'; + QFile::remove(name); + if (safe) { + name[name.size() - 1] = '1'; + QFile::remove(name); + } + } + + QByteArray _passKeySalt, _passKeyEncrypted; + + mtpAuthKey _oldKey, _passKey, _localKey; + void createLocalKey(const QByteArray &pass, QByteArray *salt, mtpAuthKey *result) { + uchar key[LocalEncryptKeySize] = { 0 }; + int32 iterCount = pass.size() ? LocalEncryptIterCount : LocalEncryptNoPwdIterCount; // dont slow down for no password + QByteArray newSalt; + if (!salt) { + newSalt.resize(LocalEncryptSaltSize); + memset_rand(newSalt.data(), newSalt.size()); + salt = &newSalt; + + cSetLocalSalt(newSalt); + } + + PKCS5_PBKDF2_HMAC_SHA1(pass.constData(), pass.size(), (uchar*)salt->data(), salt->size(), iterCount, LocalEncryptKeySize, key); + + result->setKey(key); + } + + struct FileReadDescriptor { + FileReadDescriptor() : version(0) { + } + int32 version; + QByteArray data; + QBuffer buffer; + QDataStream stream; + ~FileReadDescriptor() { + if (version) { + stream.setDevice(0); + if (buffer.isOpen()) buffer.close(); + buffer.setBuffer(0); + } + } + }; + + struct EncryptedDescriptor { + EncryptedDescriptor() { + } + EncryptedDescriptor(uint32 size) { + uint32 fullSize = sizeof(uint32) + size; + if (fullSize & 0x0F) fullSize += 0x10 - (fullSize & 0x0F); + data.reserve(fullSize); + + data.resize(sizeof(uint32)); + buffer.setBuffer(&data); + buffer.open(QIODevice::WriteOnly); + buffer.seek(sizeof(uint32)); + stream.setDevice(&buffer); + stream.setVersion(QDataStream::Qt_5_1); + } + QByteArray data; + QBuffer buffer; + QDataStream stream; + void finish() { + if (stream.device()) stream.setDevice(0); + if (buffer.isOpen()) buffer.close(); + buffer.setBuffer(0); + } + ~EncryptedDescriptor() { + finish(); + } + }; + + struct FileWriteDescriptor { + FileWriteDescriptor(const FileKey &key, bool safe = true) : dataSize(0) { + init(toFilePart(key), safe); + } + FileWriteDescriptor(const QString &name, bool safe = true) : dataSize(0) { + init(name, safe); + } + void init(const QString &name, bool safe) { + if (!_working()) return; + + // detect order of read attempts and file version + QString toTry[2]; + toTry[0] = _basePath + name + '0'; + if (safe) { + toTry[1] = _basePath + name + '1'; + QFileInfo toTry0(toTry[0]); + QFileInfo toTry1(toTry[1]); + if (toTry0.exists()) { + if (toTry1.exists()) { + QDateTime mod0 = toTry0.lastModified(), mod1 = toTry1.lastModified(); + if (mod0 > mod1) { + qSwap(toTry[0], toTry[1]); + } + } else { + qSwap(toTry[0], toTry[1]); + } + toDelete = toTry[1]; + } else if (toTry1.exists()) { + toDelete = toTry[1]; + } + } + + file.setFileName(toTry[0]); + if (file.open(QIODevice::WriteOnly)) { + file.write(tdfMagic, tdfMagicLen); + qint32 version = AppVersion; + file.write((const char*)&version, sizeof(version)); + + stream.setDevice(&file); + stream.setVersion(QDataStream::Qt_5_1); + } + } + bool writeData(const QByteArray &data) { + if (!file.isOpen()) return false; + + stream << data; + quint32 len = data.isNull() ? 0xffffffff : data.size(); + if (QSysInfo::ByteOrder != QSysInfo::BigEndian) { + len = qbswap(len); + } + md5.feed(&len, sizeof(len)); + md5.feed(data.constData(), data.size()); + dataSize += sizeof(len) + data.size(); + + return true; + } + QByteArray prepareEncrypted(EncryptedDescriptor &data, const mtpAuthKey &key = _localKey) { + data.finish(); + QByteArray &toEncrypt(data.data); + + // prepare for encryption + uint32 size = toEncrypt.size(), fullSize = size; + if (fullSize & 0x0F) { + fullSize += 0x10 - (fullSize & 0x0F); + toEncrypt.resize(fullSize); + memset_rand(toEncrypt.data() + size, fullSize - size); + } + *(uint32*)toEncrypt.data() = size; + QByteArray encrypted(0x10 + fullSize, Qt::Uninitialized); // 128bit of sha1 - key128, sizeof(data), data + hashSha1(toEncrypt.constData(), toEncrypt.size(), encrypted.data()); + aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 0x10, fullSize, &key, encrypted.constData()); + + return encrypted; + } + bool writeEncrypted(EncryptedDescriptor &data, const mtpAuthKey &key = _localKey) { + return writeData(prepareEncrypted(data, key)); + } + void finish() { + if (!file.isOpen()) return; + + stream.setDevice(0); + + md5.feed(&dataSize, sizeof(dataSize)); + qint32 version = AppVersion; + md5.feed(&version, sizeof(version)); + md5.feed(tdfMagic, tdfMagicLen); + file.write((const char*)md5.result(), 0x10); + file.close(); + + if (!toDelete.isEmpty()) { + QFile::remove(toDelete); + } + } + QFile file; + QDataStream stream; + + QString toDelete; + + HashMd5 md5; + int32 dataSize; + + ~FileWriteDescriptor() { + finish(); + } + }; + + bool readFile(FileReadDescriptor &result, const QString &name, bool safe = true) { + if (!_working()) return false; + + // detect order of read attempts + QString toTry[2]; + toTry[0] = _basePath + name + '0'; + if (safe) { + QFileInfo toTry0(toTry[0]); + if (toTry0.exists()) { + toTry[1] = _basePath + name + '1'; + QFileInfo toTry1(toTry[1]); + if (toTry1.exists()) { + QDateTime mod0 = toTry0.lastModified(), mod1 = toTry1.lastModified(); + if (mod0 < mod1) { + qSwap(toTry[0], toTry[1]); + } + } else { + toTry[1] = QString(); + } + } else { + toTry[0][toTry[0].size() - 1] = '1'; + } + } + for (int32 i = 0; i < 2; ++i) { + QString fname(toTry[i]); + if (fname.isEmpty()) break; + + QFile f(fname); + if (!f.open(QIODevice::ReadOnly)) { + DEBUG_LOG(("App Info: failed to open '%1' for reading").arg(name)); + continue; + } + + // check magic + char magic[tdfMagicLen]; + if (f.read(magic, tdfMagicLen) != tdfMagicLen) { + DEBUG_LOG(("App Info: failed to read magic from '%1'").arg(name)); + continue; + } + if (memcmp(magic, tdfMagic, tdfMagicLen)) { + DEBUG_LOG(("App Info: bad magic %1 in '%2'").arg(mb(magic, tdfMagicLen).str()).arg(name)); + continue; + } + + // read app version + qint32 version; + if (f.read((char*)&version, sizeof(version)) != sizeof(version)) { + DEBUG_LOG(("App Info: failed to read version from '%1'").arg(name)); + continue; + } + if (version > AppVersion) { + DEBUG_LOG(("App Info: version too big %1 for '%2', my version %3").arg(version).arg(name).arg(AppVersion)); + continue; + } + + // read data + QByteArray bytes = f.read(f.size()); + int32 dataSize = bytes.size() - 16; + if (dataSize < 0) { + DEBUG_LOG(("App Info: bad file '%1', could not read sign part").arg(name)); + continue; + } + + // check signature + HashMd5 md5; + md5.feed(bytes.constData(), dataSize); + md5.feed(&dataSize, sizeof(dataSize)); + md5.feed(&version, sizeof(version)); + md5.feed(magic, tdfMagicLen); + if (memcmp(md5.result(), bytes.constData() + dataSize, 16)) { + DEBUG_LOG(("App Info: bad file '%1', signature did not match").arg(name)); + continue; + } + + bytes.resize(dataSize); + result.data = bytes; + bytes = QByteArray(); + + result.version = version; + result.buffer.setBuffer(&result.data); + result.buffer.open(QIODevice::ReadOnly); + result.stream.setDevice(&result.buffer); + result.stream.setVersion(QDataStream::Qt_5_1); + + if ((i == 0 && !toTry[1].isEmpty()) || i == 1) { + QFile::remove(toTry[1 - i]); + } + + return true; + } + return false; + } + + bool decryptLocal(EncryptedDescriptor &result, const QByteArray &encrypted, const mtpAuthKey &key = _localKey) { + if (encrypted.size() <= 16 || (encrypted.size() & 0x0F)) { + LOG(("App Error: bad encrypted part size: %1").arg(encrypted.size())); + return false; + } + uint32 fullLen = encrypted.size() - 16; + + QByteArray decrypted; + decrypted.resize(fullLen); + const char *encryptedKey = encrypted.constData(), *encryptedData = encrypted.constData() + 16; + aesDecryptLocal(encryptedData, decrypted.data(), fullLen, &key, encryptedKey); + uchar sha1Buffer[20]; + if (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), encryptedKey, 16)) { + LOG(("App Error: bad decrypt key, data not decrypted")); + return false; + } + + uint32 dataLen = *(const uint32*)decrypted.constData(); + if (dataLen > uint32(decrypted.size()) || dataLen <= fullLen - 16 || dataLen < sizeof(uint32)) { + LOG(("App Error: bad decrypted part size: %1, fullLen: %2, decrypted size: %3").arg(dataLen).arg(fullLen).arg(decrypted.size())); + return false; + } + + decrypted.resize(dataLen); + result.data = decrypted; + decrypted = QByteArray(); + + result.buffer.setBuffer(&result.data); + result.buffer.open(QIODevice::ReadOnly); + result.buffer.seek(sizeof(uint32)); // skip len + result.stream.setDevice(&result.buffer); + result.stream.setVersion(QDataStream::Qt_5_1); + + return true; + } + + bool readEncryptedFile(FileReadDescriptor &result, const QString &name, bool safe = true) { + if (!readFile(result, name, safe)) { + return false; + } + QByteArray encrypted; + result.stream >> encrypted; + + EncryptedDescriptor data; + if (!decryptLocal(data, encrypted)) { + result.stream.setDevice(0); + if (result.buffer.isOpen()) result.buffer.close(); + result.buffer.setBuffer(0); + result.data = QByteArray(); + result.version = 0; + return false; + } + + result.stream.setDevice(0); + if (result.buffer.isOpen()) result.buffer.close(); + result.buffer.setBuffer(0); + result.data = data.data; + result.buffer.setBuffer(&result.data); + result.buffer.open(QIODevice::ReadOnly); + result.buffer.seek(data.buffer.pos()); + result.stream.setDevice(&result.buffer); + result.stream.setVersion(QDataStream::Qt_5_1); + + return true; + } + + enum { // Local Storage Keys + lskUserMap = 0, + lskDraft, // data: PeerId peer + lskDraftPosition, // data: PeerId peer + lskStorage, // data: StorageKey location + }; + + typedef QMap DraftsMap; + DraftsMap _draftsMap, _draftsPositionsMap; + typedef QMap DraftsNotReadMap; + DraftsNotReadMap _draftsNotReadMap; + + typedef QPair FileDesc; // file, size + typedef QMap StorageMap; + StorageMap _storageMap; + int32 _storageFilesSize = 0; + + bool _mapChanged = false; + + Local::ReadMapState _readMap(const QByteArray &pass) { + uint64 ms = getms(); + QByteArray dataNameUtf8 = cDataFile().toUtf8(); + uint64 dataNameHash[2]; + hashMd5(dataNameUtf8.constData(), dataNameUtf8.size(), dataNameHash); + _basePath = cWorkingDir() + qsl("tdata/") + toFilePart(dataNameHash[0]) + QChar('/'); + + FileReadDescriptor mapData; + if (!readFile(mapData, qsl("map"))) { + return Local::ReadMapFailed; + } + + QByteArray salt, keyEncrypted, mapEncrypted; + mapData.stream >> salt >> keyEncrypted >> mapEncrypted; + if (mapData.stream.status() != QDataStream::Ok) { + LOG(("App Error: could not read salt / key from map file - corrupted?..").arg(mapData.stream.status())); + return Local::ReadMapFailed; + } + if (salt.size() != LocalEncryptSaltSize) { + LOG(("App Error: bad salt in map file, size: %1").arg(salt.size())); + return Local::ReadMapFailed; + } + createLocalKey(pass, &salt, &_passKey); + + EncryptedDescriptor keyData, map; + if (!decryptLocal(keyData, keyEncrypted, _passKey)) { + LOG(("App Error: could not decrypt pass-protected key from map file, maybe bad password..")); + return Local::ReadMapPassNeeded; + } + uchar key[LocalEncryptKeySize] = { 0 }; + if (keyData.stream.readRawData((char*)key, LocalEncryptKeySize) != LocalEncryptKeySize || !keyData.stream.atEnd()) { + LOG(("App Error: could not read pass-protected key from map file")); + return Local::ReadMapFailed; + } + _localKey.setKey(key); + + _passKeyEncrypted = keyEncrypted; + _passKeySalt = salt; + + if (!decryptLocal(map, mapEncrypted)) { + LOG(("App Error: could not decrypt map.")); + return Local::ReadMapFailed; + } + + DraftsMap draftsMap, draftsPositionsMap; + DraftsNotReadMap draftsNotReadMap; + StorageMap storageMap; + qint64 storageFilesSize = 0; + while (!map.stream.atEnd()) { + quint32 keyType; + map.stream >> keyType; + switch (keyType) { + case lskDraft: { + quint32 count = 0; + map.stream >> count; + for (quint32 i = 0; i < count; ++i) { + FileKey key; + quint64 p; + map.stream >> key >> p; + draftsMap.insert(p, key); + draftsNotReadMap.insert(p, true); + } + } break; + case lskDraftPosition: { + quint32 count = 0; + map.stream >> count; + for (quint32 i = 0; i < count; ++i) { + FileKey key; + quint64 p; + map.stream >> key >> p; + draftsPositionsMap.insert(p, key); + } + } break; + case lskStorage: { + quint32 count = 0; + map.stream >> count; + for (quint32 i = 0; i < count; ++i) { + FileKey key; + quint64 first, second; + qint32 size; + map.stream >> key >> first >> second >> size; + storageMap.insert(StorageKey(first, second), FileDesc(key, size)); + storageFilesSize += size; + } + } break; + default: + LOG(("App Error: unknown key type in encrypted map: %1").arg(keyType)); + return Local::ReadMapFailed; + } + if (map.stream.status() != QDataStream::Ok) { + LOG(("App Error: reading encrypted map bad status: %1").arg(map.stream.status())); + return Local::ReadMapFailed; + } + } + _draftsMap = draftsMap; + _draftsPositionsMap = draftsPositionsMap; + _draftsNotReadMap = draftsNotReadMap; + _storageMap = storageMap; + _storageFilesSize = storageFilesSize; + _mapChanged = false; + LOG(("Map read time: %1").arg(getms() - ms)); + return Local::ReadMapDone; + } + + enum WriteMapWhen { + WriteMapNow, + WriteMapFast, + WriteMapSoon, + }; + void _writeMap(WriteMapWhen when = WriteMapSoon) { + if (when != WriteMapNow) { + _manager->writeMap(when == WriteMapFast); + return; + } + _manager->writingMap(); + if (!_mapChanged) return; + if (_basePath.isEmpty()) { + LOG(("App Error: _basePath is empty in writeMap()")); + return; + } + + QDir().mkpath(_basePath); + + FileWriteDescriptor map(qsl("map")); + if (_passKeySalt.isEmpty() || _passKeyEncrypted.isEmpty()) { + uchar local5Key[LocalEncryptKeySize] = { 0 }; + QByteArray pass(LocalEncryptKeySize, Qt::Uninitialized), salt(LocalEncryptSaltSize, Qt::Uninitialized); + memset_rand(pass.data(), pass.size()); + memset_rand(salt.data(), salt.size()); + createLocalKey(pass, &salt, &_localKey); + + _passKeySalt.resize(LocalEncryptSaltSize); + memset_rand(_passKeySalt.data(), _passKeySalt.size()); + createLocalKey(QByteArray(), &_passKeySalt, &_passKey); + + EncryptedDescriptor passKeyData(LocalEncryptKeySize); + _localKey.write(passKeyData.stream); + _passKeyEncrypted = map.prepareEncrypted(passKeyData, _passKey); + } + map.writeData(_passKeySalt); + map.writeData(_passKeyEncrypted); + + uint32 mapSize = 0; + if (!_draftsMap.isEmpty()) mapSize += sizeof(quint32) * 2 + _draftsMap.size() * sizeof(quint64) * 2; + if (!_draftsPositionsMap.isEmpty()) mapSize += sizeof(quint32) * 2 + _draftsPositionsMap.size() * sizeof(quint64) * 2; + if (!_storageMap.isEmpty()) mapSize += sizeof(quint32) * 2 + _storageMap.size() * (sizeof(quint64) * 3 + sizeof(qint32)); + EncryptedDescriptor mapData(mapSize); + if (!_draftsMap.isEmpty()) { + mapData.stream << quint32(lskDraft) << quint32(_draftsMap.size()); + for (DraftsMap::const_iterator i = _draftsMap.cbegin(), e = _draftsMap.cend(); i != e; ++i) { + mapData.stream << quint64(i.value()) << quint64(i.key()); + } + } + if (!_draftsPositionsMap.isEmpty()) { + mapData.stream << quint32(lskDraftPosition) << quint32(_draftsPositionsMap.size()); + for (DraftsMap::const_iterator i = _draftsPositionsMap.cbegin(), e = _draftsPositionsMap.cend(); i != e; ++i) { + mapData.stream << quint64(i.value()) << quint64(i.key()); + } + } + if (!_storageMap.isEmpty()) { + mapData.stream << quint32(lskStorage) << quint32(_storageMap.size()); + for (StorageMap::const_iterator i = _storageMap.cbegin(), e = _storageMap.cend(); i != e; ++i) { + mapData.stream << quint64(i.value().first) << quint64(i.key().first) << quint64(i.key().second) << qint32(i.value().second); + } + } + map.writeEncrypted(mapData); + + map.finish(); + + _mapChanged = false; + } + +} + +namespace _local_inner { + + Manager::Manager() { + _mapWriteTimer.setSingleShot(true); + connect(&_mapWriteTimer, SIGNAL(timeout()), this, SLOT(mapWriteTimeout())); + } + + void Manager::writeMap(bool fast) { + if (!_mapWriteTimer.isActive() || fast) { + _mapWriteTimer.start(fast ? 1 : WriteMapTimeout); + } else if (_mapWriteTimer.remainingTime() <= 0) { + mapWriteTimeout(); + } + } + + void Manager::writingMap() { + _mapWriteTimer.stop(); + } + + void Manager::mapWriteTimeout() { + _writeMap(WriteMapNow); + } + + void Manager::finish() { + if (_mapWriteTimer.isActive()) { + mapWriteTimeout(); + _mapWriteTimer.stop(); + } + } + +} + +namespace Local { + + mtpAuthKey &oldKey() { + return _oldKey; + } + + void createOldKey(QByteArray *salt) { + createLocalKey(QByteArray(), salt, &_oldKey); + } + + void start() { + if (!_started) { + _started = true; + _manager = new _local_inner::Manager(); + } + } + + void stop() { + if (_manager) { + _writeMap(WriteMapNow); + _manager->finish(); + _manager->deleteLater(); + _manager = 0; + } + } + + ReadMapState readMap(const QByteArray &pass) { + ReadMapState result = _readMap(pass); + if (result == ReadMapFailed) { + _mapChanged = true; + _writeMap(WriteMapNow); + } + return result; + } + + void writeDraft(const PeerId &peer, const QString &text) { + if (!_working()) return; + + if (text.isEmpty()) { + DraftsMap::iterator i = _draftsMap.find(peer); + if (i != _draftsMap.cend()) { + clearKey(i.value()); + _draftsMap.erase(i); + _mapChanged = true; + _writeMap(); + } + + _draftsNotReadMap.remove(peer); + } else { + DraftsMap::const_iterator i = _draftsMap.constFind(peer); + if (i == _draftsMap.cend()) { + i = _draftsMap.insert(peer, genKey()); + _mapChanged = true; + _writeMap(WriteMapFast); + } + QString to = _basePath + toFilePart(i.value()); + EncryptedDescriptor data(sizeof(quint64) + sizeof(quint32) + text.size() * sizeof(QChar)); + data.stream << quint64(peer) << text; + FileWriteDescriptor file(i.value()); + file.writeEncrypted(data); + + _draftsNotReadMap.remove(peer); + } + } + + QString readDraft(const PeerId &peer) { + if (!_draftsNotReadMap.remove(peer)) return QString(); + + DraftsMap::iterator j = _draftsMap.find(peer); + if (j == _draftsMap.cend()) { + return QString(); + } + FileReadDescriptor draft; + if (!readEncryptedFile(draft, toFilePart(j.value()))) { + clearKey(j.value()); + _draftsMap.erase(j); + return QString(); + } + + quint64 draftPeer; + QString draftText; + draft.stream >> draftPeer >> draftText; + return (draftPeer == peer) ? draftText : QString(); + } + + void writeDraftPositions(const PeerId &peer, const MessageCursor &cur) { + if (!_working()) return; + + if (cur.position == 0 && cur.anchor == 0 && cur.scroll == 0) { + DraftsMap::iterator i = _draftsPositionsMap.find(peer); + if (i != _draftsPositionsMap.cend()) { + clearKey(i.value()); + _draftsPositionsMap.erase(i); + _mapChanged = true; + _writeMap(); + } + } else { + DraftsMap::const_iterator i = _draftsPositionsMap.constFind(peer); + if (i == _draftsPositionsMap.cend()) { + i = _draftsPositionsMap.insert(peer, genKey()); + _mapChanged = true; + _writeMap(WriteMapFast); + } + QString to = _basePath + toFilePart(i.value()); + EncryptedDescriptor data(sizeof(quint64) + sizeof(qint32) * 3); + data.stream << quint64(peer) << qint32(cur.position) << qint32(cur.anchor) << qint32(cur.scroll); + FileWriteDescriptor file(i.value()); + file.writeEncrypted(data); + } + } + + MessageCursor readDraftPositions(const PeerId &peer) { + DraftsMap::iterator j = _draftsPositionsMap.find(peer); + if (j == _draftsPositionsMap.cend()) { + return MessageCursor(); + } + FileReadDescriptor draft; + if (!readEncryptedFile(draft, toFilePart(j.value()))) { + clearKey(j.value()); + _draftsPositionsMap.erase(j); + return MessageCursor(); + } + + quint64 draftPeer; + qint32 curPosition, curAnchor, curScroll; + draft.stream >> draftPeer >> curPosition >> curAnchor >> curScroll; + + return (draftPeer == peer) ? MessageCursor(curPosition, curAnchor, curScroll) : MessageCursor(); + } + + bool hasDraftPositions(const PeerId &peer) { + return (_draftsPositionsMap.constFind(peer) != _draftsPositionsMap.cend()); + } + + qint32 _storageImageSize(qint32 rawlen) { + // fulllen + storagekey + type + len + data + qint32 result = sizeof(uint32) + sizeof(quint64) * 2 + sizeof(quint32) + sizeof(quint32) + rawlen; + if (result & 0x0F) result += 0x10 - (result & 0x0F); + result += tdfMagicLen + sizeof(qint32) + sizeof(quint32) + 0x10 + 0x10; // magic + version + len of encrypted + part of sha1 + md5 + return result; + } + + void writeImage(const StorageKey &location, const ImagePtr &image) { + if (image->isNull() || !image->loaded()) return; + if (_storageMap.constFind(location) != _storageMap.cend()) return; + + QByteArray fmt = image->savedFormat(); + mtpTypeId format = 0; + if (fmt == "JPG") { + format = mtpc_storage_fileJpeg; + } else if (fmt == "PNG") { + format = mtpc_storage_filePng; + } else if (fmt == "GIF") { + format = mtpc_storage_fileGif; + } + if (format) { + image->forget(); + writeImage(location, StorageImageSaved(format, image->savedData()), false); + } + } + + void writeImage(const StorageKey &location, const StorageImageSaved &image, bool overwrite) { + if (!_working()) return; + + qint32 size = _storageImageSize(image.data.size()); + StorageMap::const_iterator i = _storageMap.constFind(location); + if (i == _storageMap.cend()) { + i = _storageMap.insert(location, FileDesc(genKey(), size)); + _storageFilesSize += size; + _mapChanged = true; + _writeMap(); + } else if (!overwrite) { + return; + } + EncryptedDescriptor data(sizeof(quint64) * 2 + sizeof(quint32) + sizeof(quint32) + image.data.size()); + data.stream << quint64(location.first) << quint64(location.second) << quint32(image.type) << image.data; + FileWriteDescriptor file(i.value().first, false); + file.writeEncrypted(data); + if (i.value().second != size) { + _storageFilesSize += size; + _storageFilesSize -= i.value().second; + _storageMap[location].second = size; + } + } + + StorageImageSaved readImage(const StorageKey &location) { + StorageMap::iterator j = _storageMap.find(location); + if (j == _storageMap.cend()) { + return StorageImageSaved(); + } + FileReadDescriptor draft; + if (!readEncryptedFile(draft, toFilePart(j.value().first), false)) { + clearKey(j.value().first, false); + _storageFilesSize -= j.value().second; + _storageMap.erase(j); + return StorageImageSaved(); + } + + QByteArray imageData; + quint64 locFirst, locSecond; + quint32 imageType; + draft.stream >> locFirst >> locSecond >> imageType >> imageData; + + return (locFirst == location.first && locSecond == location.second) ? StorageImageSaved(imageType, imageData) : StorageImageSaved(); + } + + int32 hasImages() { + return _storageMap.size(); + } + + qint64 storageFilesSize() { + return _storageFilesSize; + } + + struct ClearManagerData { + QThread *thread; + StorageMap images; + QMutex mutex; + QList tasks; + bool working; + }; + + ClearManager::ClearManager() : data(new ClearManagerData()) { + data->thread = new QThread(); + data->working = true; + } + + bool ClearManager::addTask(int task) { + QMutexLocker lock(&data->mutex); + if (!data->working) return false; + + if (!data->tasks.isEmpty() && (data->tasks.at(0) == ClearManagerAll)) return true; + if (task == ClearManagerAll) { + data->tasks.clear(); + } else { + if (task & ClearManagerImages) { + if (data->images.isEmpty()) { + data->images = _storageMap; + } else { + for (StorageMap::const_iterator i = _storageMap.cbegin(), e = _storageMap.cend(); i != e; ++i) { + StorageKey k = i.key(); + while (data->images.constFind(k) != data->images.cend()) { + ++k.second; + } + data->images.insert(k, i.value()); + } + } + _storageMap.clear(); + _storageFilesSize = 0; + _mapChanged = true; + _writeMap(); + } + for (int32 i = 0, l = data->tasks.size(); i < l; ++i) { + if (data->tasks.at(i) == task) return true; + } + } + data->tasks.push_back(task); + return true; + } + + bool ClearManager::hasTask(ClearManagerTask task) { + QMutexLocker lock(&data->mutex); + if (data->tasks.isEmpty()) return false; + if (data->tasks.at(0) == ClearManagerAll) return true; + for (int32 i = 0, l = data->tasks.size(); i < l; ++i) { + if (data->tasks.at(i) == task) return true; + } + return false; + } + + void ClearManager::start() { + moveToThread(data->thread); + connect(data->thread, SIGNAL(started()), this, SLOT(onStart())); + data->thread->start(); + } + + ClearManager::~ClearManager() { + data->thread->deleteLater(); + delete data; + } + + void ClearManager::onStart() { + while (true) { + int task = 0; + bool result = false; + StorageMap images; + { + QMutexLocker lock(&data->mutex); + if (data->tasks.isEmpty()) { + data->working = false; + break; + } + task = data->tasks.at(0); + images = data->images; + } + switch (task) { + case ClearManagerAll: + result = (QDir(cTempDir()).removeRecursively() && QDir(_basePath).removeRecursively()); + break; + case ClearManagerDownloads: + result = QDir(cTempDir()).removeRecursively(); + break; + case ClearManagerImages: + for (StorageMap::const_iterator i = images.cbegin(), e = images.cend(); i != e; ++i) { + clearKey(i.value().first, false); + } + result = true; + break; + } + { + QMutexLocker lock(&data->mutex); + if (data->tasks.at(0) == task) { + data->tasks.pop_front(); + if (data->tasks.isEmpty()) { + data->working = false; + } + } + if (result) { + emit succeed(task, data->working ? 0 : this); + } else { + emit failed(task, data->working ? 0 : this); + } + if (!data->working) break; + } + } + } + +} diff --git a/Telegram/SourceFiles/localstorage.h b/Telegram/SourceFiles/localstorage.h new file mode 100644 index 0000000000..47dbcf8df4 --- /dev/null +++ b/Telegram/SourceFiles/localstorage.h @@ -0,0 +1,102 @@ +/* +This file is part of Telegram Desktop, +an unofficial desktop messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014 John Preston, https://tdesktop.com +*/ +#pragma once + +#include "types.h" + +namespace _local_inner { + + class Manager : public QObject { + Q_OBJECT + + public: + + Manager(); + + void writeMap(bool fast); + void writingMap(); + void finish(); + + public slots: + + void mapWriteTimeout(); + + private: + + QTimer _mapWriteTimer; + }; + +} + +namespace Local { + + mtpAuthKey &oldKey(); + void createOldKey(QByteArray *salt = 0); + + void start(); + void stop(); + + enum ClearManagerTask { + ClearManagerAll = 0xFFFF, + ClearManagerDownloads = 0x01, + ClearManagerImages = 0x02, + }; + + class ClearManagerData; + class ClearManager : public QObject { + Q_OBJECT + + public: + ClearManager(); + bool addTask(int task); + bool hasTask(ClearManagerTask task); + void start(); + ~ClearManager(); + + public slots: + void onStart(); + + signals: + void succeed(int task, void *manager); + void failed(int task, void *manager); + + private: + ClearManagerData *data; + + }; + + enum ReadMapState { + ReadMapFailed = 0, + ReadMapDone = 1, + ReadMapPassNeeded = 2, + }; + ReadMapState readMap(const QByteArray &pass); + + void writeDraft(const PeerId &peer, const QString &text); + QString readDraft(const PeerId &peer); + void writeDraftPositions(const PeerId &peer, const MessageCursor &cur); + MessageCursor readDraftPositions(const PeerId &peer); + bool hasDraftPositions(const PeerId &peer); + + void writeImage(const StorageKey &location, const ImagePtr &img); + void writeImage(const StorageKey &location, const StorageImageSaved &jpeg, bool overwrite = true); + StorageImageSaved readImage(const StorageKey &location); + int32 hasImages(); + qint64 storageFilesSize(); + +}; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 084f3e4d07..34cca91187 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -143,7 +143,7 @@ void TopBarWidget::enableShadow(bool enable) { void TopBarWidget::paintEvent(QPaintEvent *e) { QPainter p(this); - if (e->rect().top() < st::topBarHeight) { + if (e->rect().top() < st::topBarHeight) { // optimize shadow-only drawing p.fillRect(QRect(0, 0, width(), st::topBarHeight), st::topBarBG->b); if (_clearSelection.isHidden()) { p.save(); @@ -154,8 +154,6 @@ void TopBarWidget::paintEvent(QPaintEvent *e) { p.setPen(st::btnDefLink.color->p); p.drawText(st::topBarSelectedPos.x(), st::topBarSelectedPos.y() + st::linkFont->ascent, _selStr); } - } else { - int a = 0; // optimize shadow-only drawing } if (_drawShadow) { p.fillRect(st::titleShadow, st::topBarHeight, width() - st::titleShadow, st::titleShadow, st::titleShadowColor->b); @@ -1734,7 +1732,6 @@ void MainWidget::gotState(const MTPupdates_State &state) { MTP::setGlobalDoneHandler(rpcDone(&MainWidget::updateReceived)); _lastUpdateTime = getms(true); noUpdatesTimer.start(NoUpdatesTimeout); - LOG(("Started no updates timeout, %1").arg(_lastUpdateTime)); updInited = true; dialogs.loadDialogs(); @@ -1752,7 +1749,7 @@ void MainWidget::gotDifference(const MTPupdates_Difference &diff) { MTP::setGlobalDoneHandler(rpcDone(&MainWidget::updateReceived)); _lastUpdateTime = getms(true); noUpdatesTimer.start(NoUpdatesTimeout); - LOG(("Started no updates timeout, %1").arg(_lastUpdateTime)); + updInited = true; } break; case mtpc_updates_differenceSlice: { @@ -2000,6 +1997,8 @@ int32 MainWidget::dlgsWidth() const { } MainWidget::~MainWidget() { + if (App::main() == this) history.showPeer(0, 0, true); + delete hider; MTP::clearGlobalHandlers(); App::deinitMedia(false); @@ -2043,7 +2042,6 @@ void MainWidget::updateReceived(const mtpPrime *from, const mtpPrime *end) { _lastUpdateTime = getms(true); noUpdatesTimer.start(NoUpdatesTimeout); - LOG(("Started no updates timeout, %1").arg(_lastUpdateTime)); handleUpdates(updates); } catch(mtpErrorUnexpected &e) { // just some other type diff --git a/Telegram/SourceFiles/mediaview.cpp b/Telegram/SourceFiles/mediaview.cpp index 4219576206..cbdd8ca5e9 100644 --- a/Telegram/SourceFiles/mediaview.cpp +++ b/Telegram/SourceFiles/mediaview.cpp @@ -468,7 +468,6 @@ void MediaView::showPhoto(PhotoData *photo) { _doc = 0; _zoom = 0; MTP::clearLoaderPriorities(); - _photo->full->load(); _full = -1; _current = QPixmap(); _down = OverNone; @@ -490,6 +489,7 @@ void MediaView::showPhoto(PhotoData *photo) { _width = _w; _from = App::user(_photo->user); updateControls(); + _photo->full->load(); if (isHidden()) { psUpdateOverlayed(this); show(); diff --git a/Telegram/SourceFiles/mtproto/mtp.cpp b/Telegram/SourceFiles/mtproto/mtp.cpp index 06ee2b54dc..0d1122de4c 100644 --- a/Telegram/SourceFiles/mtproto/mtp.cpp +++ b/Telegram/SourceFiles/mtproto/mtp.cpp @@ -18,6 +18,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "stdafx.h" #include "mtp.h" +#include "localstorage.h" + namespace { typedef QMap Sessions; Sessions sessions; @@ -61,8 +63,6 @@ namespace { MTPSessionResetHandler sessionResetHandler = 0; _mtp_internal::RequestResender *resender = 0; - mtpAuthKey _localKey; - void importDone(const MTPauth_Authorization &result, mtpRequestId req) { QMutexLocker locker1(&requestByDCLock); @@ -564,31 +564,11 @@ namespace _mtp_internal { }; namespace MTP { - mtpAuthKey &localKey() { - return _localKey; - } - - void createLocalKey(const QByteArray &pass, QByteArray *salt) { - uchar key[LocalEncryptKeySize] = { 0 }; - int32 iterCount = pass.size() ? LocalEncryptIterCount : LocalEncryptNoPwdIterCount; // dont slow down for no password - QByteArray newSalt; - if (!salt) { - newSalt.resize(LocalEncryptSaltSize); - memset_rand(newSalt.data(), newSalt.size()); - salt = &newSalt; - - cSetLocalSalt(newSalt); - } - - PKCS5_PBKDF2_HMAC_SHA1(pass.constData(), pass.size(), (uchar*)salt->data(), salt->size(), iterCount, LocalEncryptKeySize, key); - - _localKey.setKey(key); - } void start() { unixtimeInit(); - if (!localKey().created()) { + if (!Local::oldKey().created()) { LOG(("App Error: trying to start MTP without local key!")); return; } diff --git a/Telegram/SourceFiles/mtproto/mtp.h b/Telegram/SourceFiles/mtproto/mtp.h index ae0f39b2f3..7aa6d28642 100644 --- a/Telegram/SourceFiles/mtproto/mtp.h +++ b/Telegram/SourceFiles/mtproto/mtp.h @@ -63,9 +63,6 @@ namespace _mtp_internal { namespace MTP { - mtpAuthKey &localKey(); - void createLocalKey(const QByteArray &pass, QByteArray *salt = 0); - static const uint32 cfg = 1 * _mtp_internal::dcShift; // send(MTPhelp_GetConfig(), MTP::cfg + dc) - for dc enum static const uint32 dld[MTPDownloadSessionsCount] = { // send(req, callbacks, MTP::dld[i] + dc) - for download 0x10 * _mtp_internal::dcShift, diff --git a/Telegram/SourceFiles/mtproto/mtpAuthKey.h b/Telegram/SourceFiles/mtproto/mtpAuthKey.h index 61909a03b5..67e6c454cb 100644 --- a/Telegram/SourceFiles/mtproto/mtpAuthKey.h +++ b/Telegram/SourceFiles/mtproto/mtpAuthKey.h @@ -48,7 +48,7 @@ public: return _keyId; } - void prepareAES(const MTPint128 &msgKey, MTPint256 &aesKey, MTPint256 &aesIV, bool send = true) { + void prepareAES(const MTPint128 &msgKey, MTPint256 &aesKey, MTPint256 &aesIV, bool send = true) const { if (!_isset) throw mtpErrorKeyNotReady(QString("prepareAES(.., %1)").arg(logBool(send))); uint32 x = send ? 0 : 8; @@ -112,14 +112,14 @@ inline void aesEncrypt(const void *src, void *dst, uint32 len, void *key, void * AES_ige_encrypt((const uchar*)src, (uchar*)dst, len, &aes, aes_iv, AES_ENCRYPT); } -inline void aesEncrypt(const void *src, void *dst, uint32 len, mtpAuthKeyPtr authKey, const MTPint128 &msgKey) { +inline void aesEncrypt(const void *src, void *dst, uint32 len, const mtpAuthKeyPtr &authKey, const MTPint128 &msgKey) { MTPint256 aesKey, aesIV; authKey->prepareAES(msgKey, aesKey, aesIV); return aesEncrypt(src, dst, len, &aesKey, &aesIV); } -inline void aesEncryptLocal(const void *src, void *dst, uint32 len, mtpAuthKey *authKey, const void *key128) { +inline void aesEncryptLocal(const void *src, void *dst, uint32 len, const mtpAuthKey *authKey, const void *key128) { MTPint256 aesKey, aesIV; authKey->prepareAES(*(const MTPint128*)key128, aesKey, aesIV, false); @@ -136,14 +136,14 @@ inline void aesDecrypt(const void *src, void *dst, uint32 len, void *key, void * AES_ige_encrypt((const uchar*)src, (uchar*)dst, len, &aes, aes_iv, AES_DECRYPT); } -inline void aesDecrypt(const void *src, void *dst, uint32 len, mtpAuthKeyPtr authKey, const MTPint128 &msgKey) { +inline void aesDecrypt(const void *src, void *dst, uint32 len, const mtpAuthKeyPtr &authKey, const MTPint128 &msgKey) { MTPint256 aesKey, aesIV; authKey->prepareAES(msgKey, aesKey, aesIV, false); return aesDecrypt(src, dst, len, &aesKey, &aesIV); } -inline void aesDecryptLocal(const void *src, void *dst, uint32 len, mtpAuthKey *authKey, const void *key128) { +inline void aesDecryptLocal(const void *src, void *dst, uint32 len, const mtpAuthKey *authKey, const void *key128) { MTPint256 aesKey, aesIV; authKey->prepareAES(*(const MTPint128*)key128, aesKey, aesIV, false); diff --git a/Telegram/SourceFiles/mtproto/mtpConnection.cpp b/Telegram/SourceFiles/mtproto/mtpConnection.cpp index 81b57745c4..5ed9f7419d 100644 --- a/Telegram/SourceFiles/mtproto/mtpConnection.cpp +++ b/Telegram/SourceFiles/mtproto/mtpConnection.cpp @@ -707,15 +707,6 @@ void MTPautoConnection::tcpSend(mtpBuffer &buffer) { TCP_LOG(("TCP Info: write %1 packet %2 bytes").arg(packetNum).arg(len)); sock.write((const char*)&buffer[0], len); - //int64 b = sock.bytesToWrite(); - //if (b > 100000) { - // int a = 0; - //} - //sock.flush(); - //int64 b2 = sock.bytesToWrite(); - //if (b2 > 0) { - // TCP_LOG(("TCP Info: writing many, %1 left to write").arg(b2)); - //} } void MTPautoConnection::httpSend(mtpBuffer &buffer) { @@ -1127,7 +1118,9 @@ MTProtoConnectionPrivate::MTProtoConnectionPrivate(QThread *thread, MTProtoConne connect(this, SIGNAL(needToReceive()), sessionData->owner(), SLOT(tryToReceive())); connect(this, SIGNAL(stateChanged(qint32)), sessionData->owner(), SLOT(onConnectionStateChange(qint32))); connect(sessionData->owner(), SIGNAL(needToSend()), this, SLOT(tryToSend())); + connect(this, SIGNAL(needToSendAsync()), sessionData->owner(), SIGNAL(needToSend())); connect(this, SIGNAL(sessionResetDone()), sessionData->owner(), SLOT(onResetDone())); + connect(this, SIGNAL(sendAnythingAsync(quint64)), sessionData->owner(), SLOT(sendAnything(quint64))); } void MTProtoConnectionPrivate::onConfigLoaded() { @@ -1415,7 +1408,7 @@ void MTProtoConnectionPrivate::tryToSend() { toSendPingId = 0; } else { int32 st = getState(); - DEBUG_LOG(("MTP Info: trying to send after ping, state: %1").arg(st)); + DEBUG_LOG(("MTP Info: dc %1 trying to send after ping, state: %2").arg(dc).arg(st)); if (st != MTProtoConnection::Connected) { return; // just do nothing, if is not connected yet } @@ -1960,7 +1953,7 @@ void MTProtoConnectionPrivate::handleReceived() { uint32 toAckSize = ackRequestData.size(); if (toAckSize) { DEBUG_LOG(("MTP Info: will send %1 acks, ids: %2").arg(toAckSize).arg(logVectorLong(ackRequestData))); - sessionData->owner()->sendAnything(MTPAckSendWaiting); + emit sendAnythingAsync(MTPAckSendWaiting); } bool emitSignal = false; @@ -1988,7 +1981,7 @@ void MTProtoConnectionPrivate::handleReceived() { if (!wasConnected) { if (getState() == MTProtoConnection::Connected) { - emit sessionData->owner()->needToSendAsync(); + emit needToSendAsync(); } } } @@ -3199,7 +3192,7 @@ void MTProtoConnectionPrivate::authKeyCreated() { toSendPingId = MTP::nonce(); // get server_salt - emit sessionData->owner()->needToSendAsync(); + emit needToSendAsync(); // disconnect(&pinger, SIGNAL(timeout()), 0, 0); // connect(&pinger, SIGNAL(timeout()), this, SLOT(sendPing())); diff --git a/Telegram/SourceFiles/mtproto/mtpConnection.h b/Telegram/SourceFiles/mtproto/mtpConnection.h index 313e46e2e9..4d0e99426e 100644 --- a/Telegram/SourceFiles/mtproto/mtpConnection.h +++ b/Telegram/SourceFiles/mtproto/mtpConnection.h @@ -309,6 +309,8 @@ signals: void needToRestart(); void stateChanged(qint32 newState); void sessionResetDone(); + void needToSendAsync(); + void sendAnythingAsync(quint64); public slots: diff --git a/Telegram/SourceFiles/mtproto/mtpDC.cpp b/Telegram/SourceFiles/mtproto/mtpDC.cpp index ad61522870..2d19de2aae 100644 --- a/Telegram/SourceFiles/mtproto/mtpDC.cpp +++ b/Telegram/SourceFiles/mtproto/mtpDC.cpp @@ -19,6 +19,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "mtpDC.h" #include "mtp.h" +#include "localstorage.h" + namespace { MTProtoDCMap gDCs; @@ -65,7 +67,7 @@ namespace { QByteArray data, decrypted; stream >> data; - if (!MTP::localKey().created()) { + if (!Local::oldKey().created()) { LOG(("MTP Error: reading encrypted keys without local key!")); continue; } @@ -77,7 +79,7 @@ namespace { uint32 fullDataLen = data.size() - 16; decrypted.resize(fullDataLen); const char *dataKey = data.constData(), *encrypted = data.constData() + 16; - aesDecryptLocal(encrypted, decrypted.data(), fullDataLen, &MTP::localKey(), dataKey); + aesDecryptLocal(encrypted, decrypted.data(), fullDataLen, &Local::oldKey(), dataKey); uchar sha1Buffer[20]; if (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), dataKey, 16)) { LOG(("MTP Error: bad decrypt key, data from user-config not decrypted")); @@ -271,7 +273,7 @@ namespace { } QByteArray encrypted(16 + fullSize, Qt::Uninitialized); // 128bit of sha1 - key128, sizeof(data), data hashSha1(toEncrypt.constData(), toEncrypt.size(), encrypted.data()); - aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 16, fullSize, &MTP::localKey(), encrypted.constData()); + aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 16, fullSize, &Local::oldKey(), encrypted.constData()); DEBUG_LOG(("MTP Info: keys file opened for writing %1 keys").arg(keysToWrite.size())); QDataStream keysStream(&keysFile); diff --git a/Telegram/SourceFiles/mtproto/mtpFileLoader.cpp b/Telegram/SourceFiles/mtproto/mtpFileLoader.cpp index 62d12d9616..4d7145921a 100644 --- a/Telegram/SourceFiles/mtproto/mtpFileLoader.cpp +++ b/Telegram/SourceFiles/mtproto/mtpFileLoader.cpp @@ -20,6 +20,7 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "window.h" #include "application.h" +#include "localstorage.h" namespace { int32 _priority = 1; @@ -44,9 +45,9 @@ namespace { } mtpFileLoader::mtpFileLoader(int32 dc, const int64 &volume, int32 local, const int64 &secret, int32 size) : prev(0), next(0), -priority(0), inQueue(false), complete(false), skippedBytes(0), nextRequestOffset(0), lastComplete(false), +priority(0), inQueue(false), complete(false), triedLocal(false), skippedBytes(0), nextRequestOffset(0), lastComplete(false), dc(dc), locationType(0), volume(volume), local(local), secret(secret), -id(0), access(0), fileIsOpen(false), size(size), type(MTP_storage_fileUnknown()) { +id(0), access(0), fileIsOpen(false), size(size), type(mtpc_storage_fileUnknown) { LoaderQueues::iterator i = queues.find(dc); if (i == queues.cend()) { i = queues.insert(dc, mtpFileLoaderQueue()); @@ -55,9 +56,9 @@ id(0), access(0), fileIsOpen(false), size(size), type(MTP_storage_fileUnknown()) } mtpFileLoader::mtpFileLoader(int32 dc, const uint64 &id, const uint64 &access, mtpTypeId locType, const QString &to, int32 size) : prev(0), next(0), -priority(0), inQueue(false), complete(false), skippedBytes(0), nextRequestOffset(0), lastComplete(false), +priority(0), inQueue(false), complete(false), triedLocal(false), skippedBytes(0), nextRequestOffset(0), lastComplete(false), dc(dc), locationType(locType), -id(id), access(access), file(to), fname(to), fileIsOpen(false), duplicateInData(false), size(size), type(MTP_storage_fileUnknown()) { +id(id), access(access), file(to), fname(to), fileIsOpen(false), duplicateInData(false), size(size), type(mtpc_storage_fileUnknown) { LoaderQueues::iterator i = queues.find(MTP::dld[0] + dc); if (i == queues.cend()) { i = queues.insert(MTP::dld[0] + dc, mtpFileLoaderQueue()); @@ -66,9 +67,9 @@ id(id), access(access), file(to), fname(to), fileIsOpen(false), duplicateInData( } mtpFileLoader::mtpFileLoader(int32 dc, const uint64 &id, const uint64 &access, mtpTypeId locType, const QString &to, int32 size, bool todata) : prev(0), next(0), -priority(0), inQueue(false), complete(false), skippedBytes(0), nextRequestOffset(0), lastComplete(false), +priority(0), inQueue(false), complete(false), triedLocal(false), skippedBytes(0), nextRequestOffset(0), lastComplete(false), dc(dc), locationType(locType), -id(id), access(access), file(to), fname(to), fileIsOpen(false), duplicateInData(todata), size(size), type(MTP_storage_fileUnknown()) { +id(id), access(access), file(to), fname(to), fileIsOpen(false), duplicateInData(todata), size(size), type(mtpc_storage_fileUnknown) { LoaderQueues::iterator i = queues.find(MTP::dld[0] + dc); if (i == queues.cend()) { i = queues.insert(MTP::dld[0] + dc, mtpFileLoaderQueue()); @@ -85,7 +86,7 @@ bool mtpFileLoader::done() const { } mtpTypeId mtpFileLoader::fileType() const { - return type.type(); + return type; } const QByteArray &mtpFileLoader::bytes() const { @@ -130,7 +131,7 @@ void mtpFileLoader::loadNext() { void mtpFileLoader::finishFail() { bool started = currentOffset(true) > 0; cancelRequests(); - type = MTP_storage_fileUnknown(); + type = mtpc_storage_fileUnknown; complete = true; if (fileIsOpen) { file.close(); @@ -186,7 +187,7 @@ bool mtpFileLoader::loadPart() { void mtpFileLoader::partLoaded(int32 offset, const MTPupload_File &result, mtpRequestId req) { Requests::iterator i = requests.find(req); - if (i == requests.cend()) return; + if (i == requests.cend()) return loadNext(); int32 limit = locationType ? DocumentDownloadPartSize : DownloadPartSize; int32 dcIndex = i.value(); @@ -239,7 +240,7 @@ void mtpFileLoader::partLoaded(int32 offset, const MTPupload_File &result, mtpRe return finishFail(); } } - type = d.vtype; + type = d.vtype.type(); complete = true; if (fileIsOpen) { file.close(); @@ -249,10 +250,13 @@ void mtpFileLoader::partLoaded(int32 offset, const MTPupload_File &result, mtpRe removeFromQueue(); App::wnd()->update(); App::wnd()->notifyUpdateAllPhotos(); - if (!queue->queries && dcIndex) { App::app()->killDownloadSessionsStart(dc); } + + if (!locationType && triedLocal && (fname.isEmpty() || duplicateInData)) { + Local::writeImage(storageKey(dc, volume, local), StorageImageSaved(type, data)); + } } emit progress(this); loadNext(); @@ -287,12 +291,38 @@ void mtpFileLoader::pause() { void mtpFileLoader::start(bool loadFirst, bool prior) { if (complete) return; + if (!locationType && !triedLocal) { + triedLocal = true; + StorageImageSaved cached = Local::readImage(storageKey(dc, volume, local)); + if (cached.type != mtpc_storage_fileUnknown) { + data = cached.data; + if (!fname.isEmpty() && duplicateInData) { + if (!fileIsOpen) fileIsOpen = file.open(QIODevice::WriteOnly); + if (!fileIsOpen) { + return finishFail(); + } + if (file.write(data) != qint64(data.size())) { + return finishFail(); + } + } + type = cached.type; + complete = true; + if (fileIsOpen) { + file.close(); + fileIsOpen = false; + psPostprocessFile(QFileInfo(file).absoluteFilePath()); + } + App::wnd()->update(); + App::wnd()->notifyUpdateAllPhotos(); + emit progress(this); + return loadNext(); + } + } if (!fname.isEmpty() && !duplicateInData && !fileIsOpen) { fileIsOpen = file.open(QIODevice::WriteOnly); if (!fileIsOpen) { - finishFail(); - return; + return finishFail(); } } @@ -386,7 +416,7 @@ void mtpFileLoader::start(bool loadFirst, bool prior) { void mtpFileLoader::cancel() { cancelRequests(); - type = MTP_storage_fileUnknown(); + type = mtpc_storage_fileUnknown; complete = true; if (fileIsOpen) { file.close(); diff --git a/Telegram/SourceFiles/mtproto/mtpFileLoader.h b/Telegram/SourceFiles/mtproto/mtpFileLoader.h index 99781f6946..709b0ba2a5 100644 --- a/Telegram/SourceFiles/mtproto/mtpFileLoader.h +++ b/Telegram/SourceFiles/mtproto/mtpFileLoader.h @@ -60,7 +60,7 @@ signals: private: mtpFileLoaderQueue *queue; - bool inQueue, complete; + bool inQueue, complete, triedLocal; void cancelRequests(); @@ -96,6 +96,6 @@ private: QByteArray data; int32 size; - MTPstorage_FileType type; + mtpTypeId type; }; diff --git a/Telegram/SourceFiles/mtproto/mtpSession.cpp b/Telegram/SourceFiles/mtproto/mtpSession.cpp index 972ec83d0a..b58fe7652d 100644 --- a/Telegram/SourceFiles/mtproto/mtpSession.cpp +++ b/Telegram/SourceFiles/mtproto/mtpSession.cpp @@ -83,9 +83,6 @@ void MTProtoSession::start(int32 dcenter, uint32 connects) { timeouter.start(1000); connect(&sender, SIGNAL(timeout()), this, SIGNAL(needToSend())); - connect(this, SIGNAL(startSendTimer(int)), &sender, SLOT(start(int))); - connect(this, SIGNAL(stopSendTimer()), &sender, SLOT(stop())); - connect(this, SIGNAL(needToSendAsync()), this, SIGNAL(needToSend())); MTProtoDCMap &dcs(mtpDCMap()); @@ -135,7 +132,7 @@ void MTProtoSession::stop() { } } -void MTProtoSession::sendAnything(uint64 msCanWait) { +void MTProtoSession::sendAnything(quint64 msCanWait) { uint64 ms = getms(true); if (msSendCall) { if (ms > msSendCall + msWait) { @@ -150,13 +147,14 @@ void MTProtoSession::sendAnything(uint64 msCanWait) { msWait = msCanWait; } if (msWait) { + DEBUG_LOG(("MTP Info: dc %1 can wait for %2ms from current %3").arg(dcId).arg(msWait).arg(msSendCall)); msSendCall = ms; - emit startSendTimer(msWait); - DEBUG_LOG(("MTP Info: can wait for %1ms from current %2").arg(msWait).arg(msSendCall)); + sender.start(msWait); } else { - emit stopSendTimer(); + DEBUG_LOG(("MTP Info: dc %1 stopped send timer, can wait for %2ms from current %3").arg(dcId).arg(msWait).arg(msSendCall)); + sender.stop(); msSendCall = 0; - emit needToSendAsync(); + emit needToSend(); } } diff --git a/Telegram/SourceFiles/mtproto/mtpSession.h b/Telegram/SourceFiles/mtproto/mtpSession.h index 7d970cd436..372d8a642f 100644 --- a/Telegram/SourceFiles/mtproto/mtpSession.h +++ b/Telegram/SourceFiles/mtproto/mtpSession.h @@ -235,7 +235,6 @@ public: template mtpRequestId send(const TRequest &request, RPCResponseHandler callbacks = RPCResponseHandler(), uint64 msCanWait = 0, bool needsLayer = false, bool toMainDC = false, mtpRequestId after = 0); // send mtp request - void sendAnything(uint64 msCanWait); void cancel(mtpRequestId requestId, mtpMsgId msgId); int32 requestState(mtpRequestId requestId) const; @@ -253,10 +252,6 @@ signals: void authKeyCreated(); void needToSend(); - void needToSendAsync(); // emit this signal, to emit needToSend() in MTProtoSession thread - - void startSendTimer(int msec); // manipulating timer from all threads - void stopSendTimer(); public slots: @@ -268,6 +263,8 @@ public slots: void onConnectionStateChange(qint32 newState); void onResetDone(); + void sendAnything(quint64 msCanWait); + private: typedef QList MTProtoConnections; diff --git a/Telegram/SourceFiles/profilewidget.cpp b/Telegram/SourceFiles/profilewidget.cpp index 8e781e2570..bf8a071b07 100644 --- a/Telegram/SourceFiles/profilewidget.cpp +++ b/Telegram/SourceFiles/profilewidget.cpp @@ -367,7 +367,7 @@ void ProfileInner::reorderParticipants() { } else { _participants.clear(); if (_peerUser) { - _onlineText = App::onlineText(_peerUser->onlineTill, t, true); + _onlineText = App::onlineText(_peerUser, t, true); } else { _onlineText = lang(lng_chat_no_members); } @@ -520,7 +520,7 @@ void ProfileInner::paintEvent(QPaintEvent *e) { if (!data) { data = _participantsData[cnt] = new ParticipantData(); data->name.setText(st::profileListNameFont, user->name, _textNameOptions); - data->online = App::onlineText(user->onlineTill, l_time); + data->online = App::onlineText(user, l_time); data->cankick = (user != App::self()) && (_chatAdmin || (_peerChat->cankick.constFind(user) != _peerChat->cankick.cend())); } p.setPen(st::profileListNameColor->p); diff --git a/Telegram/SourceFiles/settingswidget.cpp b/Telegram/SourceFiles/settingswidget.cpp index 119f116041..5f1a3ad39a 100644 --- a/Telegram/SourceFiles/settingswidget.cpp +++ b/Telegram/SourceFiles/settingswidget.cpp @@ -31,6 +31,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "boxes/usernamebox.h" #include "gui/filedialog.h" +#include "localstorage.h" + Slider::Slider(QWidget *parent, const style::slider &st, int32 count, int32 sel) : QWidget(parent), _count(count), _sel(snap(sel, 0, _count)), _wasSel(_sel), _st(st), _pressed(false) { resize(_st.width, _st.bar.pxHeight()); @@ -154,6 +156,12 @@ SettingsInner::SettingsInner(SettingsWidget *parent) : QWidget(parent), _catsAndDogs(this, lang(lng_settings_cats_and_dogs), cCatsAndDogs()), + // local storage + _localImagesClear(this, lang(lng_local_images_clear)), + _imagesClearingWidth(st::linkFont->m.width(lang(lng_local_images_clearing))), + _imagesClearedWidth(st::linkFont->m.width(lang(lng_local_images_cleared))), + _imagesClearFailedWidth(st::linkFont->m.width(lang(lng_local_images_clear_failed))), + // advanced _connectionType(this, lang(lng_connection_auto)), _resetSessions(this, lang(lng_settings_reset)), @@ -231,11 +239,19 @@ SettingsInner::SettingsInner(SettingsWidget *parent) : QWidget(parent), case Window::TempDirExists: _tempDirClearState = TempDirExists; break; case Window::TempDirRemoving: _tempDirClearState = TempDirClearing; break; } - connect(App::wnd(), SIGNAL(tempDirCleared()), this, SLOT(onTempDirCleared())); - connect(App::wnd(), SIGNAL(tempDirClearFailed()), this, SLOT(onTempDirClearFailed())); + connect(App::wnd(), SIGNAL(tempDirCleared(int)), this, SLOT(onTempDirCleared(int))); + connect(App::wnd(), SIGNAL(tempDirClearFailed(int)), this, SLOT(onTempDirClearFailed(int))); connect(&_catsAndDogs, SIGNAL(changed()), this, SLOT(onCatsAndDogs())); + // local storage + connect(&_localImagesClear, SIGNAL(clicked()), this, SLOT(onLocalImagesClear())); + switch (App::wnd()->localImagesState()) { + case Window::TempDirEmpty: _imagesClearState = TempDirEmpty; break; + case Window::TempDirExists: _imagesClearState = TempDirExists; break; + case Window::TempDirRemoving: _imagesClearState = TempDirClearing; break; + } + // advanced connect(&_connectionType, SIGNAL(clicked()), this, SLOT(onConnectionType())); connect(&_resetSessions, SIGNAL(clicked()), this, SLOT(onResetSessions())); @@ -456,14 +472,40 @@ void SettingsInner::paintEvent(QPaintEvent *e) { top += st::setSectionSkip; top += _catsAndDogs.height(); + + // local storage + p.setFont(st::setHeaderFont->f); + p.setPen(st::setHeaderColor->p); + p.drawText(_left + st::setHeaderLeft, top + st::setHeaderTop + st::setHeaderFont->ascent, lang(lng_settings_section_cache)); + top += st::setHeaderSkip; + + QString localImagesText = lang(lng_settings_no_images_cached); + int32 cnt = Local::hasImages(); + if (cnt) { + localImagesText = lang((cnt > 1) ? lng_settings_images_cached : lng_settings_image_cached).replace(qsl("{count}"), QString::number(cnt)).replace(qsl("{size}"), formatSizeText(Local::storageFilesSize())); + } + p.setFont(st::linkFont->f); + p.setPen(st::black->p); + p.drawText(_left + st::setHeaderLeft, top + st::linkFont->ascent, localImagesText); + QString clearText; + int32 clearWidth = 0; + switch (_imagesClearState) { + case TempDirClearing: clearText = lang(lng_local_images_clearing); clearWidth = _imagesClearingWidth; break; + case TempDirCleared: clearText = lang(lng_local_images_cleared); clearWidth = _imagesClearedWidth; break; + case TempDirClearFailed: clearText = lang(lng_local_images_clear_failed); clearWidth = _imagesClearFailedWidth; break; + } + if (clearWidth) { + p.drawText(_left + st::setWidth - clearWidth, top + st::linkFont->ascent, clearText); + } + top += _localImagesClear.height(); } - + // advanced p.setFont(st::setHeaderFont->f); p.setPen(st::setHeaderColor->p); p.drawText(_left + st::setHeaderLeft, top + st::setHeaderTop + st::setHeaderFont->ascent, lang(lng_settings_section_advanced)); top += st::setHeaderSkip; - + p.setFont(st::linkFont->f); p.setPen(st::black->p); p.drawText(_left + st::setHeaderLeft, _connectionType.y() + st::linkFont->ascent, _connectionTypeText); @@ -541,6 +583,10 @@ void SettingsInner::resizeEvent(QResizeEvent *e) { } top += st::setSectionSkip; _catsAndDogs.move(_left, top); top += _catsAndDogs.height(); + + // local storage + top += st::setHeaderSkip; + _localImagesClear.move(_left + st::setWidth - _localImagesClear.width(), top); top += _localImagesClear.height(); } // advanced @@ -754,6 +800,7 @@ void SettingsInner::showAll() { _downloadPathClear.hide(); } } + } else { _replaceEmojis.hide(); _viewEmojis.hide(); @@ -763,6 +810,14 @@ void SettingsInner::showAll() { _dontAskDownloadPath.hide(); _downloadPathEdit.hide(); _downloadPathClear.hide(); + _localImagesClear.hide(); + } + + // local storage + if (self() && _imagesClearState == TempDirExists) { + _localImagesClear.show(); + } else { + _localImagesClear.hide(); } // advanced @@ -1103,20 +1158,35 @@ void SettingsInner::onDownloadPathClear() { void SettingsInner::onDownloadPathClearSure() { App::wnd()->hideLayer(); - App::wnd()->tempDirDelete(); + App::wnd()->tempDirDelete(Local::ClearManagerDownloads); _tempDirClearState = TempDirClearing; showAll(); update(); } -void SettingsInner::onTempDirCleared() { - _tempDirClearState = TempDirCleared; +void SettingsInner::onLocalImagesClear() { + App::wnd()->tempDirDelete(Local::ClearManagerImages); + _imagesClearState = TempDirClearing; showAll(); update(); } -void SettingsInner::onTempDirClearFailed() { - _tempDirClearState = TempDirClearFailed; +void SettingsInner::onTempDirCleared(int task) { + if (task & Local::ClearManagerDownloads) { + _tempDirClearState = TempDirCleared; + } else if (task & Local::ClearManagerImages) { + _imagesClearState = TempDirCleared; + } + showAll(); + update(); +} + +void SettingsInner::onTempDirClearFailed(int task) { + if (task & Local::ClearManagerDownloads) { + _tempDirClearState = TempDirClearFailed; + } else if (task & Local::ClearManagerImages) { + _imagesClearState = TempDirClearFailed; + } showAll(); update(); } diff --git a/Telegram/SourceFiles/settingswidget.h b/Telegram/SourceFiles/settingswidget.h index be325756db..8f96b382d3 100644 --- a/Telegram/SourceFiles/settingswidget.h +++ b/Telegram/SourceFiles/settingswidget.h @@ -122,11 +122,13 @@ public slots: void onDownloadPathEdited(); void onDownloadPathClear(); void onDownloadPathClearSure(); - void onTempDirCleared(); - void onTempDirClearFailed(); + void onTempDirCleared(int task); + void onTempDirClearFailed(int task); void onCatsAndDogs(); + void onLocalImagesClear(); + void onUpdateChecking(); void onUpdateLatest(); void onUpdateDownloading(qint64 ready, qint64 total); @@ -212,6 +214,11 @@ private: TempDirClearState _tempDirClearState; FlatCheckbox _catsAndDogs; + // local storage + LinkButton _localImagesClear; + int32 _imagesClearingWidth, _imagesClearedWidth, _imagesClearFailedWidth; + TempDirClearState _imagesClearState; + // advanced LinkButton _connectionType, _resetSessions; FlatButton _logOut; diff --git a/Telegram/SourceFiles/types.cpp b/Telegram/SourceFiles/types.cpp index fb75606301..d40e9d9f1a 100644 --- a/Telegram/SourceFiles/types.cpp +++ b/Telegram/SourceFiles/types.cpp @@ -17,6 +17,8 @@ Copyright (c) 2014 John Preston, https://tdesktop.com */ #include "stdafx.h" +#include "application.h" + #ifdef Q_OS_WIN #elif defined Q_OS_MAC #include @@ -216,7 +218,7 @@ bool checkms() { _msAddToUnixtime = ((ms - unixms) / 1000LL) * 1000LL; } else if (unixms > ms + 1000LL) { _msAddToMsStart += ((unixms - ms) / 1000LL) * 1000LL; - adjustSingleTimers(); + if (App::app()) emit App::app()->adjustSingleTimers(); return true; } return false; @@ -243,27 +245,24 @@ uint64 getms(bool checked) { #endif } -namespace { - QSet _activeSingleTimers; - QMutex _activeSingleTimersMutex; -} - -void regSingleTimer(SingleTimer *timer) { - QMutexLocker lock(&_activeSingleTimersMutex); - _activeSingleTimers.insert(timer); -} - -void unregSingleTimer(SingleTimer *timer) { - QMutexLocker lock(&_activeSingleTimersMutex); - _activeSingleTimers.remove(timer); -} - -void adjustSingleTimers() { - for (QSet::const_iterator i = _activeSingleTimers.cbegin(), e = _activeSingleTimers.cend(); i != e; ++i) { - emit (*i)->callAdjust(); +SingleTimer::SingleTimer() : _finishing(0), _inited(false) { + QTimer::setSingleShot(true); + if (App::app()) { + connect(App::app(), SIGNAL(adjustSingleTimers()), this, SLOT(adjust())); + _inited = true; } } +void SingleTimer::start(int msec) { + _finishing = getms(true) + (msec < 0 ? 0 : uint64(msec)); + if (!_inited && App::app()) { + connect(App::app(), SIGNAL(adjustSingleTimers()), this, SLOT(adjust())); + _inited = true; + } + QTimer::start(msec); +} + + uint64 msgid() { #ifdef Q_OS_WIN LARGE_INTEGER li; diff --git a/Telegram/SourceFiles/types.h b/Telegram/SourceFiles/types.h index 25777d5701..342e96fb65 100644 --- a/Telegram/SourceFiles/types.h +++ b/Telegram/SourceFiles/types.h @@ -101,45 +101,19 @@ inline void mylocaltime(struct tm * _Tm, const time_t * _Time) { bool checkms(); // returns true if time has changed uint64 getms(bool checked = false); -class SingleTimer; -void regSingleTimer(SingleTimer *timer); -void unregSingleTimer(SingleTimer *timer); -void adjustSingleTimers(); - class SingleTimer : public QTimer { // single shot timer with check Q_OBJECT public: - SingleTimer() : _finishing(0) { - QTimer::setSingleShot(true); - connect(this, SIGNAL(callAdjust()), this, SLOT(adjust())); - connect(this, SIGNAL(timeout()), this, SLOT(unreg())); - } - - void start(int msec) { - _finishing = getms(true) + (msec < 0 ? 0 : uint64(msec)); - QTimer::start(msec); - regSingleTimer(this); - } - void stop() { - QTimer::stop(); - unreg(); - } + SingleTimer(); void setSingleShot(bool); // is not available void start(); // is not available - ~SingleTimer() { - unreg(); - } - -signals: - - void callAdjust(); - public slots: + void start(int msec); void adjust() { uint64 n = getms(true); if (isActive()) { @@ -150,12 +124,10 @@ public slots: } } } - void unreg() { - unregSingleTimer(this); - } private: uint64 _finishing; + bool _inited; }; diff --git a/Telegram/SourceFiles/window.cpp b/Telegram/SourceFiles/window.cpp index 6adc30ebfa..c7aecc906d 100644 --- a/Telegram/SourceFiles/window.cpp +++ b/Telegram/SourceFiles/window.cpp @@ -31,6 +31,7 @@ Copyright (c) 2014 John Preston, https://tdesktop.com #include "boxes/confirmbox.h" #include "mediaview.h" +#include "localstorage.h" ConnectingWidget::ConnectingWidget(QWidget *parent, const QString &text, const QString &reconnect) : QWidget(parent), _shadow(st::boxShadow), _reconnect(this, QString()) { set(text, reconnect); @@ -66,20 +67,6 @@ void ConnectingWidget::onReconnect() { MTP::restart(); } -TempDirDeleter::TempDirDeleter(QThread *thread) { - moveToThread(thread); - connect(thread, SIGNAL(started()), this, SLOT(onStart())); -} - -void TempDirDeleter::onStart() { - if (QDir(cTempDir()).removeRecursively()) { - emit succeed(); - } else { - emit failed(); - } -} - - NotifyWindow::NotifyWindow(HistoryItem *msg, int32 x, int32 y) : history(msg->history()), item(msg) #ifdef Q_OS_WIN , started(GetTickCount()) @@ -338,7 +325,7 @@ NotifyWindow::~NotifyWindow() { Window::Window(QWidget *parent) : PsMainWindow(parent), intro(0), main(0), settings(0), layerBG(0), _topWidget(0), -_connecting(0), _tempDeleter(0), _tempDeleterThread(0), dragging(false), _inactivePress(false), _mediaView(0) { +_connecting(0), _clearManager(0), dragging(false), _inactivePress(false), _mediaView(0) { icon16 = icon256.scaledToWidth(16, Qt::SmoothTransformation); icon32 = icon256.scaledToWidth(32, Qt::SmoothTransformation); @@ -944,40 +931,56 @@ void Window::resizeEvent(QResizeEvent *e) { } Window::TempDirState Window::tempDirState() { - if (_tempDeleter) { + if (_clearManager && _clearManager->hasTask(Local::ClearManagerDownloads)) { return TempDirRemoving; } return QDir(cTempDir()).exists() ? TempDirExists : TempDirEmpty; } -void Window::tempDirDelete() { - if (_tempDeleter) return; - _tempDeleterThread = new QThread(); - _tempDeleter = new TempDirDeleter(_tempDeleterThread); - connect(_tempDeleter, SIGNAL(succeed()), this, SLOT(onTempDirCleared())); - connect(_tempDeleter, SIGNAL(failed()), this, SLOT(onTempDirClearFailed())); - _tempDeleterThread->start(); +Window::TempDirState Window::localImagesState() { + if (_clearManager && _clearManager->hasTask(Local::ClearManagerImages)) { + return TempDirRemoving; + } + return Local::hasImages() ? TempDirExists : TempDirEmpty; } -void Window::onTempDirCleared() { - _tempDeleter->deleteLater(); - _tempDeleter = 0; - _tempDeleterThread->deleteLater(); - _tempDeleterThread = 0; - emit tempDirCleared(); +void Window::tempDirDelete(int task) { + if (_clearManager) { + if (_clearManager->addTask(task)) { + return; + } else { + _clearManager->deleteLater(); + _clearManager = 0; + } + } + _clearManager = new Local::ClearManager(); + _clearManager->addTask(task); + connect(_clearManager, SIGNAL(succeed(int,void*)), this, SLOT(onClearFinished(int,void*))); + connect(_clearManager, SIGNAL(failed(int,void*)), this, SLOT(onClearFailed(int,void*))); + _clearManager->start(); } -void Window::onTempDirClearFailed() { - _tempDeleter->deleteLater(); - _tempDeleter = 0; - _tempDeleterThread->deleteLater(); - _tempDeleterThread = 0; - emit tempDirClearFailed(); +void Window::onClearFinished(int task, void *manager) { + if (manager && manager == _clearManager) { + _clearManager->deleteLater(); + _clearManager = 0; + } + emit tempDirCleared(task); +} + +void Window::onClearFailed(int task, void *manager) { + if (manager && manager == _clearManager) { + _clearManager->deleteLater(); + _clearManager = 0; + } + emit tempDirClearFailed(task); } void Window::quit() { delete _mediaView; _mediaView = 0; + delete main; + main = 0; notifyClearFast(); } @@ -1405,8 +1408,7 @@ void Window::changingMsgId(HistoryItem *row, MsgId newId) { Window::~Window() { notifyClearFast(); - delete _tempDeleter; - delete _tempDeleterThread; + delete _clearManager; delete _connecting; delete _mediaView; delete trayIcon; diff --git a/Telegram/SourceFiles/window.h b/Telegram/SourceFiles/window.h index 97e28baf06..e01bd4c062 100644 --- a/Telegram/SourceFiles/window.h +++ b/Telegram/SourceFiles/window.h @@ -29,6 +29,9 @@ class MainWidget; class SettingsWidget; class BackgroundWidget; class LayeredWidget; +namespace Local { + class ClearManager; +} class ConnectingWidget : public QWidget { Q_OBJECT @@ -52,21 +55,6 @@ private: }; -class TempDirDeleter : public QObject { - Q_OBJECT -public: - TempDirDeleter(QThread *thread); - -public slots: - void onStart(); - -signals: - void succeed(); - void failed(); - -}; - - class NotifyWindow : public QWidget, public Animated { Q_OBJECT @@ -205,7 +193,8 @@ public: TempDirEmpty, }; TempDirState tempDirState(); - void tempDirDelete(); + TempDirState localImagesState(); + void tempDirDelete(int task); void quit(); @@ -243,8 +232,8 @@ public slots: void onInactiveTimer(); - void onTempDirCleared(); - void onTempDirClearFailed(); + void onClearFinished(int task, void *manager); + void onClearFailed(int task, void *manager); void notifyFire(); void updateTrayMenu(bool force = false); @@ -257,8 +246,8 @@ public slots: signals: void resized(const QSize &size); - void tempDirCleared(); - void tempDirClearFailed(); + void tempDirCleared(int task); + void tempDirClearFailed(int task); protected: @@ -281,8 +270,7 @@ private: QWidget *_topWidget; // temp hack for CountrySelect ConnectingWidget *_connecting; - TempDirDeleter *_tempDeleter; - QThread *_tempDeleterThread; + Local::ClearManager *_clearManager; void clearWidgets(); diff --git a/Telegram/Telegram.plist b/Telegram/Telegram.plist index fd626455d8..2fabc80584 100644 --- a/Telegram/Telegram.plist +++ b/Telegram/Telegram.plist @@ -11,7 +11,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.6.11 + 0.6.12 CFBundleSignature ???? NOTE diff --git a/Telegram/Telegram.pro b/Telegram/Telegram.pro index 1f4c58aa75..25acb49d11 100644 --- a/Telegram/Telegram.pro +++ b/Telegram/Telegram.pro @@ -93,6 +93,7 @@ SOURCES += \ ./SourceFiles/overviewwidget.cpp \ ./SourceFiles/profilewidget.cpp \ ./SourceFiles/localimageloader.cpp \ + ./SourceFiles/localstorage.cpp \ ./SourceFiles/logs.cpp \ ./SourceFiles/mainwidget.cpp \ ./SourceFiles/settings.cpp \ @@ -168,6 +169,7 @@ HEADERS += \ ./SourceFiles/overviewwidget.h \ ./SourceFiles/profilewidget.h \ ./SourceFiles/localimageloader.h \ + ./SourceFiles/localstorage.h \ ./SourceFiles/logs.h \ ./SourceFiles/mainwidget.h \ ./SourceFiles/settings.h \ diff --git a/Telegram/Telegram.rc b/Telegram/Telegram.rc index 25fb32ece8..77ad32607d 100644 Binary files a/Telegram/Telegram.rc and b/Telegram/Telegram.rc differ diff --git a/Telegram/Telegram.vcxproj b/Telegram/Telegram.vcxproj index 2930574eb0..98b7965a25 100644 --- a/Telegram/Telegram.vcxproj +++ b/Telegram/Telegram.vcxproj @@ -270,6 +270,10 @@ true true + + true + true + true true @@ -486,6 +490,10 @@ true true + + true + true + true true @@ -711,6 +719,10 @@ true true + + true + true + true true @@ -852,6 +864,7 @@ + @@ -1502,6 +1515,20 @@ $(QTDIR)\bin\moc.exe;%(FullPath) + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing localstorage.h... + .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fstdafx.h" "-f../../SourceFiles/localstorage.h" -DAL_LIBTYPE_STATIC -DCUSTOM_API_ID -DUNICODE -D_WITH_DEBUG -DWIN32 -DWIN64 -DHAVE_STDINT_H -DZLIB_WINAPI -DQT_NO_DEBUG -DNDEBUG "-I.\..\..\Libraries\lzma\C" "-I.\..\..\Libraries\libexif-0.6.20" "-I.\..\..\Libraries\zlib-1.2.8" "-I.\..\..\Libraries\OpenSSL-Win32\include" "-I.\..\..\Libraries\libogg-1.3.2\include" "-I.\..\..\Libraries\opus\include" "-I.\..\..\Libraries\opusfile\include" "-I.\..\..\Libraries\openal-soft\include" "-I.\SourceFiles" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I.\..\..\Libraries\QtStatic\qtbase\include\QtCore\5.3.1\QtCore" "-I.\..\..\Libraries\QtStatic\qtbase\include\QtGui\5.3.1\QtGui" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing localstorage.h... + .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fstdafx.h" "-f../../SourceFiles/localstorage.h" -DAL_LIBTYPE_STATIC -DUNICODE -DWIN32 -DWIN64 -DHAVE_STDINT_H -DZLIB_WINAPI "-I.\..\..\Libraries\lzma\C" "-I.\..\..\Libraries\libexif-0.6.20" "-I.\..\..\Libraries\zlib-1.2.8" "-I.\..\..\Libraries\OpenSSL-Win32\include" "-I.\..\..\Libraries\libogg-1.3.2\include" "-I.\..\..\Libraries\opus\include" "-I.\..\..\Libraries\opusfile\include" "-I.\..\..\Libraries\openal-soft\include" "-I.\SourceFiles" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I.\..\..\Libraries\QtStatic\qtbase\include\QtCore\5.3.1\QtCore" "-I.\..\..\Libraries\QtStatic\qtbase\include\QtGui\5.3.1\QtGui" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing localstorage.h... + .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fstdafx.h" "-f../../SourceFiles/localstorage.h" -DAL_LIBTYPE_STATIC -DUNICODE -D_WITH_DEBUG -DWIN32 -DWIN64 -DHAVE_STDINT_H -DZLIB_WINAPI -DQT_NO_DEBUG -DNDEBUG "-I.\..\..\Libraries\lzma\C" "-I.\..\..\Libraries\libexif-0.6.20" "-I.\..\..\Libraries\zlib-1.2.8" "-I.\..\..\Libraries\OpenSSL-Win32\include" "-I.\..\..\Libraries\libogg-1.3.2\include" "-I.\..\..\Libraries\opus\include" "-I.\..\..\Libraries\opusfile\include" "-I.\..\..\Libraries\openal-soft\include" "-I.\SourceFiles" "-I.\GeneratedFiles" "-I." "-I$(QTDIR)\include" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I.\..\..\Libraries\QtStatic\qtbase\include\QtCore\5.3.1\QtCore" "-I.\..\..\Libraries\QtStatic\qtbase\include\QtGui\5.3.1\QtGui" + Moc%27ing mtpConnection.h... diff --git a/Telegram/Telegram.vcxproj.filters b/Telegram/Telegram.vcxproj.filters index 52a8752892..c06ca8714e 100644 --- a/Telegram/Telegram.vcxproj.filters +++ b/Telegram/Telegram.vcxproj.filters @@ -752,6 +752,18 @@ Generated Files\Release + + Source Files + + + Generated Files\Deploy + + + Generated Files\Debug + + + Generated Files\Release + @@ -1005,6 +1017,9 @@ Source Files + + Source Files + diff --git a/Telegram/Telegram.xcodeproj/project.pbxproj b/Telegram/Telegram.xcodeproj/project.pbxproj index bd4359d8b8..184a356471 100644 --- a/Telegram/Telegram.xcodeproj/project.pbxproj +++ b/Telegram/Telegram.xcodeproj/project.pbxproj @@ -1521,7 +1521,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0.6.11; + CURRENT_PROJECT_VERSION = 0.6.12; DEBUG_INFORMATION_FORMAT = dwarf; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_OPTIMIZATION_LEVEL = 0; @@ -1539,7 +1539,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 0.6.11; + CURRENT_PROJECT_VERSION = 0.6.12; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_OPTIMIZATION_LEVEL = fast; GCC_PREFIX_HEADER = ./SourceFiles/stdafx.h; @@ -1565,10 +1565,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0.6.11; + CURRENT_PROJECT_VERSION = 0.6.12; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_COMPATIBILITY_VERSION = 0.6; - DYLIB_CURRENT_VERSION = 0.6.11; + DYLIB_CURRENT_VERSION = 0.6.12; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = ""; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; @@ -1708,10 +1708,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0.6.11; + CURRENT_PROJECT_VERSION = 0.6.12; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_COMPATIBILITY_VERSION = 0.6; - DYLIB_CURRENT_VERSION = 0.6.11; + DYLIB_CURRENT_VERSION = 0.6.12; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = ""; GCC_GENERATE_DEBUGGING_SYMBOLS = YES;