From 938707203c6f151735609c281da04bdd17d24c0d Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 24 Mar 2015 13:00:27 +0300 Subject: [PATCH] 0.7.24.dev version with hashtags autocomplete, forwarding with comment and move back to reply by bottom arrow --- Telegram/PrepareWin.bat | 10 +- Telegram/Resources/lang.strings | 2 + Telegram/Resources/style.txt | 1 + Telegram/SourceFiles/app.cpp | 2 +- Telegram/SourceFiles/application.cpp | 2 +- Telegram/SourceFiles/config.h | 6 +- Telegram/SourceFiles/dialogswidget.cpp | 263 ++++++++++++++---- Telegram/SourceFiles/dialogswidget.h | 14 + Telegram/SourceFiles/dropdown.cpp | 130 +++++---- Telegram/SourceFiles/dropdown.h | 9 +- Telegram/SourceFiles/gui/flattextarea.cpp | 40 +-- Telegram/SourceFiles/gui/flattextarea.h | 4 +- Telegram/SourceFiles/gui/text.cpp | 28 +- Telegram/SourceFiles/gui/text.h | 2 + Telegram/SourceFiles/history.cpp | 6 +- Telegram/SourceFiles/historywidget.cpp | 283 ++++++++++++-------- Telegram/SourceFiles/historywidget.h | 20 +- Telegram/SourceFiles/localstorage.cpp | 108 +++++++- Telegram/SourceFiles/localstorage.h | 3 + Telegram/SourceFiles/mainwidget.cpp | 168 +++++++++++- Telegram/SourceFiles/mainwidget.h | 18 +- Telegram/SourceFiles/overviewwidget.cpp | 2 +- Telegram/SourceFiles/settings.cpp | 2 + Telegram/SourceFiles/settings.h | 39 +++ Telegram/Telegram.plist | 2 +- Telegram/Telegram.rc | Bin 5540 -> 5540 bytes Telegram/Telegram.xcodeproj/project.pbxproj | 12 +- Telegram/Version.sh | 2 +- 28 files changed, 879 insertions(+), 299 deletions(-) diff --git a/Telegram/PrepareWin.bat b/Telegram/PrepareWin.bat index e4a03bb3b..434d6b44c 100644 --- a/Telegram/PrepareWin.bat +++ b/Telegram/PrepareWin.bat @@ -1,10 +1,10 @@ @echo OFF -set "AppVersion=7023" -set "AppVersionStrSmall=0.7.23" -set "AppVersionStr=0.7.23" -set "AppVersionStrFull=0.7.23.0" -set "DevChannel=0" +set "AppVersion=7024" +set "AppVersionStrSmall=0.7.24" +set "AppVersionStr=0.7.24" +set "AppVersionStrFull=0.7.24.0" +set "DevChannel=1" if %DevChannel% neq 0 goto preparedev diff --git a/Telegram/Resources/lang.strings b/Telegram/Resources/lang.strings index 7701f24f2..27eb641df 100644 --- a/Telegram/Resources/lang.strings +++ b/Telegram/Resources/lang.strings @@ -431,6 +431,8 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org "lng_forward" = "Forward"; "lng_forward_send" = "Send"; "lng_forward_messages" = "{count:_not_used_|Forwarded message|# forwarded messages}"; +"lng_forwarding_from" = "{user} and {count:_not_used_|# other|# others}"; +"lng_forwarding_from_two" = "{user} and {second_user}"; "lng_contact_phone" = "Phone number"; "lng_enter_contact_data" = "New Contact"; diff --git a/Telegram/Resources/style.txt b/Telegram/Resources/style.txt index cf83938bc..0d3e4e89e 100644 --- a/Telegram/Resources/style.txt +++ b/Telegram/Resources/style.txt @@ -988,6 +988,7 @@ replyCancel: iconedButton(btnDefIconed) { width: 49px; height: 49px; } +forwardIcon: sprite(368px, 173px, 24px, 24px); historyScroll: flatScroll(scrollDef) { barColor: #89a0b47a; diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index 1450b2641..c82538c40 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -1702,7 +1702,7 @@ namespace App { void searchByHashtag(const QString &tag) { if (App::main()) { - App::main()->searchMessages(tag); + App::main()->searchMessages(tag + ' '); } } diff --git a/Telegram/SourceFiles/application.cpp b/Telegram/SourceFiles/application.cpp index a946db28b..b859e015f 100644 --- a/Telegram/SourceFiles/application.cpp +++ b/Telegram/SourceFiles/application.cpp @@ -654,7 +654,7 @@ void Application::checkMapVersion() { if (Local::oldMapVersion()) { QString versionFeatures; if (DevChannel && Local::oldMapVersion() < 7022) { - versionFeatures = QString::fromUtf8("\xe2\x80\x94 Critical bug with messages delivery fixed"); + versionFeatures = QString::fromUtf8("\xe2\x80\x94 Forward messages with comment or sticker before them\n\xe2\x80\x94 Recent used hashtags autocomplete in message field and search field\n\xe2\x80\x94 Move from reply to original message and back by an arrow in the bottom"); } else if (!DevChannel && Local::oldMapVersion() < 7023) { versionFeatures = lang(lng_new_version7022).trimmed().replace('@', qsl("@") + QChar(0x200D)); } diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index 3a3e69351..77ec99c09 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -17,9 +17,9 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org */ #pragma once -static const int32 AppVersion = 7023; -static const wchar_t *AppVersionStr = L"0.7.23"; -static const bool DevChannel = false; +static const int32 AppVersion = 7024; +static const wchar_t *AppVersionStr = L"0.7.24"; +static const bool DevChannel = true; static const wchar_t *AppNameOld = L"Telegram Win (Unofficial)"; static const wchar_t *AppName = L"Telegram Desktop"; diff --git a/Telegram/SourceFiles/dialogswidget.cpp b/Telegram/SourceFiles/dialogswidget.cpp index 2d752a6ee..037cc1302 100644 --- a/Telegram/SourceFiles/dialogswidget.cpp +++ b/Telegram/SourceFiles/dialogswidget.cpp @@ -25,6 +25,8 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org #include "boxes/addcontactbox.h" #include "boxes/newgroupbox.h" +#include "localstorage.h" + DialogsListWidget::DialogsListWidget(QWidget *parent, MainWidget *main) : QWidget(parent), dialogs(false), contactsNoDialogs(true), @@ -32,6 +34,7 @@ contacts(true), sel(0), contactSel(false), selByMouse(false), +hashtagSel(-1), filteredSel(-1), searchedCount(0), searchedSel(-1), @@ -47,6 +50,18 @@ _addContactLnk(this, lang(lng_add_contact_button)) { refresh(false); } +int32 DialogsListWidget::filteredOffset() const { + return hashtagResults.size() * st::mentionHeight; +} + +int32 DialogsListWidget::peopleOffset() const { + return filteredOffset() + (filterResults.size() * st::dlgHeight) + st::searchedBarHeight; +} + +int32 DialogsListWidget::searchedOffset() const { + return peopleOffset() + (peopleResults.isEmpty() ? 0 : ((peopleResults.size() * st::dlgHeight) + st::searchedBarHeight)); +} + void DialogsListWidget::paintEvent(QPaintEvent *e) { QRect r(e->rect()); bool trivial = (rect() == r); @@ -70,10 +85,31 @@ void DialogsListWidget::paintEvent(QPaintEvent *e) { p.drawText(QRect(0, 0, width(), st::noContactsHeight - (cContactsReceived() ? st::noContactsFont->height : 0)), lang(cContactsReceived() ? lng_no_contacts : lng_contacts_loading), style::al_center); } } else if (_state == FilteredState || _state == SearchedState) { - if (filterResults.isEmpty()) { - // .. paint no dialogs - } else { - int32 from = r.top() / int32(st::dlgHeight); + if (!hashtagResults.isEmpty()) { + int32 from = r.top() / int32(st::mentionHeight); + if (from < 0) { + from = 0; + } else if (from > hashtagResults.size()) { + from = hashtagResults.size(); + } + p.translate(0, from * st::mentionHeight); + if (from < hashtagResults.size()) { + int32 to = (r.bottom() / int32(st::mentionHeight)) + 1, w = width(); + if (to > hashtagResults.size()) to = hashtagResults.size(); + p.setFont(st::mentionFont->f); + p.setPen(st::black->p); + for (; from < to; ++from) { + bool selected = (from == hashtagSel); + if (selected) p.fillRect(0, 0, w, st::mentionHeight, st::dlgHoverBG->b); + QString tag = st::mentionFont->m.elidedText('#' + hashtagResults.at(from), Qt::ElideRight, w - st::dlgPaddingHor * 2); + p.drawText(st::dlgPaddingHor, st::mentionTop + st::mentionFont->ascent, tag); + p.translate(0, st::mentionHeight); + } + } + } + if (!filterResults.isEmpty()) { + int32 skip = filteredOffset(); + int32 from = (r.top() - skip) / int32(st::dlgHeight); if (from < 0) { from = 0; } else if (from > filterResults.size()) { @@ -99,7 +135,7 @@ void DialogsListWidget::paintEvent(QPaintEvent *e) { p.drawText(QRect(0, 0, width(), st::searchedBarHeight), lang(lng_search_global_results), style::al_center); p.translate(0, st::searchedBarHeight); - int32 skip = filterResults.size() * st::dlgHeight + st::searchedBarHeight; + int32 skip = peopleOffset(); int32 from = (r.top() - skip) / int32(st::dlgHeight); if (from < 0) { from = 0; @@ -127,8 +163,7 @@ void DialogsListWidget::paintEvent(QPaintEvent *e) { p.drawText(QRect(0, 0, width(), st::searchedBarHeight), text, style::al_center); p.translate(0, st::searchedBarHeight); - int32 skip = filterResults.size() * st::dlgHeight + st::searchedBarHeight; - if (!peopleResults.isEmpty()) skip += peopleResults.size() * st::dlgHeight + st::searchedBarHeight; + int32 skip = searchedOffset(); int32 from = (r.top() - skip) / int32(st::dlgHeight); if (from < 0) { from = 0; @@ -221,8 +256,20 @@ void DialogsListWidget::onUpdateSelected(bool force) { parentWidget()->update(); } } else if (_state == FilteredState || _state == SearchedState) { + if (!hashtagResults.isEmpty()) { + int32 newHashtagSel = mouseY / int32(st::mentionHeight); + if (newHashtagSel < 0 || newHashtagSel >= hashtagResults.size()) { + newHashtagSel = -1; + } + if (newHashtagSel != hashtagSel) { + hashtagSel = newHashtagSel; + setCursor((hashtagSel >= 0) ? style::cur_pointer : style::cur_default); + parentWidget()->update(); + } + mouseY -= filteredOffset(); + } if (!filterResults.isEmpty()) { - int32 newFilteredSel = mouseY / int32(st::dlgHeight); + int32 newFilteredSel = (mouseY >= 0) ? (mouseY / int32(st::dlgHeight)) : -1; if (newFilteredSel < 0 || newFilteredSel >= filterResults.size()) { newFilteredSel = -1; } @@ -232,7 +279,7 @@ void DialogsListWidget::onUpdateSelected(bool force) { parentWidget()->update(); } } - mouseY -= filterResults.size() * st::dlgHeight + st::searchedBarHeight; + mouseY -= peopleOffset() - filteredOffset(); if (!peopleResults.isEmpty()) { int32 newPeopleSel = (mouseY >= 0) ? (mouseY / int32(st::dlgHeight)) : -1; if (newPeopleSel < 0 || newPeopleSel >= peopleResults.size()) { @@ -243,8 +290,8 @@ void DialogsListWidget::onUpdateSelected(bool force) { setCursor((peopleSel >= 0) ? style::cur_pointer : style::cur_default); parentWidget()->update(); } - mouseY -= peopleResults.size() * st::dlgHeight + st::searchedBarHeight; } + mouseY -= searchedOffset() - peopleOffset(); if (_state == SearchedState && !searchResults.isEmpty()) { int32 newSearchedSel = (mouseY >= 0) ? (mouseY / int32(st::dlgHeight)) : -1; if (newSearchedSel < 0 || newSearchedSel >= searchResults.size()) { @@ -369,7 +416,7 @@ void DialogsListWidget::dlgUpdated(History *history) { } } } else if (_state == FilteredState || _state == SearchedState) { - int32 cnt = 0; + int32 cnt = 0, add = filteredOffset(); for (FilteredDialogs::const_iterator i = filterResults.cbegin(), e = filterResults.cend(); i != e; ++i) { if ((*i)->history == history) { update(0, cnt * st::dlgHeight, width(), st::dlgHeight); @@ -378,7 +425,7 @@ void DialogsListWidget::dlgUpdated(History *history) { ++cnt; } if (!peopleResults.isEmpty()) { - int32 cnt = 0, add = filterResults.size() * st::dlgHeight + st::searchedBarHeight; + int32 cnt = 0, add = peopleOffset(); for (PeopleResults::const_iterator i = peopleResults.cbegin(), e = peopleResults.cend(); i != e; ++i) { if ((*i) == history->peer) { update(0, add + cnt * st::dlgHeight, width(), st::dlgHeight); @@ -388,7 +435,7 @@ void DialogsListWidget::dlgUpdated(History *history) { } } if (!searchResults.isEmpty()) { - int32 cnt = 0, add = (filterResults.size() + peopleResults.size()) * st::dlgHeight + (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight; + int32 cnt = 0, add = searchedOffset(); for (SearchResults::const_iterator i = searchResults.cbegin(), e = searchResults.cend(); i != e; ++i) { if ((*i)->_item->history() == history) { update(0, add + cnt * st::dlgHeight, width(), st::dlgHeight); @@ -408,9 +455,9 @@ void DialogsListWidget::enterEvent(QEvent *e) { void DialogsListWidget::leaveEvent(QEvent *e) { setMouseTracking(false); - if (sel || filteredSel >= 0) { + if (sel || filteredSel >= 0 || hashtagSel >= 0 || searchedSel >= 0 || peopleSel >= 0) { sel = 0; - filteredSel = -1; + filteredSel = searchedSel = peopleSel = hashtagSel = -1; parentWidget()->update(); } } @@ -462,6 +509,7 @@ void DialogsListWidget::onFilterUpdate(QString newFilter, bool force) { filter = newFilter; if (filter.isEmpty()) { _state = DefaultState; + hashtagResults.clear(); filterResults.clear(); peopleResults.clear(); searchResults.clear(); @@ -549,6 +597,33 @@ void DialogsListWidget::onFilterUpdate(QString newFilter, bool force) { } } +void DialogsListWidget::onHashtagFilterUpdate(QString newFilter) { + if (newFilter.isEmpty() || newFilter.at(0) != '#') { + if (!hashtagResults.isEmpty()) { + hashtagResults.clear(); + refresh(true); + setMouseSel(false, true); + } + return; + } + if (cRecentSearchHashtags().isEmpty() && cRecentWriteHashtags().isEmpty()) { + Local::readRecentHashtags(); + } + const RecentHashtagPack &recent(cRecentSearchHashtags()); + hashtagResults.clear(); + if (!recent.isEmpty()) { + hashtagResults.reserve(qMin(recent.size(), 5)); + for (RecentHashtagPack::const_iterator i = recent.cbegin(), e = recent.cend(); i != e; ++i) { + if (i->first.startsWith(newFilter.midRef(1), Qt::CaseInsensitive) && i->first.size() + 1 != newFilter.size()) { + hashtagResults.push_back(i->first); + if (hashtagResults.size() == 5) break; + } + } + } + refresh(true); + setMouseSel(false, true); +} + DialogsListWidget::~DialogsListWidget() { clearSearchResults(); } @@ -683,9 +758,9 @@ void DialogsListWidget::refresh(bool toTop) { } else { if (!_addContactLnk.isHidden()) _addContactLnk.hide(); if (_state == FilteredState) { - h = (filterResults.count() + peopleResults.count() + searchResults.count()) * st::dlgHeight + (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) + (searchResults.isEmpty() ? 0 : st::searchedBarHeight); + h = searchedOffset() + (searchResults.count() * st::dlgHeight) + (searchResults.isEmpty() ? 0 : st::searchedBarHeight); } else if (_state == SearchedState) { - h = (filterResults.count() + peopleResults.count() + searchResults.count()) * st::dlgHeight + (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight; + h = searchedOffset() + (searchResults.count() * st::dlgHeight) + st::searchedBarHeight; } } resize(width(), h); @@ -703,7 +778,7 @@ void DialogsListWidget::setMouseSel(bool msel, bool toTop) { sel = (dialogs.list.count ? dialogs.list.begin : (contactsNoDialogs.list.count ? contactsNoDialogs.list.begin : 0)); contactSel = !dialogs.list.count && contactsNoDialogs.list.count; } else if (_state == FilteredState || _state == SearchedState) { // don't select first elem in search - filteredSel = peopleSel = searchedSel = -1; + filteredSel = peopleSel = searchedSel = hashtagSel = -1; } } } @@ -712,8 +787,10 @@ void DialogsListWidget::setState(State newState) { _state = newState; if (_state == DefaultState) { clearSearchResults(); - searchedSel = peopleSel = filteredSel = -1; + searchedSel = peopleSel = filteredSel = hashtagSel = -1; } else if (_state == DefaultState || _state == SearchedState) { + hashtagResults.clear(); + hashtagSel = -1; filterResults.clear(); filteredSel = -1; } @@ -726,12 +803,13 @@ DialogsListWidget::State DialogsListWidget::state() const { } bool DialogsListWidget::hasFilteredResults() const { - return !filterResults.isEmpty(); + return !filterResults.isEmpty() && hashtagResults.isEmpty(); } void DialogsListWidget::clearFilter() { if (_state == FilteredState || _state == SearchedState) { _state = DefaultState; + hashtagResults.clear(); filterResults.clear(); peopleResults.clear(); searchResults.clear(); @@ -778,37 +856,45 @@ void DialogsListWidget::selectSkip(int32 direction) { int32 fromY = (sel->pos + (contactSel ? dialogs.list.count : 0)) * st::dlgHeight; emit mustScrollTo(fromY, fromY + st::dlgHeight); } else if (_state == FilteredState || _state == SearchedState) { - if (filterResults.isEmpty() && peopleResults.isEmpty() && searchResults.isEmpty()) return; - if ((filteredSel < 0 || filteredSel >= filterResults.size()) && + if (hashtagResults.isEmpty() && filterResults.isEmpty() && peopleResults.isEmpty() && searchResults.isEmpty()) return; + if ((hashtagSel < 0 || hashtagSel >= hashtagResults.size()) && + (filteredSel < 0 || filteredSel >= filterResults.size()) && (peopleSel < 0 || peopleSel >= peopleResults.size()) && (searchedSel < 0 || searchedSel >= searchResults.size())) { - if (filterResults.isEmpty() && peopleResults.isEmpty()) { + if (hashtagResults.isEmpty() && filterResults.isEmpty() && peopleResults.isEmpty()) { searchedSel = 0; - } else if (filterResults.isEmpty()) { + } else if (hashtagResults.isEmpty() && filterResults.isEmpty()) { peopleSel = 0; - } else { + } else if (hashtagResults.isEmpty()) { filteredSel = 0; + } else { + hashtagSel = 0; } } else { - int32 cur = (filteredSel >= 0 && filteredSel < filterResults.size()) ? filteredSel : ((peopleSel >= 0 && peopleSel < peopleResults.size()) ? (peopleSel + filterResults.size()) : (searchedSel + peopleResults.size() + filterResults.size())); - cur = snap(cur + direction, 0, filterResults.size() + peopleResults.size() + searchResults.size() - 1); - if (cur < filterResults.size()) { - filteredSel = cur; - peopleSel = searchedSel = -1; - } else if (cur < filterResults.size() + peopleResults.size()) { - peopleSel = cur - filterResults.size(); - filteredSel = searchedSel = -1; + int32 cur = (hashtagSel >= 0 && hashtagSel < hashtagResults.size()) ? hashtagSel : ((filteredSel >= 0 && filteredSel < filterResults.size()) ? (hashtagResults.size() + filteredSel) : ((peopleSel >= 0 && peopleSel < peopleResults.size()) ? (peopleSel + filterResults.size() + hashtagResults.size()) : (searchedSel + peopleResults.size() + filterResults.size() + hashtagResults.size()))); + cur = snap(cur + direction, 0, hashtagResults.size() + filterResults.size() + peopleResults.size() + searchResults.size() - 1); + if (cur < hashtagResults.size()) { + hashtagSel = cur; + filteredSel = peopleSel = searchedSel = -1; + } else if (cur < hashtagResults.size() + filterResults.size()) { + filteredSel = cur - hashtagResults.size(); + hashtagSel = peopleSel = searchedSel = -1; + } else if (cur < hashtagResults.size() + filterResults.size() + peopleResults.size()) { + peopleSel = cur - hashtagResults.size() - filterResults.size(); + hashtagSel = filteredSel = searchedSel = -1; } else { - filteredSel = peopleSel = -1; - searchedSel = cur - filterResults.size() - peopleResults.size(); + hashtagSel = filteredSel = peopleSel = -1; + searchedSel = cur - hashtagResults.size() - filterResults.size() - peopleResults.size(); } } - if (filteredSel >= 0 && filteredSel < filterResults.size()) { - emit mustScrollTo(filteredSel * st::dlgHeight, (filteredSel + 1) * st::dlgHeight); + if (hashtagSel >= 0 && hashtagSel < hashtagResults.size()) { + emit mustScrollTo(hashtagSel * st::mentionHeight, (hashtagSel + 1) * st::mentionHeight); + } else if (filteredSel >= 0 && filteredSel < filterResults.size()) { + emit mustScrollTo(filteredOffset() + filteredSel * st::dlgHeight, filteredOffset() + (filteredSel + 1) * st::dlgHeight); } else if (peopleSel >= 0 && peopleSel < peopleResults.size()) { - emit mustScrollTo((peopleSel + filterResults.size()) * st::dlgHeight + (peopleSel ? st::searchedBarHeight : 0), (peopleSel + filterResults.size() + 1) * st::dlgHeight); + emit mustScrollTo(peopleOffset() + peopleSel * st::dlgHeight + (peopleSel ? 0 : -st::searchedBarHeight), peopleOffset() + (peopleSel + 1) * st::dlgHeight); } else { - emit mustScrollTo((searchedSel + peopleResults.size() + filterResults.size()) * st::dlgHeight + (peopleResults.size() ? st::searchedBarHeight : 0) + (searchedSel ? st::searchedBarHeight : 0), (searchedSel + peopleResults.size() + filterResults.size() + 1) * st::dlgHeight + (peopleResults.size() ? st::searchedBarHeight : 0) + st::searchedBarHeight); + emit mustScrollTo(searchedOffset() + searchedSel * st::dlgHeight + (searchedSel ? 0 : -st::searchedBarHeight), searchedOffset() + (searchedSel + 1) * st::dlgHeight); } } parentWidget()->update(); @@ -830,7 +916,7 @@ void DialogsListWidget::scrollToPeer(const PeerId &peer, MsgId msgId) { if (msgId) { for (int32 i = 0, c = searchResults.size(); i < c; ++i) { if (searchResults[i]->_item->history()->peer->id == peer && searchResults[i]->_item->id == msgId) { - fromY = filterResults.size() * st::dlgHeight + st::searchedBarHeight + i * st::dlgHeight; + fromY = searchedOffset() + i * st::dlgHeight; break; } } @@ -838,7 +924,7 @@ void DialogsListWidget::scrollToPeer(const PeerId &peer, MsgId msgId) { if (fromY < 0) { for (int32 i = 0, c = filterResults.size(); i < c; ++i) { if (filterResults[i]->history->peer->id == peer) { - fromY = i * st::dlgHeight; + fromY = filteredOffset() + (i * st::dlgHeight); break; } } @@ -914,7 +1000,7 @@ void DialogsListWidget::loadPeerPhotos(int32 yFrom) { } } } else if (_state == FilteredState || _state == SearchedState) { - int32 from = yFrom / st::dlgHeight; + int32 from = (yFrom - filteredOffset()) / st::dlgHeight; if (from < 0) from = 0; if (from < filterResults.size()) { int32 to = (yTo / int32(st::dlgHeight)) + 1, w = width(); @@ -925,20 +1011,20 @@ void DialogsListWidget::loadPeerPhotos(int32 yFrom) { } } - from = (yFrom > st::searchedBarHeight ? ((yFrom - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size(); + from = (yFrom > filteredOffset() + st::searchedBarHeight ? ((yFrom - filteredOffset() - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size(); if (from < 0) from = 0; if (from < peopleResults.size()) { - int32 to = (yTo > st::searchedBarHeight ? ((yTo - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size() + 1, w = width(); + int32 to = (yTo > filteredOffset() + st::searchedBarHeight ? ((yTo - filteredOffset() - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size() + 1, w = width(); if (to > peopleResults.size()) to = peopleResults.size(); for (; from < to; ++from) { peopleResults[from]->photo->load(); } } - from = (yFrom > ((peopleResults.isEmpty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight) ? ((yFrom - (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size() - peopleResults.size(); + from = (yFrom > filteredOffset() + ((peopleResults.isEmpty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight) ? ((yFrom - filteredOffset() - (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size() - peopleResults.size(); if (from < 0) from = 0; if (from < searchResults.size()) { - int32 to = (yTo >(peopleResults.isEmpty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight ? ((yTo - (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size() - peopleResults.size() + 1, w = width(); + int32 to = (yTo > filteredOffset() + (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight ? ((yTo - filteredOffset() - (peopleResults.isEmpty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / int32(st::dlgHeight)) : 0) - filterResults.size() - peopleResults.size() + 1, w = width(); if (to > searchResults.size()) to = searchResults.size(); for (; from < to; ++from) { @@ -954,6 +1040,11 @@ bool DialogsListWidget::choosePeer() { if (_state == DefaultState) { if (sel) history = sel->history; } else if (_state == FilteredState || _state == SearchedState) { + if (hashtagSel >= 0 && hashtagSel < hashtagResults.size()) { + saveRecentHashtags('#' + hashtagResults.at(hashtagSel)); + emit completeHashtag(hashtagResults.at(hashtagSel)); + return true; + } if (filteredSel >= 0 && filteredSel < filterResults.size()) { history = filterResults[filteredSel]->history; } else if (peopleSel >= 0 && peopleSel < peopleResults.size()) { @@ -964,22 +1055,55 @@ bool DialogsListWidget::choosePeer() { } } if (history) { + if (msgId) { + saveRecentHashtags(filter); + } bool chosen = (!App::main()->selectingPeer() && (_state == FilteredState || _state == SearchedState) && filteredSel >= 0 && filteredSel < filterResults.size()); App::main()->showPeer(history->peer->id, msgId); if (chosen) { emit searchResultChosen(); } sel = 0; - filteredSel = peopleSel = searchedSel = -1; + filteredSel = peopleSel = searchedSel = hashtagSel = -1; parentWidget()->update(); return true; } return false; } +void DialogsListWidget::saveRecentHashtags(const QString &text) { + bool found = false; + QRegularExpressionMatch m; + RecentHashtagPack recent(cRecentSearchHashtags()); + for (int32 i = 0, next = 0; (m = reHashtag().match(text, i)).hasMatch(); i = next) { + i = m.capturedStart(); + next = m.capturedEnd(); + if (m.hasMatch()) { + if (!m.capturedRef(1).isEmpty()) { + ++i; + } + if (!m.capturedRef(2).isEmpty()) { + --next; + } + } + if (!found && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) { + Local::readRecentStickers(); + recent = cRecentSearchHashtags(); + } + found = true; + incrementRecentHashtag(recent, text.mid(i + 1, next - i - 1)); + } + if (found) { + cSetRecentSearchHashtags(recent); + Local::writeRecentHashtags(); + } +} + void DialogsListWidget::destroyData() { sel = 0; contactSel = false; + hashtagSel = -1; + hashtagResults.clear(); filteredSel = -1; filterResults.clear(); filter.clear(); @@ -1193,11 +1317,13 @@ DialogsWidget::DialogsWidget(MainWidget *parent) : QWidget(parent) connect(&list, SIGNAL(dialogToTopFrom(int)), this, SLOT(onDialogToTopFrom(int))); connect(&list, SIGNAL(searchMessages()), this, SLOT(onNeedSearchMessages())); connect(&list, SIGNAL(searchResultChosen()), this, SLOT(onCancel())); + connect(&list, SIGNAL(completeHashtag(QString)), this, SLOT(onCompleteHashtag(QString))); connect(&scroll, SIGNAL(geometryChanged()), &list, SLOT(onParentGeometryChanged())); connect(&scroll, SIGNAL(scrolled()), &list, SLOT(onUpdateSelected())); connect(&scroll, SIGNAL(scrolled()), this, SLOT(onListScroll())); connect(&_filter, SIGNAL(cancelled()), this, SLOT(onCancel())); connect(&_filter, SIGNAL(changed()), this, SLOT(onFilterUpdate())); + connect(&_filter, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(onFilterCursorMoved(int,int))); connect(parent, SIGNAL(dialogsUpdated()), this, SLOT(onListScroll())); connect(&_addContact, SIGNAL(clicked()), this, SLOT(onAddContact())); connect(&_newGroup, SIGNAL(clicked()), this, SLOT(onNewGroup())); @@ -1424,6 +1550,8 @@ void DialogsWidget::searchMessages(const QString &query) { onFilterUpdate(); _searchTimer.stop(); onSearchMessages(); + + list.saveRecentHashtags(query); } } @@ -1578,6 +1706,47 @@ void DialogsWidget::onFilterUpdate(bool force) { } } +void DialogsWidget::onFilterCursorMoved(int from, int to) { + QString t = _filter.text(), r; + for (int start = to; start > 0;) { + --start; + if (t.size() <= start) break; + if (t.at(start) == '#') { + r = t.mid(start, to - start); + break; + } + if (!t.at(start).isLetterOrNumber() && t.at(start) != '_') break; + } + list.onHashtagFilterUpdate(r); +} + +void DialogsWidget::onCompleteHashtag(QString tag) { + QString t = _filter.text(), r; + int cur = _filter.cursorPosition(); + for (int start = cur; start > 0;) { + --start; + if (t.size() <= start) break; + if (t.at(start) == '#') { + if (cur == start + 1 || t.midRef(start + 1, cur - start - 1) == tag.midRef(0, cur - start - 1)) { + for (; cur < t.size() && cur - start - 1 < tag.size(); ++cur) { + if (t.at(cur) != tag.at(cur - start - 1)) break; + } + if (cur - start - 1 == tag.size() && cur < t.size() && t.at(cur) == ' ') ++cur; + r = t.mid(0, start + 1) + tag + ' ' + t.mid(cur); + _filter.setText(r); + _filter.setCursorPosition(start + 1 + tag.size() + 1); + onFilterUpdate(true); + return; + } + break; + } + if (!t.at(start).isLetterOrNumber() && t.at(start) != '_') break; + } + _filter.setText(t.mid(0, cur) + '#' + tag + ' ' + t.mid(cur)); + _filter.setCursorPosition(cur + 1 + tag.size() + 1); + onFilterUpdate(true); +} + void DialogsWidget::resizeEvent(QResizeEvent *e) { int32 w = width() - st::dlgShadow; _filter.setGeometry(st::dlgPaddingHor, st::dlgFilterPadding, w - 2 * st::dlgPaddingHor, _filter.height()); diff --git a/Telegram/SourceFiles/dialogswidget.h b/Telegram/SourceFiles/dialogswidget.h index 5d3a96441..9dd528200 100644 --- a/Telegram/SourceFiles/dialogswidget.h +++ b/Telegram/SourceFiles/dialogswidget.h @@ -36,6 +36,10 @@ public: void contactsReceived(const QVector &contacts); int32 addNewContact(int32 uid, bool sel = false); // return y of row or -1 if failed + int32 filteredOffset() const; + int32 peopleOffset() const; + int32 searchedOffset() const; + void paintEvent(QPaintEvent *e); void mouseMoveEvent(QMouseEvent *e); void mousePressEvent(QMouseEvent *e); @@ -59,6 +63,7 @@ public: void refresh(bool toTop = false); bool choosePeer(); + void saveRecentHashtags(const QString &text); void destroyData(); @@ -89,6 +94,7 @@ public: bool hasFilteredResults() const; void onFilterUpdate(QString newFilter, bool force = false); + void onHashtagFilterUpdate(QString newFilter); void itemRemoved(HistoryItem *item); void itemReplaced(HistoryItem *oldItem, HistoryItem *newItem); @@ -109,6 +115,7 @@ signals: void dialogToTopFrom(int movedFrom); void searchMessages(); void searchResultChosen(); + void completeHashtag(QString tag); private: @@ -123,6 +130,10 @@ private: bool selByMouse; QString filter; + + QStringList hashtagResults; + int32 hashtagSel; + FilteredDialogs filterResults; int32 filteredSel; @@ -206,6 +217,9 @@ public slots: void onNewGroup(); bool onCancelSearch(); + void onFilterCursorMoved(int from, int to); + void onCompleteHashtag(QString tag); + void onDialogToTopFrom(int movedFrom); bool onSearchMessages(bool searchCache = false); void onNeedSearchMessages(); diff --git a/Telegram/SourceFiles/dropdown.cpp b/Telegram/SourceFiles/dropdown.cpp index 9260427e0..5d161f4c6 100644 --- a/Telegram/SourceFiles/dropdown.cpp +++ b/Telegram/SourceFiles/dropdown.cpp @@ -881,35 +881,42 @@ void EmojiPan::onTabChange() { } } -MentionsInner::MentionsInner(MentionsDropdown *parent, MentionRows *rows) : _parent(parent), _rows(rows), _sel(-1), _mouseSel(false) { +MentionsInner::MentionsInner(MentionsDropdown *parent, MentionRows *rows, HashtagRows *hrows) : _parent(parent), _rows(rows), _hrows(hrows), _sel(-1), _mouseSel(false) { } void MentionsInner::paintEvent(QPaintEvent *e) { QPainter p(this); - int32 atwidth = st::mentionFont->m.width('@'), availwidth = width() - 2 * st::mentionPadding.left() - st::mentionPhotoSize - 2 * st::mentionPadding.right(); + int32 atwidth = st::mentionFont->m.width('@'), hashwidth = st::mentionFont->m.width('#'); + int32 availwidth = width() - 2 * st::mentionPadding.left() - st::mentionPhotoSize - 2 * st::mentionPadding.right(); - int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1, last = _rows->size(); + int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1, last = _rows->isEmpty() ? _hrows->size() : _rows->size(); for (int32 i = from; i < to; ++i) { if (i >= last) break; if (i == _sel) p.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::dlgHoverBG->b); - UserData *user = _rows->at(last - i - 1); - QString uname = user->username; - int32 unamewidth = atwidth + st::mentionFont->m.width(uname), namewidth = user->nameText.maxWidth(); - if (availwidth < unamewidth + namewidth) { - namewidth = (availwidth * namewidth) / (namewidth + unamewidth); - unamewidth = availwidth - namewidth; - uname = st::mentionFont->m.elidedText('@' + uname, Qt::ElideRight, unamewidth); + if (_rows->isEmpty()) { + QString tag = st::mentionFont->m.elidedText('#' + _hrows->at(last - i - 1), Qt::ElideRight, availwidth); + p.setFont(st::mentionFont->f); + p.drawText(2 * st::mentionPadding.left() + st::mentionPhotoSize + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, tag); } else { - uname = '@' + uname; + UserData *user = _rows->at(last - i - 1); + QString uname = user->username; + int32 unamewidth = atwidth + st::mentionFont->m.width(uname), namewidth = user->nameText.maxWidth(); + if (availwidth < unamewidth + namewidth) { + namewidth = (availwidth * namewidth) / (namewidth + unamewidth); + unamewidth = availwidth - namewidth; + uname = st::mentionFont->m.elidedText('@' + uname, Qt::ElideRight, unamewidth); + } else { + uname = '@' + uname; + } + user->photo->load(); + p.drawPixmap(st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), user->photo->pix(st::mentionPhotoSize)); + user->nameText.drawElided(p, 2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop, namewidth); + p.setFont(st::mentionFont->f); + p.drawText(2 * st::mentionPadding.left() + st::mentionPhotoSize + namewidth + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, uname); } - user->photo->load(); - p.drawPixmap(st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), user->photo->pix(st::mentionPhotoSize)); - user->nameText.drawElided(p, 2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop, namewidth); - p.setFont(st::mentionFont->f); - p.drawText(2 * st::mentionPadding.left() + st::mentionPhotoSize + namewidth + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, uname); } p.fillRect(cWideMode() ? st::dlgShadow : 0, _parent->innerTop(), width() - (cWideMode() ? st::dlgShadow : 0), st::titleShadow, st::titleShadowColor->b); @@ -929,19 +936,22 @@ void MentionsInner::clearSel() { bool MentionsInner::moveSel(int direction) { _mouseSel = false; - if (_sel >= _rows->size() || _sel < 0) { - if (direction < 0) setSel(_rows->size() - 1, true); - return (_sel >= 0 && _sel < _rows->size()); + int32 maxSel = (_rows->isEmpty() ? _hrows->size() : _rows->size()); + if (_sel >= maxSel || _sel < 0) { + if (direction < 0) setSel(maxSel - 1, true); + return (_sel >= 0 && _sel < maxSel); } if (_sel > 0 || direction > 0) { - setSel((_sel + direction >= _rows->size()) ? -1 : (_sel + direction), true); + setSel((_sel + direction >= maxSel) ? -1 : (_sel + direction), true); } return true; } bool MentionsInner::select() { - if (_sel >= 0 && _sel < _rows->size()) { - emit mentioned(_rows->at(_rows->size() - _sel - 1)->username); + int32 maxSel = (_rows->isEmpty() ? _hrows->size() : _rows->size()); + if (_sel >= 0 && _sel < maxSel) { + QString result = _rows->isEmpty() ? ('#' + _hrows->at(_hrows->size() - _sel - 1)) : ('@' + _rows->at(_rows->size() - _sel - 1)->username); + emit chosen(result); return true; } return false; @@ -972,7 +982,8 @@ void MentionsInner::leaveEvent(QEvent *e) { void MentionsInner::setSel(int sel, bool scroll) { _sel = sel; parentWidget()->update(); - if (scroll && _sel >= 0 && _sel < _rows->size()) emit mustScrollTo(_sel * st::mentionHeight, (_sel + 1) * st::mentionHeight); + int32 maxSel = _rows->isEmpty() ? _hrows->size() : _rows->size(); + if (scroll && _sel >= 0 && _sel < maxSel) emit mustScrollTo(_sel * st::mentionHeight, (_sel + 1) * st::mentionHeight); } void MentionsInner::onUpdateSelected(bool force) { @@ -980,8 +991,8 @@ void MentionsInner::onUpdateSelected(bool force) { if ((!force && !rect().contains(mouse)) || !_mouseSel) return; int w = width(), mouseY = mouse.y(); - int32 sel = mouseY / int32(st::mentionHeight); - if (sel < 0 || sel >= _rows->size()) { + int32 sel = mouseY / int32(st::mentionHeight), maxSel = _rows->isEmpty() ? _hrows->size() : _rows->size(); + if (sel < 0 || sel >= maxSel) { sel = -1; } if (sel != _sel) { @@ -998,10 +1009,10 @@ void MentionsInner::onParentGeometryChanged() { } MentionsDropdown::MentionsDropdown(QWidget *parent) : QWidget(parent), -_scroll(this, st::mentionScroll), _inner(this, &_rows), _chat(0), _hiding(false), a_opacity(0), _shadow(st::dropdownShadow) { +_scroll(this, st::mentionScroll), _inner(this, &_rows, &_hrows), _chat(0), _hiding(false), a_opacity(0), _shadow(st::dropdownShadow) { _hideTimer.setSingleShot(true); connect(&_hideTimer, SIGNAL(timeout()), this, SLOT(hideStart())); - connect(&_inner, SIGNAL(mentioned(QString)), this, SIGNAL(mentioned(QString))); + connect(&_inner, SIGNAL(chosen(QString)), this, SIGNAL(chosen(QString))); connect(&_inner, SIGNAL(mustScrollTo(int,int)), &_scroll, SLOT(scrollToY(int,int))); setFocusPolicy(Qt::NoFocus); @@ -1047,42 +1058,53 @@ void MentionsDropdown::showFiltered(ChatData *chat, QString start) { int32 now = unixtime(); QMultiMap ordered; MentionRows rows; - rows.reserve(_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size()); - if (_chat->participants.isEmpty()) { - if (_chat->count > 0) { - App::api()->requestFullPeer(_chat); + HashtagRows hrows; + if (_filter.at(0) == '@') { + rows.reserve(_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size()); + if (_chat->participants.isEmpty()) { + if (_chat->count > 0) { + App::api()->requestFullPeer(_chat); + } + } else { + for (ChatData::Participants::const_iterator i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { + UserData *user = i.key(); + if (user->username.isEmpty()) continue; + if (_filter.size() > 1 && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + ordered.insertMulti(App::onlineForSort(user->onlineTill, now), user); + } + } + for (MentionRows::const_iterator i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { + UserData *user = *i; + if (user->username.isEmpty()) continue; + if (_filter.size() > 1 && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + rows.push_back(user); + if (!ordered.isEmpty()) { + ordered.remove(App::onlineForSort(user->onlineTill, now), user); + } + } + if (!ordered.isEmpty()) { + for (QMultiMap::const_iterator i = ordered.cend(), b = ordered.cbegin(); i != b;) { + --i; + rows.push_back(i.value()); + } } } else { - for (ChatData::Participants::const_iterator i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { - UserData *user = i.key(); - if (user->username.isEmpty()) continue; - if (!_filter.isEmpty() && !user->username.startsWith(_filter, Qt::CaseInsensitive)) continue; - ordered.insertMulti(App::onlineForSort(user->onlineTill, now), user); + const RecentHashtagPack &recent(cRecentWriteHashtags()); + hrows.reserve(recent.size()); + for (RecentHashtagPack::const_iterator i = recent.cbegin(), e = recent.cend(); i != e; ++i) { + if (_filter.size() > 1 && !i->first.startsWith(_filter.midRef(1), Qt::CaseInsensitive)) continue; + hrows.push_back(i->first); } } - for (MentionRows::const_iterator i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { - UserData *user = *i; - if (user->username.isEmpty()) continue; - if (!_filter.isEmpty() && !user->username.startsWith(_filter, Qt::CaseInsensitive)) continue; - rows.push_back(user); - if (!ordered.isEmpty()) { - ordered.remove(App::onlineForSort(user->onlineTill, now), user); - } - } - if (!ordered.isEmpty()) { - for (QMultiMap::const_iterator i = ordered.cend(), b = ordered.cbegin(); i != b;) { - --i; - rows.push_back(i.value()); - } - } - - if (rows.isEmpty()) { + if (rows.isEmpty() && hrows.isEmpty()) { if (!isHidden()) { hideStart(); _rows.clear(); + _hrows.clear(); } } else { _rows = rows; + _hrows = hrows; bool hidden = _hiding || isHidden(); if (hidden) { show(); @@ -1105,7 +1127,7 @@ void MentionsDropdown::setBoundings(QRect boundings) { } void MentionsDropdown::recount(bool toDown) { - int32 h = _rows.size() * st::mentionHeight, oldst = _scroll.scrollTop(), st = oldst; + int32 h = (_rows.isEmpty() ? _hrows.size() : _rows.size()) * st::mentionHeight, oldst = _scroll.scrollTop(), st = oldst; if (_inner.height() != h) { st += h - _inner.height(); diff --git a/Telegram/SourceFiles/dropdown.h b/Telegram/SourceFiles/dropdown.h index e31ff84bd..18fb14cbb 100644 --- a/Telegram/SourceFiles/dropdown.h +++ b/Telegram/SourceFiles/dropdown.h @@ -228,6 +228,7 @@ private: }; typedef QList MentionRows; +typedef QList HashtagRows; class MentionsDropdown; class MentionsInner : public QWidget { @@ -235,7 +236,7 @@ class MentionsInner : public QWidget { public: - MentionsInner(MentionsDropdown *parent, MentionRows *rows); + MentionsInner(MentionsDropdown *parent, MentionRows *rows, HashtagRows *hrows); void paintEvent(QPaintEvent *e); @@ -251,7 +252,7 @@ public: signals: - void mentioned(QString username); + void chosen(QString mentionOrHashtag); void mustScrollTo(int scrollToTop, int scrollToBottom); public slots: @@ -265,6 +266,7 @@ private: MentionsDropdown *_parent; MentionRows *_rows; + HashtagRows *_hrows; int32 _sel; bool _mouseSel; QPoint _mousePos; @@ -296,7 +298,7 @@ public: signals: - void mentioned(QString username); + void chosen(QString mentionOrHashtag); public slots: @@ -311,6 +313,7 @@ private: QPixmap _cache; MentionRows _rows; + HashtagRows _hrows; ScrollArea _scroll; MentionsInner _inner; diff --git a/Telegram/SourceFiles/gui/flattextarea.cpp b/Telegram/SourceFiles/gui/flattextarea.cpp index 04db563b4..ae117810a 100644 --- a/Telegram/SourceFiles/gui/flattextarea.cpp +++ b/Telegram/SourceFiles/gui/flattextarea.cpp @@ -180,9 +180,9 @@ EmojiPtr FlatTextarea::getSingleEmoji() const { return 0; } -bool FlatTextarea::getMentionStart(QString &start) const { +void FlatTextarea::getMentionHashtagStart(QString &start) const { int32 pos = textCursor().position(); - if (textCursor().anchor() != pos) return false; + if (textCursor().anchor() != pos) return; QTextDocument *doc(document()); QTextBlock block = doc->findBlock(pos); @@ -199,18 +199,24 @@ bool FlatTextarea::getMentionStart(QString &start) const { QString t(fr.text()); for (int i = pos - p; i > 0; --i) { if (t.at(i - 1) == '@') { - start = t.mid(i, pos - p - i); - return (start.isEmpty() || start.at(0).isLetter()) && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_')); + if ((pos - p - i < 1 || t.at(i).isLetter()) && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { + start = t.mid(i - 1, pos - p - i + 1); + } + return; + } else if (t.at(i - 1) == '#') { + if (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_')) { + start = t.mid(i - 1, pos - p - i + 1); + } + return; } - if (pos - p - i > 31) break; + if (pos - p - i > 63) break; if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; } - return false; + return; } - return false; } -void FlatTextarea::onMentionInsert(QString mention) { +void FlatTextarea::onMentionOrHashtagInsert(QString mentionOrHashtag) { QTextCursor c(textCursor()); int32 pos = c.position(); @@ -228,29 +234,29 @@ void FlatTextarea::onMentionInsert(QString mention) { QString t(fr.text()); for (int i = pos - p; i > 0; --i) { - if (t.at(i - 1) == '@') { - if ((i == pos - p || t.at(i).isLetter()) && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { - c.setPosition(p + i, QTextCursor::MoveAnchor); + if (t.at(i - 1) == '@' || t.at(i - 1) == '#') { + if ((i == pos - p || t.at(i).isLetter() || t.at(i - 1) == '#') && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { + c.setPosition(p + i - 1, QTextCursor::MoveAnchor); int till = p + i; - for (; (till < e) && (till - p - i < mention.size()); ++till) { - if (t.at(till - p).toLower() != mention.at(till - p - i).toLower()) { + for (; (till < e) && (till - p - i + 1 < mentionOrHashtag.size()); ++till) { + if (t.at(till - p).toLower() != mentionOrHashtag.at(till - p - i + 1).toLower()) { break; } } - if (till - p - i == mention.size() && till < e && t.at(till - p) == ' ') { + if (till - p - i + 1 == mentionOrHashtag.size() && till < e && t.at(till - p) == ' ') { ++till; } c.setPosition(till, QTextCursor::KeepAnchor); - c.insertText(mention + ' '); + c.insertText(mentionOrHashtag + ' '); return; } break; } - if (pos - p - i > 31) break; + if (pos - p - i > 63) break; if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; } } - c.insertText('@' + mention + ' '); + c.insertText(mentionOrHashtag + ' '); } void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const { diff --git a/Telegram/SourceFiles/gui/flattextarea.h b/Telegram/SourceFiles/gui/flattextarea.h index 87faaaf40..4f09c6973 100644 --- a/Telegram/SourceFiles/gui/flattextarea.h +++ b/Telegram/SourceFiles/gui/flattextarea.h @@ -49,7 +49,7 @@ public: QSize minimumSizeHint() const; EmojiPtr getSingleEmoji() const; - bool getMentionStart(QString &start) const; + void getMentionHashtagStart(QString &start) const; void removeSingleEmoji(); QString getText(int32 start = 0, int32 end = -1) const; bool hasText() const; @@ -67,7 +67,7 @@ public slots: void onUndoAvailable(bool avail); void onRedoAvailable(bool avail); - void onMentionInsert(QString mention); + void onMentionOrHashtagInsert(QString mentionOrHashtag); signals: diff --git a/Telegram/SourceFiles/gui/text.cpp b/Telegram/SourceFiles/gui/text.cpp index 3f0a4860d..8055f259b 100644 --- a/Telegram/SourceFiles/gui/text.cpp +++ b/Telegram/SourceFiles/gui/text.cpp @@ -132,12 +132,12 @@ namespace { return false; } - const QRegularExpression reDomain(QString::fromUtf8("(?|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])#[A-Za-z_\\.0-9]{2,20}([\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10]|$)")); - const QRegularExpression reMention(qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])@[A-Za-z_0-9]{5,32}([\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10]|$)")); + const QRegularExpression _reDomain(QString::fromUtf8("(?|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])#[\\w]{2,64}([\\W]|$)"), QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression _reMention(qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])@[A-Za-z_0-9]{5,32}([\\W]|$)"), QRegularExpression::UseUnicodePropertiesOption); QSet validProtocols, validTopDomains; void initLinkSets(); @@ -158,6 +158,10 @@ namespace { } } +const QRegularExpression &reHashtag() { + return _reHashtag; +} + const style::textStyle *textstyleCurrent() { return _textStyle; } @@ -425,7 +429,7 @@ public: } else if (!original.isEmpty() && original.at(0) == '#') { result = original; fullDisplayed = -2; // hashtag - } else if (reMailStart.match(original).hasMatch()) { + } else if (_reMailStart.match(original).hasMatch()) { result = original; fullDisplayed = -1; // email } else { @@ -4146,10 +4150,10 @@ LinkRanges textParseLinks(const QString &text, bool rich) { } } } - QRegularExpressionMatch mDomain = reDomain.match(text, matchOffset); - QRegularExpressionMatch mExplicitDomain = reExplicitDomain.match(text, matchOffset); - QRegularExpressionMatch mHashtag = reHashtag.match(text, matchOffset); - QRegularExpressionMatch mMention = reMention.match(text, matchOffset); + QRegularExpressionMatch mDomain = _reDomain.match(text, matchOffset); + QRegularExpressionMatch mExplicitDomain = _reExplicitDomain.match(text, matchOffset); + QRegularExpressionMatch mHashtag = _reHashtag.match(text, matchOffset); + QRegularExpressionMatch mMention = _reMention.match(text, matchOffset); if (!mDomain.hasMatch() && !mExplicitDomain.hasMatch() && !mHashtag.hasMatch() && !mMention.hasMatch()) break; LinkRange link; @@ -4225,7 +4229,7 @@ LinkRanges textParseLinks(const QString &text, bool rich) { if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) { QString forMailName = text.mid(offset, domainOffset - offset - 1); - QRegularExpressionMatch mMailName = reMailName.match(forMailName); + QRegularExpressionMatch mMailName = _reMailName.match(forMailName); if (mMailName.hasMatch()) { int32 mailOffset = offset + mMailName.capturedStart(); if (mailOffset < offset) { diff --git a/Telegram/SourceFiles/gui/text.h b/Telegram/SourceFiles/gui/text.h index 7f3e21376..894e53fe2 100644 --- a/Telegram/SourceFiles/gui/text.h +++ b/Telegram/SourceFiles/gui/text.h @@ -471,6 +471,8 @@ private: }; +const QRegularExpression &reHashtag(); + // text style const style::textStyle *textstyleCurrent(); void textstyleSet(const style::textStyle *style); diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 504774e4f..b6eb1e296 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -800,6 +800,10 @@ void PeerLink::onClick(Qt::MouseButton button) const { void MessageLink::onClick(Qt::MouseButton button) const { if (button == Qt::LeftButton && App::main()) { + HistoryItem *current = App::mousedItem(); + if (current && current->history()->peer->id == peer()) { + App::main()->pushReplyReturn(current); + } App::main()->showPeer(peer(), msgid()); } } @@ -5048,7 +5052,7 @@ void HistoryReply::replyToNameUpdated() const { int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; _maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgReplyPadding.right(); - int32 _textw = st::msgReplyPadding.left() + st::msgReplyBarSkip + previewSkip + qMin(replyToText.maxWidth(), 2 * replyToName.maxWidth()) + st::msgReplyPadding.right(); + int32 _textw = st::msgReplyPadding.left() + st::msgReplyBarSkip + previewSkip + qMin(replyToText.maxWidth(), 4 * replyToName.maxWidth()) + st::msgReplyPadding.right(); if (_textw > _maxReplyWidth) _maxReplyWidth = _textw; if (!_media) { int maxw = _maxReplyWidth - st::msgReplyPadding.left() - st::msgReplyPadding.right() + st::msgPadding.left() + st::msgPadding.right(); diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 12a488a3a..7067ca29c 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -1037,7 +1037,7 @@ void HistoryList::fillSelectedItems(SelectedItemSet &sel, bool forDelete) { for (SelectedItems::const_iterator i = _selected.cbegin(), e = _selected.cend(); i != e; ++i) { HistoryItem *item = i.key(); - if (item->itemType() == HistoryItem::MsgType && ((item->id > 0 && !item->serviceMsg()) || forDelete)) { + if (dynamic_cast(item) && item->id > 0) { sel.insert(item->id, item); } } @@ -1461,20 +1461,15 @@ void HistoryHider::startHide() { } void HistoryHider::forward() { - if (_forwardRequest) return; - if (!hiding && offered) { if (_sharedContact) { parent()->onShareContact(offered->id, _sharedContact); } else if (_sendPath) { parent()->onSendPaths(offered->id); } else { - _forwardRequest = parent()->onForward(offered->id, _forwardSelected); + parent()->onForward(offered->id, _forwardSelected); } } - if (!_forwardRequest) { - startHide(); - } } void HistoryHider::forwardDone() { @@ -1501,13 +1496,13 @@ void HistoryHider::resizeEvent(QResizeEvent *e) { forwardButton.move(box.x() + box.width() - forwardButton.width(), cancelButton.y()); } -void HistoryHider::offerPeer(PeerId peer) { +bool HistoryHider::offerPeer(PeerId peer) { if (!peer) { offered = 0; toText.setText(st::boxFont, QString()); toTextWidth = 0; resizeEvent(0); - return; + return false; } offered = App::peer(peer); LangString phrase; @@ -1525,7 +1520,11 @@ void HistoryHider::offerPeer(PeerId peer) { phrase = lng_forward_send_file_confirm(lt_name, name, lt_recipient, recipient); } } else { - phrase = lng_forward_confirm(lt_recipient, recipient); + PeerId to = offered->id; + offered = 0; + parent()->onForward(to, _forwardSelected); + startHide(); + return false; } toText.setText(st::boxFont, phrase, _textNameOptions); @@ -1537,6 +1536,8 @@ void HistoryHider::offerPeer(PeerId peer) { resizeEvent(0); update(); setFocus(); + + return true; } QString HistoryHider::offeredText() const { @@ -1557,7 +1558,8 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) , _replyToId(0) , _replyTo(0) , _replyToNameVersion(0) -, _replyCancel(this, st::replyCancel) +, _replyForwardCancel(this, st::replyCancel) +, _replyReturn(0) , _lastStickersUpdate(0) , _stickersUpdateRequest(0) , _loadingMessages(false) @@ -1601,7 +1603,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) connect(&_scroll, SIGNAL(scrolled()), this, SLOT(onListScroll())); connect(&_toHistoryEnd, SIGNAL(clicked()), this, SLOT(onHistoryToEnd())); - connect(&_replyCancel, SIGNAL(clicked()), this, SLOT(onReplyCancel())); + connect(&_replyForwardCancel, SIGNAL(clicked()), this, SLOT(onReplyForwardCancel())); connect(&_send, SIGNAL(clicked()), this, SLOT(onSend())); connect(&_attachDocument, SIGNAL(clicked()), this, SLOT(onDocumentSelect())); connect(&_attachPhoto, SIGNAL(clicked()), this, SLOT(onPhotoSelect())); @@ -1632,7 +1634,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) connect(_field.verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onDraftSaveDelayed())); connect(&_field, SIGNAL(cursorPositionChanged()), this, SLOT(onFieldCursorChanged())); - _replyCancel.hide(); + _replyForwardCancel.hide(); _scroll.hide(); _scroll.move(0, 0); @@ -1642,7 +1644,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : QWidget(parent) _toHistoryEnd.hide(); _attachMention.hide(); - connect(&_attachMention, SIGNAL(mentioned(QString)), &_field, SLOT(onMentionInsert(QString))); + connect(&_attachMention, SIGNAL(chosen(QString)), &_field, SLOT(onMentionOrHashtagInsert(QString))); _field.installEventFilter(&_attachMention); _field.hide(); @@ -1914,6 +1916,42 @@ void HistoryWidget::clearLoadingAround() { } } +void HistoryWidget::clearReplyReturns() { + _replyReturns.clear(); + _replyReturn = 0; +} + +void HistoryWidget::pushReplyReturn(HistoryItem *item) { + if (!item) return; + _replyReturn = item; + _replyReturns.push_back(_replyReturn->id); + updateControlsVisibility(); +} + +QList HistoryWidget::replyReturns() { + return _replyReturns; +} + +void HistoryWidget::setReplyReturns(PeerId peer, const QList &replyReturns) { + if (!histPeer || histPeer->id != peer) return; + _replyReturns = replyReturns; + _replyReturn = _replyReturns.isEmpty() ? 0 : App::histItemById(_replyReturns.back()); + while (!_replyReturns.isEmpty() && !_replyReturn) { + _replyReturns.pop_back(); + _replyReturn = _replyReturns.isEmpty() ? 0 : App::histItemById(_replyReturns.back()); + } + updateControlsVisibility(); +} + +void HistoryWidget::calcNextReplyReturn() { + _replyReturn = 0; + while (!_replyReturns.isEmpty() && !_replyReturn) { + _replyReturns.pop_back(); + _replyReturn = _replyReturns.isEmpty() ? 0 : App::histItemById(_replyReturns.back()); + } + if (!_replyReturn) updateControlsVisibility(); +} + void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool leaveActive) { if (App::main()->selectingPeer() && !force) { hiderOffered = true; @@ -1936,6 +1974,8 @@ void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool l if (histPreloadingDown) MTP::cancel(histPreloadingDown); histPreloading = histPreloadingDown = 0; } + if (_replyReturn && _replyReturn->id == msgId) calcNextReplyReturn(); + if (hist->unreadBar) hist->unreadBar->destroy(); checkUnreadLoaded(); @@ -1947,7 +1987,7 @@ void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool l } stopGif(); clearLoadingAround(); - + clearReplyReturns(); if (_list) { if (!histPreload.isEmpty()) { _list->messagesReceived(histPreload); @@ -1977,7 +2017,7 @@ void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool l if (_replyToId) { _replyTo = 0; _replyToId = 0; - _replyCancel.hide(); + _replyForwardCancel.hide(); } _scroll.setWidget(0); if (_list) _list->deleteLater(); @@ -2051,7 +2091,7 @@ void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool l setFieldText(hist->draft); _field.setFocus(); hist->draftCursor.applyTo(_field, &_synthedTextUpdate); - _replyToId = hist->draftToId; + _replyToId = App::main()->hasForwardingItems() ? 0 : hist->draftToId; } else { Local::MessageDraft draft = Local::readDraft(hist->peer->id); setFieldText(draft.text); @@ -2060,7 +2100,7 @@ void HistoryWidget::showPeer(const PeerId &peer, MsgId msgId, bool force, bool l MessageCursor cur = Local::readDraftPositions(hist->peer->id); cur.applyTo(_field, &_synthedTextUpdate); } - _replyToId = draft.replyTo; + _replyToId = App::main()->hasForwardingItems() ? 0 : draft.replyTo; } if (_replyToId) { updateReplyTo(); @@ -2109,7 +2149,7 @@ void HistoryWidget::updateControlsVisibility() { _toHistoryEnd.hide(); _attachMention.hide(); _field.hide(); - _replyCancel.hide(); + _replyForwardCancel.hide(); _attachDocument.hide(); _attachPhoto.hide(); _attachEmoji.hide(); @@ -2120,7 +2160,7 @@ void HistoryWidget::updateControlsVisibility() { } if (hist->readyForWork()) { - if (hist->loadedAtBottom()) { + if (hist->loadedAtBottom() && !_replyReturn) { _toHistoryEnd.hide(); } else { _toHistoryEnd.show(); @@ -2135,7 +2175,9 @@ void HistoryWidget::updateControlsVisibility() { _attachEmoji.show(); if (_field.isHidden()) { _field.show(); - if (_replyToId) _replyCancel.show(); + } + if ((_replyToId || App::main()->hasForwardingItems()) && _replyForwardCancel.isHidden()) { + _replyForwardCancel.show(); resizeEvent(0); update(); } @@ -2171,7 +2213,7 @@ void HistoryWidget::updateControlsVisibility() { _emojiPan.hide(); // _stickerPan.hide(); _toHistoryEnd.hide(); - _replyCancel.hide(); + _replyForwardCancel.hide(); if (!_field.isHidden()) { _field.hide(); update(); @@ -2443,15 +2485,26 @@ void HistoryWidget::onListScroll() { return; } - if (hist->readyForWork() && (_scroll.scrollTop() + PreloadHeightsCount * _scroll.height() > _scroll.scrollTopMax())) { + int st = _scroll.scrollTop(), stm = _scroll.scrollTopMax(), sh = _scroll.height(); + if (hist->readyForWork() && (st + PreloadHeightsCount * sh > stm)) { loadMessagesDown(); } - if (!hist->readyForWork() || _scroll.scrollTop() < PreloadHeightsCount * _scroll.height()) { + if (!hist->readyForWork() || st < PreloadHeightsCount * sh) { loadMessages(); } else { checkUnreadLoaded(true); } + + while (_replyReturn) { + bool below = (_replyReturn->detached() && !hist->isEmpty() && _replyReturn->id < hist->back()->back()->id); + if (!below && !_replyReturn->detached()) below = (st >= stm) || (_replyReturn->y + _replyReturn->block()->y < st + sh / 2); + if (below) { + calcNextReplyReturn(); + } else { + break; + } + } } void HistoryWidget::onVisibleChanged() { @@ -2470,9 +2523,13 @@ QString HistoryWidget::prepareMessage(QString result) { } void HistoryWidget::onHistoryToEnd() { - _toHistoryEnd.hide(); - if (hist && !hist->loadedAtBottom()) { - showPeer(histPeer->id, 0); + if (_replyReturn) { + showPeer(histPeer->id, _replyReturn->id); + } else { + _toHistoryEnd.hide(); + if (hist && !hist->loadedAtBottom()) { + showPeer(histPeer->id, 0); + } } } @@ -2484,7 +2541,7 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) { App::main()->readServerHistory(hist, false); hist->loadAround(0); - App::main()->sendPreparedText(hist, prepareMessage(_field.getText()), replyTo); + App::main()->sendPreparedText(hist, text, replyTo); setFieldText(QString()); _saveDraftText = true; @@ -2495,76 +2552,14 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) { if (!_attachType.isHidden()) _attachType.hideStart(); if (!_emojiPan.isHidden()) _emojiPan.hideStart(); // if (!_stickerPan.isHidden()) _stickerPan.hideStart(); - } - if (replyTo < 0) onReplyCancel(); - _field.setFocus(); -} - -mtpRequestId HistoryWidget::onForward(const PeerId &peer, SelectedItemSet toForward) { - if (toForward.isEmpty() && App::contextItem()) { - toForward.insert(0, App::contextItem()); - } - if (toForward.isEmpty()) return 0; - - if (toForward.size() == 1) { - App::main()->clearSelectedItems(); - App::main()->showPeer(peer, 0, false, true); - if (!hist) return 0; - - HistoryItem *item = toForward.cbegin().value(); - uint64 randomId = MTP::nonce(); - HistoryMessage *msg = dynamic_cast(item); - HistoryServiceMsg *srv = dynamic_cast(item); - MsgId newId = 0; - + } else if (App::main()->hasForwardingItems()) { + App::main()->readServerHistory(hist, false); hist->loadAround(0); - if (item->id > 0 && msg) { - App::main()->readServerHistory(hist, false); - newId = clientMsgId(); - hist->addToBackForwarded(newId, msg); - hist->sendRequestId = MTP::send(MTPmessages_ForwardMessage(histPeer->input, MTP_int(item->id), MTP_long(randomId)), App::main()->rpcDone(&MainWidget::sentFullDataReceived, randomId), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); - } else if (srv || (msg && msg->selectedText(FullItemSel).isEmpty())) { - // newId = clientMsgId(); - // hist->sendRequestId = MTP::send(MTPmessages_ForwardMessage(histPeer->input, MTP_int(item->id), MTP_long(randomId)), App::main()->rpcDone(&MainWidget::sentFullDataReceived, randomId), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); - } else if (msg) { - App::main()->readServerHistory(hist, false); - - newId = clientMsgId(); - - MTPstring msgText(MTP_string(msg->selectedText(FullItemSel))); - - int32 flags = (histPeer->input.type() == mtpc_inputPeerSelf) ? 0 : (MTPDmessage_flag_unread | MTPDmessage_flag_out); - hist->addToBack(MTP_message(MTP_int(flags), MTP_int(newId), MTP_int(MTP::authedId()), App::peerToMTP(histPeer->id), MTPint(), MTPint(), MTPint(), MTP_int(unixtime()), msgText, MTP_messageMediaEmpty())); - hist->sendRequestId = MTP::send(MTPmessages_SendMessage(histPeer->input, MTP_int(0), msgText, MTP_long(randomId)), App::main()->rpcDone(&MainWidget::sentDataReceived, randomId), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); - } - if (newId) { - App::historyRegRandom(randomId, newId); - App::main()->historyToDown(hist); - App::main()->dialogsToUp(); - peerMessagesUpdated(); - onClearSelected(); - } - return 0; + App::main()->finishForwarding(hist); } - - PeerData *toPeer = App::peerLoaded(peer); - if (!toPeer) return 0; - - History *hist = App::history(peer); - App::main()->readServerHistory(hist, false); - - QVector ids; - QVector randomIds; - ids.reserve(toForward.size()); - randomIds.reserve(toForward.size()); - for (SelectedItemSet::const_iterator i = toForward.cbegin(), e = toForward.cend(); i != e; ++i) { - ids.push_back(MTP_int(i.value()->id)); - randomIds.push_back(MTP_long(MTP::nonce())); - //App::historyRegRandom() - } - hist->sendRequestId = MTP::send(MTPmessages_ForwardMessages(toPeer->input, MTP_vector(ids), MTP_vector(randomIds)), App::main()->rpcDone(&MainWidget::forwardDone, peer), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); - return hist->sendRequestId; + if (replyTo < 0) cancelReply(); + _field.setFocus(); } void HistoryWidget::onShareContact(const PeerId &peer, UserData *contact) { @@ -2574,7 +2569,6 @@ void HistoryWidget::onShareContact(const PeerId &peer, UserData *contact) { if (!hist) return; shareContact(peer, contact->phone, contact->firstName, contact->lastName, _replyToId, int32(contact->id & 0xFFFFFFFF)); - onReplyCancel(); } void HistoryWidget::shareContact(const PeerId &peer, const QString &phone, const QString &fname, const QString &lname, MsgId replyTo, int32 userId) { @@ -2598,6 +2592,9 @@ void HistoryWidget::shareContact(const PeerId &peer, const QString &phone, const } App::main()->dialogsToUp(); peerMessagesUpdated(peer); + + App::main()->finishForwarding(h); + cancelReply(); } void HistoryWidget::onSendPaths(const PeerId &peer) { @@ -2640,7 +2637,7 @@ void HistoryWidget::animShow(const QPixmap &bgAnimCache, const QPixmap &bgAnimTo _attachPhoto.hide(); _attachEmoji.hide(); _field.hide(); - _replyCancel.hide(); + _replyForwardCancel.hide(); _send.hide(); a_coord = back ? anim::ivalue(-st::introSlideShift, 0) : anim::ivalue(st::introSlideShift, 0); a_alpha = anim::fvalue(0, 1); @@ -3020,7 +3017,7 @@ void HistoryWidget::updateOnlineDisplayTimer() { void HistoryWidget::onFieldResize() { _field.move(_attachDocument.x() + _attachDocument.width(), height() - _field.height() - st::sendPadding); - _replyCancel.move(width() - _replyCancel.width(), _field.y() - st::sendPadding - _replyCancel.height()); + _replyForwardCancel.move(width() - _replyForwardCancel.width(), _field.y() - st::sendPadding - _replyForwardCancel.height()); updateListSize(); int backy = _scroll.y() + _scroll.height(); update(0, backy, width(), height() - backy); @@ -3031,10 +3028,13 @@ void HistoryWidget::onFieldFocused() { } void HistoryWidget::checkMentionDropdown() { - if (!hist || !hist->peer->chat) return; + if (!hist) return; QString start; - if (_field.getMentionStart(start)) { + _field.getMentionHashtagStart(start); + if (!start.isEmpty()) { + if (start.at(0) == '#' && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) Local::readRecentHashtags(); + if (start.at(0) == '@' && !hist->peer->chat) return; _attachMention.showFiltered(hist->peer->asChat(), start); } else if (!_attachMention.isHidden()) { _attachMention.hideStart(); @@ -3084,7 +3084,7 @@ void HistoryWidget::uploadConfirmImageUncompressed(bool ctrlShiftEnter, MsgId re confirmImageId = 0; confirmWithText = false; confirmImage = QImage(); - onReplyCancel(); + cancelReply(); } void HistoryWidget::uploadMedias(const QStringList &files, ToPrepareMediaType type) { @@ -3092,7 +3092,7 @@ void HistoryWidget::uploadMedias(const QStringList &files, ToPrepareMediaType ty App::wnd()->activateWindow(); imageLoader.append(files, histPeer->id, _replyToId, type); - onReplyCancel(); + cancelReply(); } void HistoryWidget::uploadMedia(const QByteArray &fileContent, ToPrepareMediaType type, PeerId peer) { @@ -3100,7 +3100,7 @@ void HistoryWidget::uploadMedia(const QByteArray &fileContent, ToPrepareMediaTyp App::wnd()->activateWindow(); imageLoader.append(fileContent, peer ? peer : histPeer->id, _replyToId, type); - onReplyCancel(); + cancelReply(); } void HistoryWidget::onPhotoReady() { @@ -3133,7 +3133,6 @@ void HistoryWidget::confirmShareContact(bool ctrlShiftEnter, const QString &phon confirmImage = QImage(); } shareContact(peerId, phone, fname, lname, replyTo); - onReplyCancel(); } void HistoryWidget::confirmSendImage(const ReadyLocalMedia &img) { @@ -3294,7 +3293,7 @@ void HistoryWidget::resizeEvent(QResizeEvent *e) { _attachPhoto.move(_attachDocument.x(), _attachDocument.y()); _field.move(_attachDocument.x() + _attachDocument.width(), height() - _field.height() - st::sendPadding); - _replyCancel.move(width() - _replyCancel.width(), _field.y() - st::sendPadding - _replyCancel.height()); + _replyForwardCancel.move(width() - _replyForwardCancel.width(), _field.y() - st::sendPadding - _replyForwardCancel.height()); updateListSize(); _field.resize(width() - _send.width() - _attachDocument.width() - _attachEmoji.width(), _field.height()); @@ -3329,13 +3328,17 @@ void HistoryWidget::resizeEvent(QResizeEvent *e) { void HistoryWidget::itemRemoved(HistoryItem *item) { if (_list) _list->itemRemoved(item); if (item == _replyTo) { - onReplyCancel(); + cancelReply(); + } + if (item == _replyReturn) { + calcNextReplyReturn(); } } void HistoryWidget::itemReplaced(HistoryItem *oldItem, HistoryItem *newItem) { if (_list) _list->itemReplaced(oldItem, newItem); if (_replyTo == oldItem) _replyTo = newItem; + if (_replyReturn == oldItem) _replyReturn = newItem; } void HistoryWidget::itemResized(HistoryItem *row) { @@ -3360,7 +3363,7 @@ void HistoryWidget::updateListSize(int32 addToY, bool initial, bool loadedDown, } int32 newScrollHeight = height() - (hist->readyForWork() && (!histPeer->chat || !histPeer->asChat()->forbidden) ? (_field.height() + 2 * st::sendPadding) : 0); - if (_replyToId) { + if (_replyToId || App::main()->hasForwardingItems()) { newScrollHeight -= st::replyHeight; } bool wasAtBottom = _scroll.scrollTop() + 1 > _scroll.scrollTopMax(), needResize = _scroll.width() != width() || _scroll.height() != newScrollHeight; @@ -3522,7 +3525,8 @@ void HistoryWidget::onStickerSend(DocumentData *sticker) { hist->addToBackDocument(newId, flags, _replyToId, date(MTP_int(unixtime())), MTP::authedId(), sticker); hist->sendRequestId = MTP::send(MTPmessages_SendMedia(histPeer->input, MTP_int(_replyToId), MTP_inputMediaDocument(MTP_inputDocument(MTP_long(sticker->id), MTP_long(sticker->access))), MTP_long(randomId)), App::main()->rpcDone(&MainWidget::sentFullDataReceived, randomId), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); - onReplyCancel(); + App::main()->finishForwarding(hist); + cancelReply(); App::historyRegRandom(randomId, newId); App::main()->historyToDown(hist); @@ -3549,10 +3553,12 @@ void HistoryWidget::onReplyToMessage() { HistoryItem *to = App::contextItem(); if (!to || to->id <= 0) return; + App::main()->cancelForwarding(); + _replyTo = to; _replyToId = to->id; _replyToText.setText(st::msgFont, _replyTo->inDialogsText(), _textDlgOptions); - if (!_field.isHidden()) _replyCancel.show(); + if (!_field.isHidden()) _replyForwardCancel.show(); updateReplyToName(); resizeEvent(0); update(); @@ -3564,11 +3570,11 @@ void HistoryWidget::onReplyToMessage() { _field.setFocus(); } -void HistoryWidget::onReplyCancel() { +void HistoryWidget::cancelReply() { if (!_replyToId) return; _replyTo = 0; _replyToId = 0; - _replyCancel.hide(); + if (!App::main()->hasForwardingItems()) _replyForwardCancel.hide(); resizeEvent(0); update(); @@ -3577,6 +3583,17 @@ void HistoryWidget::onReplyCancel() { onDraftSave(); } +void HistoryWidget::cancelForwarding() { + _replyForwardCancel.hide(); + resizeEvent(0); + update(); +} + +void HistoryWidget::onReplyForwardCancel() { + App::main()->cancelForwarding(); + cancelReply(); +} + void HistoryWidget::onCancel() { if (App::main()) App::main()->showPeer(0); emit cancelled(); @@ -3711,15 +3728,23 @@ void HistoryWidget::updateReplyTo(bool force) { _replyTo = App::histItemById(_replyToId); if (_replyTo) { _replyToText.setText(st::msgFont, _replyTo->inDialogsText(), _textDlgOptions); - if (!_field.isHidden()) _replyCancel.show(); + if (!_field.isHidden()) _replyForwardCancel.show(); updateReplyToName(); int backy = _scroll.y() + _scroll.height(); update(0, backy, width(), height() - backy); } else if (force) { - onReplyCancel(); + cancelReply(); } } +void HistoryWidget::updateForwarding(bool force) { + if (App::main()->hasForwardingItems()) { + _replyForwardCancel.show(); + } + resizeEvent(0); + update(); +} + void HistoryWidget::updateReplyToName() { if (!_replyTo) return; _replyToName.setText(st::msgServiceNameFont, App::peerName(_replyTo->from()), _textNameOptions); @@ -3728,12 +3753,19 @@ void HistoryWidget::updateReplyToName() { void HistoryWidget::drawFieldBackground(QPainter &p) { int32 backy = _field.y() - st::sendPadding, backh = _field.height() + 2 * st::sendPadding; + Text *from = 0, *text = 0; + bool serviceColor = false; + ImagePtr preview; if (_replyToId) { if (_replyTo && _replyTo->from()->nameVersion > _replyToNameVersion) { updateReplyToName(); } backy -= st::replyHeight; backh += st::replyHeight; + } else if (App::main()->hasForwardingItems()) { + App::main()->fillForwardingInfo(from, text, serviceColor, preview); + backy -= st::replyHeight; + backh += st::replyHeight; } p.fillRect(0, backy, width(), backh, st::taMsgField.bgColor->b); if (_replyToId) { @@ -3754,14 +3786,31 @@ void HistoryWidget::drawFieldBackground(QPainter &p) { replyLeft += st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x(); } p.setPen(st::replyColor->p); - _replyToName.drawElided(p, replyLeft, backy + st::msgReplyPadding.top(), width() - replyLeft - _replyCancel.width() - st::msgReplyPadding.right()); - p.setPen((_replyTo->getMedia() ? st::msgInDateColor : st::msgColor)->p); - _replyToText.drawElided(p, replyLeft, backy + st::msgReplyPadding.top() + st::msgServiceNameFont->height, width() - replyLeft - _replyCancel.width() - st::msgReplyPadding.right()); + _replyToName.drawElided(p, replyLeft, backy + st::msgReplyPadding.top(), width() - replyLeft - _replyForwardCancel.width() - st::msgReplyPadding.right()); + p.setPen(((_replyTo->getMedia() || _replyTo->serviceMsg()) ? st::msgInDateColor : st::msgColor)->p); + _replyToText.drawElided(p, replyLeft, backy + st::msgReplyPadding.top() + st::msgServiceNameFont->height, width() - replyLeft - _replyForwardCancel.width() - st::msgReplyPadding.right()); } else { p.setFont(st::msgDateFont->f); p.setPen(st::msgInDateColor->p); - p.drawText(replyLeft, backy + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2 + st::msgDateFont->ascent, st::msgDateFont->m.elidedText(lang(lng_profile_loading), Qt::ElideRight, width() - replyLeft - _replyCancel.width() - st::msgReplyPadding.right())); + p.drawText(replyLeft, backy + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2 + st::msgDateFont->ascent, st::msgDateFont->m.elidedText(lang(lng_profile_loading), Qt::ElideRight, width() - replyLeft - _replyForwardCancel.width() - st::msgReplyPadding.right())); } + } else if (from && text) { + int32 forwardLeft = st::replySkip; + p.drawPixmap(QPoint(st::replyIconPos.x(), backy + st::replyIconPos.y()), App::sprite(), st::forwardIcon); + if (!preview->isNull()) { + QRect to(forwardLeft, backy + st::msgReplyPadding.top(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height()); + if (preview->width() == preview->height()) { + p.drawPixmap(to.x(), to.y(), preview->pix()); + } else { + QRect from = (preview->width() > preview->height()) ? QRect((preview->width() - preview->height()) / 2, 0, preview->height(), preview->height()) : QRect(0, (preview->height() - preview->width()) / 2, preview->width(), preview->width()); + p.drawPixmap(to, preview->pix(), from); + } + forwardLeft += st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x(); + } + p.setPen(st::replyColor->p); + from->drawElided(p, forwardLeft, backy + st::msgReplyPadding.top(), width() - forwardLeft - _replyForwardCancel.width() - st::msgReplyPadding.right()); + p.setPen((serviceColor ? st::msgInDateColor : st::msgColor)->p); + text->drawElided(p, forwardLeft, backy + st::msgReplyPadding.top() + st::msgServiceNameFont->height, width() - forwardLeft - _replyForwardCancel.width() - st::msgReplyPadding.right()); } } diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 72375ca2c..b809f6271 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -203,7 +203,7 @@ public: void mousePressEvent(QMouseEvent *e); void resizeEvent(QResizeEvent *e); - void offerPeer(PeerId peer); + bool offerPeer(PeerId peer); QString offeredText() const; bool wasOffered() const; @@ -311,7 +311,7 @@ public: void updateOnlineDisplay(int32 x, int32 w); void updateOnlineDisplayTimer(); - mtpRequestId onForward(const PeerId &peer, SelectedItemSet toForward); +// mtpRequestId onForward(const PeerId &peer, SelectedItemSet toForward); void onShareContact(const PeerId &peer, UserData *contact); void onSendPaths(const PeerId &peer); @@ -348,6 +348,15 @@ public: MsgId replyToId() const; void updateReplyTo(bool force = false); + void cancelReply(); + void updateForwarding(bool force = false); + void cancelForwarding(); // called by MainWidget + + void clearReplyReturns(); + void pushReplyReturn(HistoryItem *item); + QList replyReturns(); + void setReplyReturns(PeerId peer, const QList &replyReturns); + void calcNextReplyReturn(); ~HistoryWidget(); @@ -360,7 +369,7 @@ public slots: void onCancel(); void onReplyToMessage(); - void onReplyCancel(); + void onReplyForwardCancel(); void peerUpdated(PeerData *data); void onPeerLoaded(PeerData *data); @@ -423,10 +432,13 @@ private: HistoryItem *_replyTo; Text _replyToName, _replyToText; int32 _replyToNameVersion; - IconedButton _replyCancel; + IconedButton _replyForwardCancel; void updateReplyToName(); void drawFieldBackground(QPainter &p); + HistoryItem *_replyReturn; + QList _replyReturns; + bool messagesFailed(const RPCError &error, mtpRequestId requestId); void updateListSize(int32 addToY = 0, bool initial = false, bool loadedDown = false, HistoryItem *resizedItem = 0); void addMessagesToFront(const QVector &messages); diff --git a/Telegram/SourceFiles/localstorage.cpp b/Telegram/SourceFiles/localstorage.cpp index 6e4295417..66c77ef50 100644 --- a/Telegram/SourceFiles/localstorage.cpp +++ b/Telegram/SourceFiles/localstorage.cpp @@ -494,6 +494,7 @@ namespace { lskRecentStickers, // no data lskBackground, // no data lskUserSettings, // no data + lskRecentHashtags, // no data }; typedef QMap DraftsMap; @@ -514,6 +515,7 @@ namespace { bool _backgroundWasRead = false; FileKey _userSettingsKey = 0; + FileKey _recentHashtagsKey = 0; typedef QPair FileDesc; // file, size typedef QMap StorageMap; @@ -1271,7 +1273,7 @@ namespace { DraftsNotReadMap draftsNotReadMap; StorageMap imagesMap, stickersMap, audiosMap; qint64 storageImagesSize = 0, storageStickersSize = 0, storageAudiosSize = 0; - quint64 locationsKey = 0, recentStickersKey = 0, backgroundKey = 0, userSettingsKey = 0; + quint64 locationsKey = 0, recentStickersKey = 0, backgroundKey = 0, userSettingsKey = 0, recentHashtagsKey = 0; while (!map.stream.atEnd()) { quint32 keyType; map.stream >> keyType; @@ -1345,6 +1347,9 @@ namespace { case lskUserSettings: { map.stream >> userSettingsKey; } break; + case lskRecentHashtags: { + map.stream >> recentHashtagsKey; + } break; default: LOG(("App Error: unknown key type in encrypted map: %1").arg(keyType)); return Local::ReadMapFailed; @@ -1369,6 +1374,7 @@ namespace { _recentStickersKey = recentStickersKey; _backgroundKey = backgroundKey; _userSettingsKey = userSettingsKey; + _recentHashtagsKey = recentHashtagsKey; _oldMapVersion = mapData.version; if (_oldMapVersion < AppVersion) { _mapChanged = true; @@ -1428,9 +1434,10 @@ namespace { if (!_stickersMap.isEmpty()) mapSize += sizeof(quint32) * 2 + _stickersMap.size() * (sizeof(quint64) * 3 + sizeof(qint32)); if (!_audiosMap.isEmpty()) mapSize += sizeof(quint32) * 2 + _audiosMap.size() * (sizeof(quint64) * 3 + sizeof(qint32)); if (_locationsKey) mapSize += sizeof(quint32) + sizeof(quint64); - if (_userSettingsKey) mapSize += sizeof(quint32) + sizeof(quint64); if (_recentStickersKey) mapSize += sizeof(quint32) + sizeof(quint64); if (_backgroundKey) mapSize += sizeof(quint32) + sizeof(quint64); + if (_userSettingsKey) mapSize += sizeof(quint32) + sizeof(quint64); + if (_recentHashtagsKey) mapSize += sizeof(quint32) + sizeof(quint64); EncryptedDescriptor mapData(mapSize); if (!_draftsMap.isEmpty()) { mapData.stream << quint32(lskDraft) << quint32(_draftsMap.size()); @@ -1465,15 +1472,18 @@ namespace { if (_locationsKey) { mapData.stream << quint32(lskLocations) << quint64(_locationsKey); } - if (_userSettingsKey) { - mapData.stream << quint32(lskUserSettings) << quint64(_userSettingsKey); - } if (_recentStickersKey) { mapData.stream << quint32(lskRecentStickers) << quint64(_recentStickersKey); } if (_backgroundKey) { mapData.stream << quint32(lskBackground) << quint64(_backgroundKey); } + if (_userSettingsKey) { + mapData.stream << quint32(lskUserSettings) << quint64(_userSettingsKey); + } + if (_recentHashtagsKey) { + mapData.stream << quint32(lskRecentHashtags) << quint64(_recentHashtagsKey); + } map.writeEncrypted(mapData); _mapChanged = false; @@ -1696,7 +1706,7 @@ namespace Local { _draftsNotReadMap.clear(); _stickersMap.clear(); _audiosMap.clear(); - _locationsKey = _userSettingsKey = _recentStickersKey = _backgroundKey = 0; + _locationsKey = _recentStickersKey = _backgroundKey = _userSettingsKey = _recentHashtagsKey = 0; _mapChanged = true; _writeMap(WriteMapNow); @@ -2199,7 +2209,87 @@ namespace Local { } return false; } - + + void writeRecentHashtags() { + if (!_working()) return; + + const RecentHashtagPack &write(cRecentWriteHashtags()), &search(cRecentSearchHashtags()); + if (write.isEmpty() && search.isEmpty()) readRecentHashtags(); + if (write.isEmpty() && search.isEmpty()) { + if (_recentHashtagsKey) { + clearKey(_recentHashtagsKey); + _recentHashtagsKey = 0; + _mapChanged = true; + } + _writeMap(); + } else { + if (!_recentHashtagsKey) { + _recentHashtagsKey = genKey(); + _mapChanged = true; + _writeMap(WriteMapFast); + } + quint32 size = sizeof(quint32) * 2, writeCnt = 0, searchCnt = 0; + for (RecentHashtagPack::const_iterator i = write.cbegin(); i != write.cend(); ++i) { + if (!i->first.isEmpty()) { + size += _stringSize(i->first) + sizeof(quint16); + ++writeCnt; + } + } + for (RecentHashtagPack::const_iterator i = search.cbegin(); i != search.cend(); ++i) { + if (!i->first.isEmpty()) { + size += _stringSize(i->first) + sizeof(quint16); + ++searchCnt; + } + } + EncryptedDescriptor data(size); + data.stream << quint32(writeCnt) << quint32(searchCnt); + for (RecentHashtagPack::const_iterator i = write.cbegin(); i != write.cend(); ++i) { + if (!i->first.isEmpty()) data.stream << i->first << quint16(i->second); + } + for (RecentHashtagPack::const_iterator i = search.cbegin(); i != search.cend(); ++i) { + if (!i->first.isEmpty()) data.stream << i->first << quint16(i->second); + } + FileWriteDescriptor file(_recentHashtagsKey); + file.writeEncrypted(data); + } + } + + void readRecentHashtags() { + if (!_recentHashtagsKey) return; + + FileReadDescriptor hashtags; + if (!readEncryptedFile(hashtags, _recentHashtagsKey)) { + clearKey(_recentHashtagsKey); + _recentHashtagsKey = 0; + _writeMap(); + return; + } + + quint32 writeCount = 0, searchCount = 0; + hashtags.stream >> writeCount >> searchCount; + + QString tag; + quint16 count; + + RecentHashtagPack write, search; + if (writeCount) { + write.reserve(writeCount); + for (uint32 i = 0; i < writeCount; ++i) { + hashtags.stream >> tag >> count; + write.push_back(qMakePair(tag.trimmed(), count)); + } + } + if (searchCount) { + search.reserve(searchCount); + for (uint32 i = 0; i < searchCount; ++i) { + hashtags.stream >> tag >> count; + search.push_back(qMakePair(tag.trimmed(), count)); + } + } + cSetRecentWriteHashtags(write); + cSetRecentSearchHashtags(search); + } + struct ClearManagerData { QThread *thread; StorageMap images, stickers, audios; @@ -2251,6 +2341,10 @@ namespace Local { _recentStickersKey = 0; _mapChanged = true; } + if (_recentHashtagsKey) { + _recentHashtagsKey = 0; + _mapChanged = true; + } _writeMap(); } else { if (task & ClearManagerStorage) { diff --git a/Telegram/SourceFiles/localstorage.h b/Telegram/SourceFiles/localstorage.h index e3d02caed..df7fa9f75 100644 --- a/Telegram/SourceFiles/localstorage.h +++ b/Telegram/SourceFiles/localstorage.h @@ -137,4 +137,7 @@ namespace Local { void writeBackground(int32 id, const QImage &img); bool readBackground(); + void writeRecentHashtags(); + void readRecentHashtags(); + }; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index eba95a619..414066bba 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -348,7 +348,7 @@ MainWidget *TopBarWidget::main() { return static_cast(parentWidget()); } -MainWidget::MainWidget(Window *window) : QWidget(window), _started(0), failedObjId(0), _dialogsWidth(st::dlgMinWidth), +MainWidget::MainWidget(Window *window) : QWidget(window), _started(0), failedObjId(0), _toForwardNameVersion(0), _dialogsWidth(st::dlgMinWidth), dialogs(this), history(this), profile(0), overview(0), _topBar(this), _forwardConfirm(0), hider(0), _mediaType(this), _mediaTypeMask(0), updGoodPts(0), updLastPts(0), updPtsCount(0), updDate(0), updQts(-1), updSeq(0), updInited(false), updSkipPtsUpdateLevel(0), _onlineRequest(0), _lastWasOnline(false), _lastSetOnline(0), _isIdle(false), _failDifferenceTimeout(1), _lastUpdateTime(0), _cachedX(0), _cachedY(0), _background(0), _api(new ApiWrap(this)) { @@ -403,19 +403,111 @@ _failDifferenceTimeout(1), _lastUpdateTime(0), _cachedX(0), _cachedY(0), _backgr _api->init(); } -mtpRequestId MainWidget::onForward(const PeerId &peer, bool forwardSelected) { - SelectedItemSet selected; +void MainWidget::onForward(const PeerId &peer, bool forwardSelected) { + history.cancelReply(); + _toForward.clear(); if (forwardSelected) { if (overview) { - overview->fillSelectedItems(selected, false); + overview->fillSelectedItems(_toForward, false); } else { - history.fillSelectedItems(selected, false); + history.fillSelectedItems(_toForward, false); } - if (selected.isEmpty()) { - return 0; + } else if (App::contextItem() && dynamic_cast(App::contextItem()) && App::contextItem()->id > 0) { + _toForward.insert(App::contextItem()->id, App::contextItem()); + } + updateForwardingTexts(); + showPeer(peer, 0, false, true); + history.onClearSelected(); + history.updateForwarding(); +} + +bool MainWidget::hasForwardingItems() { + return !_toForward.isEmpty(); +} + +void MainWidget::fillForwardingInfo(Text *&from, Text *&text, bool &serviceColor, ImagePtr &preview) { + if (_toForward.isEmpty()) return; + int32 version = 0; + for (SelectedItemSet::const_iterator i = _toForward.cbegin(), e = _toForward.cend(); i != e; ++i) { + version += i.value()->from()->nameVersion; + } + if (version != _toForwardNameVersion) { + updateForwardingTexts(); + } + from = &_toForwardFrom; + text = &_toForwardText; + serviceColor = (_toForward.size() > 1) || _toForward.cbegin().value()->getMedia() || _toForward.cbegin().value()->serviceMsg(); + if (_toForward.size() < 2 && _toForward.cbegin().value()->getMedia() && _toForward.cbegin().value()->getMedia()->hasReplyPreview()) { + preview = _toForward.cbegin().value()->getMedia()->replyPreview(); + } +} + +void MainWidget::updateForwardingTexts() { + int32 version = 0; + QString from, text; + if (!_toForward.isEmpty()) { + QMap fromUsersMap; + QVector fromUsers; + fromUsers.reserve(_toForward.size()); + for (SelectedItemSet::const_iterator i = _toForward.cbegin(), e = _toForward.cend(); i != e; ++i) { + if (!fromUsersMap.contains(i.value()->from())) { + fromUsersMap.insert(i.value()->from(), true); + fromUsers.push_back(i.value()->from()); + } + version += i.value()->from()->nameVersion; + } + if (fromUsers.size() > 2) { + from = lng_forwarding_from(lt_user, fromUsers.at(0)->firstName, lt_count, fromUsers.size() - 1); + } else if (fromUsers.size() < 2) { + from = fromUsers.at(0)->name; + } else { + from = lng_forwarding_from_two(lt_user, fromUsers.at(0)->firstName, lt_second_user, fromUsers.at(1)->firstName); + } + + if (_toForward.size() < 2) { + text = _toForward.cbegin().value()->inReplyText(); + } else { + text = lng_forward_messages(lt_count, _toForward.size()); } } - return history.onForward(peer, selected); + _toForwardFrom.setText(st::msgServiceNameFont, from, _textNameOptions); + _toForwardText.setText(st::msgFont, text, _textDlgOptions); + _toForwardNameVersion = version; +} + +void MainWidget::cancelForwarding() { + if (_toForward.isEmpty()) return; + + _toForward.clear(); + history.cancelForwarding(); +} + +void MainWidget::finishForwarding(History *hist) { + if (_toForward.isEmpty() || !hist) return; + App::main()->readServerHistory(hist, false); + if (_toForward.size() < 2) { + uint64 randomId = MTP::nonce(); + MsgId newId = clientMsgId(); + hist->addToBackForwarded(newId, static_cast(_toForward.cbegin().value())); + App::historyRegRandom(randomId, newId); + hist->sendRequestId = MTP::send(MTPmessages_ForwardMessage(hist->peer->input, MTP_int(_toForward.cbegin().key()), MTP_long(randomId)), rpcDone(&MainWidget::sentFullDataReceived, randomId), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); + } else { + QVector ids; + QVector randomIds; + ids.reserve(_toForward.size()); + randomIds.reserve(_toForward.size()); + for (SelectedItemSet::const_iterator i = _toForward.cbegin(), e = _toForward.cend(); i != e; ++i) { + uint64 randomId = MTP::nonce(); + //MsgId newId = clientMsgId(); + //hist->addToBackForwarded(newId, static_cast(i.value())); + //App::historyRegRandom(randomId, newId); + ids.push_back(MTP_int(i.key())); + randomIds.push_back(MTP_long(randomId)); + } + hist->sendRequestId = MTP::send(MTPmessages_ForwardMessages(hist->peer->input, MTP_vector(ids), MTP_vector(randomIds)), rpcDone(&MainWidget::forwardDone, hist->peer->id), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); + } + if (history.peer() == hist->peer) history.peerMessagesUpdated(); + cancelForwarding(); } void MainWidget::onShareContact(const PeerId &peer, UserData *contact) { @@ -517,8 +609,7 @@ bool MainWidget::selectingPeer() { } void MainWidget::offerPeer(PeerId peer) { - hider->offerPeer(peer); - if (!cWideMode()) { + if (hider->offerPeer(peer) && !cWideMode()) { _forwardConfirm = new ConfirmBox(hider->offeredText(), lang(lng_forward)); connect(_forwardConfirm, SIGNAL(confirmed()), hider, SLOT(forward())); connect(_forwardConfirm, SIGNAL(cancelled()), this, SLOT(onForwardCancel())); @@ -784,6 +875,7 @@ DialogsIndexed &MainWidget::contactsList() { } void MainWidget::sendPreparedText(History *hist, const QString &text, MsgId replyTo) { + saveRecentHashtags(text); QString sendingText, leftText = text; if (replyTo < 0) replyTo = history.replyToId(); while (textSplit(sendingText, leftText, MaxMessageSize)) { @@ -799,6 +891,8 @@ void MainWidget::sendPreparedText(History *hist, const QString &text, MsgId repl hist->sendRequestId = MTP::send(MTPmessages_SendMessage(hist->peer->input, MTP_int(replyTo), msgText, MTP_long(randomId)), App::main()->rpcDone(&MainWidget::sentDataReceived, randomId), RPCFailHandlerPtr(), 0, 0, hist->sendRequestId); } + finishForwarding(hist); + historyToDown(hist); if (history.peer() == hist->peer) { history.peerMessagesUpdated(); @@ -811,6 +905,34 @@ void MainWidget::sendMessage(History *hist, const QString &text, MsgId replyTo) sendPreparedText(hist, history.prepareMessage(text), replyTo); } +void MainWidget::saveRecentHashtags(const QString &text) { + bool found = false; + QRegularExpressionMatch m; + RecentHashtagPack recent(cRecentWriteHashtags()); + for (int32 i = 0, next = 0; (m = reHashtag().match(text, i)).hasMatch(); i = next) { + i = m.capturedStart(); + next = m.capturedEnd(); + if (m.hasMatch()) { + if (!m.capturedRef(1).isEmpty()) { + ++i; + } + if (!m.capturedRef(2).isEmpty()) { + --next; + } + } + if (!found && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) { + Local::readRecentStickers(); + recent = cRecentWriteHashtags(); + } + found = true; + incrementRecentHashtag(recent, text.mid(i + 1, next - i - 1)); + } + if (found) { + cSetRecentWriteHashtags(recent); + Local::writeRecentHashtags(); + } +} + void MainWidget::readServerHistory(History *hist, bool force) { if (!hist || (!force && (!hist->unreadCount || !hist->readyForWork()))) return; @@ -948,6 +1070,13 @@ void MainWidget::itemRemoved(HistoryItem *item) { history.itemRemoved(item); } itemRemovedGif(item); + if (!_toForward.isEmpty()) { + SelectedItemSet::iterator i = _toForward.find(item->id); + if (i != _toForward.end()) { + _toForward.erase(i); + updateForwardingTexts(); + } + } } void MainWidget::itemReplaced(HistoryItem *oldItem, HistoryItem *newItem) { @@ -957,6 +1086,12 @@ void MainWidget::itemReplaced(HistoryItem *oldItem, HistoryItem *newItem) { history.itemReplaced(oldItem, newItem); } itemReplacedGif(oldItem, newItem); + if (!_toForward.isEmpty()) { + SelectedItemSet::iterator i = _toForward.find(oldItem->id); + if (i != _toForward.end()) { + i.value() = newItem; + } + } } void MainWidget::itemResized(HistoryItem *row) { @@ -1281,12 +1416,12 @@ void MainWidget::confirmShareContact(bool ctrlShiftEnter, const QString &phone, void MainWidget::confirmSendImage(const ReadyLocalMedia &img) { history.confirmSendImage(img); - history.onReplyCancel(); + history.cancelReply(); } void MainWidget::confirmSendImageUncompressed(bool ctrlShiftEnter, MsgId replyTo) { history.uploadConfirmImageUncompressed(ctrlShiftEnter, replyTo); - history.onReplyCancel(); + history.cancelReply(); } void MainWidget::cancelSendImage() { @@ -1427,6 +1562,10 @@ void MainWidget::updateReplyTo() { history.updateReplyTo(true); } +void MainWidget::pushReplyReturn(HistoryItem *item) { + history.pushReplyReturn(item); +} + void MainWidget::setInnerFocus() { if (hider || !history.peer()) { if (hider && hider->wasOffered()) { @@ -1602,7 +1741,7 @@ void MainWidget::showMediaOverview(PeerData *peer, MediaOverviewType type, bool } else if (profile) { _stack.push_back(new StackItemProfile(profile->peer(), profile->lastScrollTop(), profile->allMediaShown())); } else { - _stack.push_back(new StackItemHistory(history.peer(), history.lastWidth(), history.lastScrollTop())); + _stack.push_back(new StackItemHistory(history.peer(), history.lastWidth(), history.lastScrollTop(), history.replyReturns())); } } if (overview) { @@ -1648,7 +1787,7 @@ void MainWidget::showPeerProfile(PeerData *peer, bool back, int32 lastScrollTop, } else if (profile) { _stack.push_back(new StackItemProfile(profile->peer(), profile->lastScrollTop(), profile->allMediaShown())); } else { - _stack.push_back(new StackItemHistory(history.peer(), history.lastWidth(), history.lastScrollTop())); + _stack.push_back(new StackItemHistory(history.peer(), history.lastWidth(), history.lastScrollTop(), history.replyReturns())); } } if (overview) { @@ -1684,6 +1823,7 @@ void MainWidget::showBackFromStack() { if (item->type() == HistoryStackItem) { StackItemHistory *histItem = static_cast(item); showPeer(histItem->peer->id, App::main()->activeMsgId(), true); + history.setReplyReturns(histItem->peer->id, histItem->replyReturns); } else if (item->type() == ProfileStackItem) { StackItemProfile *profItem = static_cast(item); showPeerProfile(profItem->peer, true, profItem->lastScrollTop, profItem->allMediaShown); diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 3cc90fafa..7e0259474 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -110,11 +110,12 @@ public: class StackItemHistory : public StackItem { public: - StackItemHistory(PeerData *peer, int32 lastWidth, int32 lastScrollTop) : StackItem(peer), lastWidth(lastWidth), lastScrollTop(lastScrollTop) { + StackItemHistory(PeerData *peer, int32 lastWidth, int32 lastScrollTop, QList replyReturns) : StackItem(peer), lastWidth(lastWidth), lastScrollTop(lastScrollTop), replyReturns(replyReturns) { } StackItemType type() const { return HistoryStackItem; } + QList replyReturns; int32 lastWidth, lastScrollTop; }; @@ -249,7 +250,7 @@ public: void shareContactLayer(UserData *contact); void hiderLayer(HistoryHider *h); void noHider(HistoryHider *destroyed); - mtpRequestId onForward(const PeerId &peer, bool forwardSelected); + void onForward(const PeerId &peer, bool forwardSelected); void onShareContact(const PeerId &peer, UserData *contact); void onSendPaths(const PeerId &peer); bool selectingPeer(); @@ -287,6 +288,7 @@ public: void sendMessage(History *history, const QString &text, MsgId replyTo); void sendPreparedText(History *hist, const QString &text, MsgId replyTo); + void saveRecentHashtags(const QString &text); void readServerHistory(History *history, bool force = true); @@ -326,7 +328,15 @@ public: ApiWrap *api(); void updateReplyTo(); + + void pushReplyReturn(HistoryItem *item); + bool hasForwardingItems(); + void fillForwardingInfo(Text *&from, Text *&text, bool &serviceColor, ImagePtr &preview); + void updateForwardingTexts(); + void cancelForwarding(); + void finishForwarding(History *hist); // send them + ~MainWidget(); signals: @@ -395,6 +405,10 @@ private: QList _resendImgRandomIds; + SelectedItemSet _toForward; + Text _toForwardFrom, _toForwardText; + int32 _toForwardNameVersion; + void gotDifference(const MTPupdates_Difference &diff); bool failDifference(const RPCError &e); void feedDifference(const MTPVector &users, const MTPVector &chats, const MTPVector &msgs, const MTPVector &other); diff --git a/Telegram/SourceFiles/overviewwidget.cpp b/Telegram/SourceFiles/overviewwidget.cpp index e90b3561d..73da06f58 100644 --- a/Telegram/SourceFiles/overviewwidget.cpp +++ b/Telegram/SourceFiles/overviewwidget.cpp @@ -1274,7 +1274,7 @@ void OverviewInner::fillSelectedItems(SelectedItemSet &sel, bool forDelete) { for (SelectedItems::const_iterator i = _selected.cbegin(), e = _selected.cend(); i != e; ++i) { HistoryItem *item = App::histItemById(i.key()); - if (item && item->itemType() == HistoryItem::MsgType && ((item->id > 0 && !item->serviceMsg()) || forDelete)) { + if (dynamic_cast(item) && item->id > 0) { sel.insert(item->id, item); } } diff --git a/Telegram/SourceFiles/settings.cpp b/Telegram/SourceFiles/settings.cpp index 93cfa1afa..d399a799c 100644 --- a/Telegram/SourceFiles/settings.cpp +++ b/Telegram/SourceFiles/settings.cpp @@ -92,6 +92,8 @@ EmojiStickersMap gEmojiStickers; RecentStickerPack gRecentStickers; +RecentHashtagPack gRecentWriteHashtags, gRecentSearchHashtags; + int32 gLang = -2; // auto QString gLangFile; diff --git a/Telegram/SourceFiles/settings.h b/Telegram/SourceFiles/settings.h index d793fef5d..6f58db58b 100644 --- a/Telegram/SourceFiles/settings.h +++ b/Telegram/SourceFiles/settings.h @@ -177,6 +177,45 @@ DeclareSetting(EmojiStickersMap, EmojiStickers); typedef QList > RecentStickerPack; DeclareSetting(RecentStickerPack, RecentStickers); +typedef QList > RecentHashtagPack; +DeclareSetting(RecentHashtagPack, RecentWriteHashtags); +DeclareSetting(RecentHashtagPack, RecentSearchHashtags); + +inline void incrementRecentHashtag(RecentHashtagPack &recent, const QString &tag) { + RecentHashtagPack::iterator i = recent.begin(), e = recent.end(); + for (; i != e; ++i) { + if (i->first == tag) { + ++i->second; + if (qAbs(i->second) > 0x4000) { + for (RecentHashtagPack::iterator j = recent.begin(); j != e; ++j) { + if (j->second > 1) { + j->second /= 2; + } else if (j->second > 0) { + j->second = 1; + } + } + } + for (; i != recent.begin(); --i) { + if (qAbs((i - 1)->second) > qAbs(i->second)) { + break; + } + qSwap(*i, *(i - 1)); + } + break; + } + } + if (i == e) { + while (recent.size() >= 64) recent.pop_back(); + recent.push_back(qMakePair(tag, 1)); + for (i = recent.end() - 1; i != recent.begin(); --i) { + if ((i - 1)->second > i->second) { + break; + } + qSwap(*i, *(i - 1)); + } + } +} + DeclareSetting(int32, Lang); DeclareSetting(QString, LangFile); diff --git a/Telegram/Telegram.plist b/Telegram/Telegram.plist index cdf8334be..6404ac886 100644 --- a/Telegram/Telegram.plist +++ b/Telegram/Telegram.plist @@ -11,7 +11,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.7.23 + 0.7.24 CFBundleSignature ???? CFBundleURLTypes diff --git a/Telegram/Telegram.rc b/Telegram/Telegram.rc index 50a6995d351df0a1e6f90e6b9c18bfb373b83afe..4bd3ccf752f3c45454a7b5cc0f380c4a28c6ab6d 100644 GIT binary patch delta 53 zcmZ3Yy+nIM5ig_3