/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/share_box.h" #include "dialogs/dialogs_indexed_list.h" #include "lang/lang_keys.h" #include "mainwindow.h" #include "mainwidget.h" #include "base/qthelp_url.h" #include "storage/storage_account.h" #include "boxes/confirm_box.h" #include "apiwrap.h" #include "ui/toast/toast.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/input_fields.h" #include "ui/wrap/slide_wrap.h" #include "ui/text_options.h" #include "chat_helpers/message_field.h" #include "chat_helpers/send_context_menu.h" #include "history/history.h" #include "history/history_message.h" #include "history/view/history_view_schedule_box.h" #include "window/themes/window_theme.h" #include "window/window_session_controller.h" #include "boxes/peer_list_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "data/data_channel.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_changes.h" #include "main/main_session.h" #include "core/application.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_history.h" class ShareBox::Inner final : public Ui::RpWidget, private base::Subscriber { public: Inner( QWidget *parent, not_null navigation, ShareBox::FilterCallback &&filterCallback); void setPeerSelectedChangedCallback( Fn callback); void peerUnselected(not_null peer); std::vector> selected() const; bool hasSelected() const; void peopleReceived( const QString &query, const QVector &my, const QVector &people); void activateSkipRow(int direction); void activateSkipColumn(int direction); void activateSkipPage(int pageHeight, int direction); void updateFilter(QString filter = QString()); void selectActive(); rpl::producer scrollToRequests() const; rpl::producer<> searchRequests() const; protected: void visibleTopBottomUpdated( int visibleTop, int visibleBottom) override; void paintEvent(QPaintEvent *e) override; void enterEventHook(QEvent *e) override; void leaveEventHook(QEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void resizeEvent(QResizeEvent *e) override; private: struct Chat { Chat(PeerData *peer, Fn updateCallback); PeerData *peer; Ui::RoundImageCheckbox checkbox; Ui::Text::String name; Ui::Animations::Simple nameActive; }; void invalidateCache(); int displayedChatsCount() const; void paintChat(Painter &p, not_null chat, int index); void updateChat(not_null peer); void updateChatName(not_null chat, not_null peer); void repaintChat(not_null peer); int chatIndex(not_null peer) const; void repaintChatAtIndex(int index); Chat *getChatAtIndex(int index); void loadProfilePhotos(int yFrom); void changeCheckState(Chat *chat); enum class ChangeStateWay { Default, SkipCallback, }; void changePeerCheckState( not_null chat, bool checked, ChangeStateWay useCallback = ChangeStateWay::Default); not_null getChat(not_null row); void setActive(int active); void updateUpon(const QPoint &pos); void refresh(); const not_null _navigation; float64 _columnSkip = 0.; float64 _rowWidthReal = 0.; int _rowsLeft = 0; int _rowsTop = 0; int _rowWidth = 0; int _rowHeight = 0; int _columnCount = 4; int _active = -1; int _upon = -1; ShareBox::FilterCallback _filterCallback; std::unique_ptr _chatsIndexed; QString _filter; std::vector> _filtered; std::map, std::unique_ptr> _dataMap; base::flat_set> _selected; Fn _peerSelectedChangedCallback; bool _searching = false; QString _lastQuery; std::vector _byUsernameFiltered; std::vector> d_byUsernameFiltered; rpl::event_stream _scrollToRequests; rpl::event_stream<> _searchRequests; }; ShareBox::ShareBox( QWidget*, not_null navigation, CopyCallback &©Callback, SubmitCallback &&submitCallback, FilterCallback &&filterCallback) : _navigation(navigation) , _api(&_navigation->session().mtp()) , _copyCallback(std::move(copyCallback)) , _submitCallback(std::move(submitCallback)) , _filterCallback(std::move(filterCallback)) , _select( this, st::contactsMultiSelect, tr::lng_participant_filter()) , _comment( this, object_ptr( this, st::shareComment, Ui::InputField::Mode::MultiLine, tr::lng_photos_comment()), st::shareCommentPadding) , _searchTimer([=] { searchByUsername(); }) { } void ShareBox::prepareCommentField() { using namespace rpl::mappers; _comment->hide(anim::type::instant); rpl::combine( heightValue(), _comment->heightValue(), _1 - _2 ) | rpl::start_with_next([=](int top) { _comment->moveToLeft(0, top); }, _comment->lifetime()); const auto field = _comment->entity(); connect(field, &Ui::InputField::submitted, [=] { submit({}); }); field->setInstantReplaces(Ui::InstantReplaces::Default()); field->setInstantReplacesEnabled( Core::App().settings().replaceEmojiValue()); field->setMarkdownReplacesEnabled(rpl::single(true)); field->setEditLinkCallback( DefaultEditLinkCallback(_navigation->parentController(), field)); field->setSubmitSettings(Core::App().settings().sendSubmitWay()); InitSpellchecker(_navigation->parentController(), field); Ui::SendPendingMoveResizeEvents(_comment); } void ShareBox::prepare() { prepareCommentField(); _select->resizeToWidth(st::boxWideWidth); Ui::SendPendingMoveResizeEvents(_select); setTitle(tr::lng_share_title()); _inner = setInnerWidget( object_ptr( this, _navigation, std::move(_filterCallback)), getTopScrollSkip(), getBottomScrollSkip()); createButtons(); setDimensions(st::boxWideWidth, st::boxMaxListHeight); _select->setQueryChangedCallback([=](const QString &query) { applyFilterUpdate(query); }); _select->setItemRemovedCallback([=](uint64 itemId) { if (const auto peer = _navigation->session().data().peerLoaded(itemId)) { _inner->peerUnselected(peer); selectedChanged(); update(); } }); _select->setResizedCallback([=] { updateScrollSkips(); }); _select->setSubmittedCallback([=](Qt::KeyboardModifiers modifiers) { if (modifiers.testFlag(Qt::ControlModifier) || modifiers.testFlag(Qt::MetaModifier)) { submit({}); } else { _inner->selectActive(); } }); _comment->heightValue( ) | rpl::start_with_next([=] { updateScrollSkips(); }, _comment->lifetime()); _inner->searchRequests( ) | rpl::start_with_next([=] { needSearchByUsername(); }, _inner->lifetime()); _inner->scrollToRequests( ) | rpl::start_with_next([=](const Ui::ScrollToRequest &request) { scrollTo(request); }, _inner->lifetime()); _inner->setPeerSelectedChangedCallback([=](PeerData *peer, bool checked) { innerSelectedChanged(peer, checked); }); Ui::Emoji::SuggestionsController::Init( getDelegate()->outerContainer(), _comment->entity(), &_navigation->session()); _select->raise(); } int ShareBox::getTopScrollSkip() const { return _select->isHidden() ? 0 : _select->height(); } int ShareBox::getBottomScrollSkip() const { return _comment->isHidden() ? 0 : _comment->height(); } int ShareBox::contentHeight() const { return height() - getTopScrollSkip() - getBottomScrollSkip(); } void ShareBox::updateScrollSkips() { setInnerTopSkip(getTopScrollSkip(), true); setInnerBottomSkip(getBottomScrollSkip()); } bool ShareBox::searchByUsername(bool searchCache) { auto query = _select->getQuery(); if (query.isEmpty()) { if (_peopleRequest) { _peopleRequest = 0; } return true; } if (!query.isEmpty()) { if (searchCache) { auto i = _peopleCache.constFind(query); if (i != _peopleCache.cend()) { _peopleQuery = query; _peopleRequest = 0; peopleDone(i.value(), 0); return true; } } else if (_peopleQuery != query) { _peopleQuery = query; _peopleFull = false; _peopleRequest = _api.request(MTPcontacts_Search( MTP_string(_peopleQuery), MTP_int(SearchPeopleLimit) )).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) { peopleDone(result, requestId); }).fail([=](const RPCError &error, mtpRequestId requestId) { peopleFail(error, requestId); }).send(); _peopleQueries.insert(_peopleRequest, _peopleQuery); } } return false; } void ShareBox::needSearchByUsername() { if (!searchByUsername(true)) { _searchTimer.callOnce(AutoSearchTimeout); } } void ShareBox::peopleDone( const MTPcontacts_Found &result, mtpRequestId requestId) { Expects(result.type() == mtpc_contacts_found); auto query = _peopleQuery; auto i = _peopleQueries.find(requestId); if (i != _peopleQueries.cend()) { query = i.value(); _peopleCache[query] = result; _peopleQueries.erase(i); } if (_peopleRequest == requestId) { switch (result.type()) { case mtpc_contacts_found: { auto &found = result.c_contacts_found(); _navigation->session().data().processUsers(found.vusers()); _navigation->session().data().processChats(found.vchats()); _inner->peopleReceived( query, found.vmy_results().v, found.vresults().v); } break; } _peopleRequest = 0; } } void ShareBox::peopleFail(const RPCError &error, mtpRequestId requestId) { if (_peopleRequest == requestId) { _peopleRequest = 0; _peopleFull = true; } } void ShareBox::setInnerFocus() { if (_comment->isHidden()) { _select->setInnerFocus(); } else { _comment->entity()->setFocusFast(); } } void ShareBox::resizeEvent(QResizeEvent *e) { BoxContent::resizeEvent(e); _select->resizeToWidth(width()); _select->moveToLeft(0, 0); updateScrollSkips(); _inner->resizeToWidth(width()); } void ShareBox::keyPressEvent(QKeyEvent *e) { auto focused = focusWidget(); if (_select == focused || _select->isAncestorOf(focusWidget())) { if (e->key() == Qt::Key_Up) { _inner->activateSkipColumn(-1); } else if (e->key() == Qt::Key_Down) { _inner->activateSkipColumn(1); } else if (e->key() == Qt::Key_PageUp) { _inner->activateSkipPage(contentHeight(), -1); } else if (e->key() == Qt::Key_PageDown) { _inner->activateSkipPage(contentHeight(), 1); } else { BoxContent::keyPressEvent(e); } } else { BoxContent::keyPressEvent(e); } } SendMenu::Type ShareBox::sendMenuType() const { const auto selected = _inner->selected(); return ranges::all_of(selected, HistoryView::CanScheduleUntilOnline) ? SendMenu::Type::ScheduledToUser : (selected.size() == 1 && selected.front()->isSelf()) ? SendMenu::Type::Reminder : SendMenu::Type::Scheduled; } void ShareBox::createButtons() { clearButtons(); if (_hasSelected) { const auto send = addButton(tr::lng_share_confirm(), [=] { submit({}); }); SendMenu::SetupMenuAndShortcuts( send, [=] { return sendMenuType(); }, [=] { submitSilent(); }, [=] { submitScheduled(); }); } else if (_copyCallback) { addButton(tr::lng_share_copy_link(), [=] { copyLink(); }); } addButton(tr::lng_cancel(), [=] { closeBox(); }); } void ShareBox::applyFilterUpdate(const QString &query) { onScrollToY(0); _inner->updateFilter(query); } void ShareBox::addPeerToMultiSelect(PeerData *peer, bool skipAnimation) { using AddItemWay = Ui::MultiSelect::AddItemWay; auto addItemWay = skipAnimation ? AddItemWay::SkipAnimation : AddItemWay::Default; _select->addItem( peer->id, peer->isSelf() ? tr::lng_saved_short(tr::now) : peer->shortName(), st::activeButtonBg, PaintUserpicCallback(peer, true), addItemWay); } void ShareBox::innerSelectedChanged(PeerData *peer, bool checked) { if (checked) { addPeerToMultiSelect(peer); _select->clearQuery(); } else { _select->removeItem(peer->id); } selectedChanged(); update(); } void ShareBox::submit(Api::SendOptions options) { if (_submitCallback) { _submitCallback( _inner->selected(), _comment->entity()->getTextWithAppliedMarkdown(), options); } } void ShareBox::submitSilent() { auto options = Api::SendOptions(); options.silent = true; submit(options); } void ShareBox::submitScheduled() { const auto callback = [=](Api::SendOptions options) { submit(options); }; Ui::show( HistoryView::PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } void ShareBox::copyLink() { if (_copyCallback) { _copyCallback(); } } void ShareBox::selectedChanged() { auto hasSelected = _inner->hasSelected(); if (_hasSelected != hasSelected) { _hasSelected = hasSelected; createButtons(); _comment->toggle(_hasSelected, anim::type::normal); _comment->resizeToWidth(st::boxWideWidth); } update(); } void ShareBox::scrollTo(Ui::ScrollToRequest request) { onScrollToY(request.ymin, request.ymax); //auto scrollTop = scrollArea()->scrollTop(), scrollBottom = scrollTop + scrollArea()->height(); //auto from = scrollTop, to = scrollTop; //if (scrollTop > top) { // to = top; //} else if (scrollBottom < bottom) { // to = bottom - (scrollBottom - scrollTop); //} //if (from != to) { // _scrollAnimation.start([this]() { scrollAnimationCallback(); }, from, to, st::shareScrollDuration, anim::sineInOut); //} } void ShareBox::scrollAnimationCallback() { //auto scrollTop = qRound(_scrollAnimation.current(scrollArea()->scrollTop())); //scrollArea()->scrollToY(scrollTop); } ShareBox::Inner::Inner( QWidget *parent, not_null navigation, ShareBox::FilterCallback &&filterCallback) : RpWidget(parent) , _navigation(navigation) , _filterCallback(std::move(filterCallback)) , _chatsIndexed( std::make_unique( Dialogs::SortMode::Add)) { _rowsTop = st::shareRowsTop; _rowHeight = st::shareRowHeight; setAttribute(Qt::WA_OpaquePaintEvent); const auto self = _navigation->session().user(); if (_filterCallback(self)) { _chatsIndexed->addToEnd(self->owner().history(self)); } const auto addList = [&](not_null list) { for (const auto row : list->all()) { if (const auto history = row->history()) { if (!history->peer->isSelf() && _filterCallback(history->peer)) { _chatsIndexed->addToEnd(history); } } } }; addList(_navigation->session().data().chatsList()->indexed()); const auto id = Data::Folder::kId; if (const auto folder = _navigation->session().data().folderLoaded(id)) { addList(folder->chatsList()->indexed()); } addList(_navigation->session().data().contactsNoChatsList()); _filter = qsl("a"); updateFilter(); _navigation->session().changes().peerUpdates( Data::PeerUpdate::Flag::Photo ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { updateChat(update.peer); }, lifetime()); _navigation->session().changes().realtimeNameUpdates( ) | rpl::start_with_next([=](const Data::NameUpdate &update) { _chatsIndexed->peerNameChanged( update.peer, update.oldFirstLetters); }, lifetime()); _navigation->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { update(); }, lifetime()); subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { if (update.paletteChanged()) { invalidateCache(); } }); } void ShareBox::Inner::invalidateCache() { for (const auto &[peer, data] : _dataMap) { data->checkbox.invalidateCache(); } } void ShareBox::Inner::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { loadProfilePhotos(visibleTop); } void ShareBox::Inner::activateSkipRow(int direction) { activateSkipColumn(direction * _columnCount); } int ShareBox::Inner::displayedChatsCount() const { return _filter.isEmpty() ? _chatsIndexed->size() : (_filtered.size() + d_byUsernameFiltered.size()); } void ShareBox::Inner::activateSkipColumn(int direction) { if (_active < 0) { if (direction > 0) { setActive(0); } return; } auto count = displayedChatsCount(); auto active = _active + direction; if (active < 0) { active = (_active > 0) ? 0 : -1; } if (active >= count) { active = count - 1; } setActive(active); } void ShareBox::Inner::activateSkipPage(int pageHeight, int direction) { activateSkipRow(direction * (pageHeight / _rowHeight)); } void ShareBox::Inner::updateChat(not_null peer) { if (const auto i = _dataMap.find(peer); i != end(_dataMap)) { updateChatName(i->second.get(), peer); repaintChat(peer); } } void ShareBox::Inner::updateChatName( not_null chat, not_null peer) { const auto text = peer->isSelf() ? tr::lng_saved_messages(tr::now) : peer->name; chat->name.setText(st::shareNameStyle, text, Ui::NameTextOptions()); } void ShareBox::Inner::repaintChatAtIndex(int index) { if (index < 0) return; auto row = index / _columnCount; auto column = index % _columnCount; update(style::rtlrect(_rowsLeft + qFloor(column * _rowWidthReal), row * _rowHeight, _rowWidth, _rowHeight, width())); } ShareBox::Inner::Chat *ShareBox::Inner::getChatAtIndex(int index) { if (index < 0) { return nullptr; } const auto row = [=] { if (_filter.isEmpty()) { return _chatsIndexed->rowAtY(index, 1); } return (index < _filtered.size()) ? _filtered[index].get() : nullptr; }(); if (row) { return static_cast(row->attached); } if (!_filter.isEmpty()) { index -= _filtered.size(); if (index >= 0 && index < d_byUsernameFiltered.size()) { return d_byUsernameFiltered[index].get(); } } return nullptr; } void ShareBox::Inner::repaintChat(not_null peer) { repaintChatAtIndex(chatIndex(peer)); } int ShareBox::Inner::chatIndex(not_null peer) const { int index = 0; if (_filter.isEmpty()) { for (const auto row : _chatsIndexed->all()) { if (const auto history = row->history()) { if (history->peer == peer) { return index; } } ++index; } } else { for (const auto row : _filtered) { if (const auto history = row->history()) { if (history->peer == peer) { return index; } } ++index; } for (const auto &row : d_byUsernameFiltered) { if (row->peer == peer) { return index; } ++index; } } return -1; } void ShareBox::Inner::loadProfilePhotos(int yFrom) { if (!parentWidget()) return; if (yFrom < 0) { yFrom = 0; } if (auto part = (yFrom % _rowHeight)) { yFrom -= part; } int yTo = yFrom + parentWidget()->height() * 5 * _columnCount; if (!yTo) { return; } yFrom *= _columnCount; yTo *= _columnCount; if (_filter.isEmpty()) { if (!_chatsIndexed->empty()) { auto i = _chatsIndexed->cfind(yFrom, _rowHeight); for (auto end = _chatsIndexed->cend(); i != end; ++i) { if (((*i)->pos() * _rowHeight) >= yTo) { break; } (*i)->entry()->loadUserpic(); } } } else if (!_filtered.empty()) { int from = yFrom / _rowHeight; if (from < 0) from = 0; if (from < _filtered.size()) { int to = (yTo / _rowHeight) + 1; if (to > _filtered.size()) to = _filtered.size(); for (; from < to; ++from) { _filtered[from]->entry()->loadUserpic(); } } } } auto ShareBox::Inner::getChat(not_null row) -> not_null { Expects(row->history() != nullptr); if (const auto data = static_cast(row->attached)) { return data; } const auto peer = row->history()->peer; if (const auto i = _dataMap.find(peer); i != end(_dataMap)) { row->attached = i->second.get(); return i->second.get(); } const auto [i, ok] = _dataMap.emplace( peer, std::make_unique(peer, [=] { repaintChat(peer); })); updateChatName(i->second.get(), peer); row->attached = i->second.get(); return i->second.get(); } void ShareBox::Inner::setActive(int active) { if (active != _active) { auto changeNameFg = [this](int index, float64 from, float64 to) { if (auto chat = getChatAtIndex(index)) { chat->nameActive.start([this, peer = chat->peer] { repaintChat(peer); }, from, to, st::shareActivateDuration); } }; changeNameFg(_active, 1., 0.); _active = active; changeNameFg(_active, 0., 1.); } auto y = (_active < _columnCount) ? 0 : (_rowsTop + ((_active / _columnCount) * _rowHeight)); _scrollToRequests.fire({ y, y + _rowHeight }); } void ShareBox::Inner::paintChat( Painter &p, not_null chat, int index) { auto x = _rowsLeft + qFloor((index % _columnCount) * _rowWidthReal); auto y = _rowsTop + (index / _columnCount) * _rowHeight; auto outerWidth = width(); auto photoLeft = (_rowWidth - (st::sharePhotoCheckbox.imageRadius * 2)) / 2; auto photoTop = st::sharePhotoTop; chat->checkbox.paint(p, x + photoLeft, y + photoTop, outerWidth); auto nameActive = chat->nameActive.value((index == _active) ? 1. : 0.); p.setPen(anim::pen(st::shareNameFg, st::shareNameActiveFg, nameActive)); auto nameWidth = (_rowWidth - st::shareColumnSkip); auto nameLeft = st::shareColumnSkip / 2; auto nameTop = photoTop + st::sharePhotoCheckbox.imageRadius * 2 + st::shareNameTop; chat->name.drawLeftElided(p, x + nameLeft, y + nameTop, nameWidth, outerWidth, 2, style::al_top, 0, -1, 0, true); } ShareBox::Inner::Chat::Chat(PeerData *peer, Fn updateCallback) : peer(peer) , checkbox(st::sharePhotoCheckbox, updateCallback, PaintUserpicCallback(peer, true)) , name(st::sharePhotoCheckbox.imageRadius * 2) { } void ShareBox::Inner::paintEvent(QPaintEvent *e) { Painter p(this); auto r = e->rect(); p.setClipRect(r); p.fillRect(r, st::boxBg); auto yFrom = r.y(), yTo = r.y() + r.height(); auto rowFrom = yFrom / _rowHeight; auto rowTo = (yTo + _rowHeight - 1) / _rowHeight; auto indexFrom = rowFrom * _columnCount; auto indexTo = rowTo * _columnCount; if (_filter.isEmpty()) { if (!_chatsIndexed->empty()) { auto i = _chatsIndexed->cfind(indexFrom, 1); for (auto end = _chatsIndexed->cend(); i != end; ++i) { if (indexFrom >= indexTo) { break; } paintChat(p, getChat(*i), indexFrom); ++indexFrom; } } else { p.setFont(st::noContactsFont); p.setPen(st::noContactsColor); p.drawText( rect().marginsRemoved(st::boxPadding), tr::lng_bot_no_chats(tr::now), style::al_center); } } else { if (_filtered.empty() && _byUsernameFiltered.empty() && !_searching) { p.setFont(st::noContactsFont); p.setPen(st::noContactsColor); p.drawText( rect().marginsRemoved(st::boxPadding), tr::lng_bot_chats_not_found(tr::now), style::al_center); } else { auto filteredSize = _filtered.size(); if (filteredSize) { if (indexFrom < 0) indexFrom = 0; while (indexFrom < indexTo) { if (indexFrom >= _filtered.size()) { break; } paintChat(p, getChat(_filtered[indexFrom]), indexFrom); ++indexFrom; } indexFrom -= filteredSize; indexTo -= filteredSize; } if (!_byUsernameFiltered.empty()) { if (indexFrom < 0) indexFrom = 0; while (indexFrom < indexTo) { if (indexFrom >= d_byUsernameFiltered.size()) { break; } paintChat( p, d_byUsernameFiltered[indexFrom].get(), filteredSize + indexFrom); ++indexFrom; } } } } } void ShareBox::Inner::enterEventHook(QEvent *e) { setMouseTracking(true); } void ShareBox::Inner::leaveEventHook(QEvent *e) { setMouseTracking(false); } void ShareBox::Inner::mouseMoveEvent(QMouseEvent *e) { updateUpon(e->pos()); setCursor((_upon >= 0) ? style::cur_pointer : style::cur_default); } void ShareBox::Inner::updateUpon(const QPoint &pos) { auto x = pos.x(), y = pos.y(); auto row = (y - _rowsTop) / _rowHeight; auto column = qFloor((x - _rowsLeft) / _rowWidthReal); auto left = _rowsLeft + qFloor(column * _rowWidthReal) + st::shareColumnSkip / 2; auto top = _rowsTop + row * _rowHeight + st::sharePhotoTop; auto xupon = (x >= left) && (x < left + (_rowWidth - st::shareColumnSkip)); auto yupon = (y >= top) && (y < top + st::sharePhotoCheckbox.imageRadius * 2 + st::shareNameTop + st::shareNameStyle.font->height * 2); auto upon = (xupon && yupon) ? (row * _columnCount + column) : -1; if (upon >= displayedChatsCount()) { upon = -1; } _upon = upon; } void ShareBox::Inner::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { updateUpon(e->pos()); changeCheckState(getChatAtIndex(_upon)); } } void ShareBox::Inner::selectActive() { changeCheckState(getChatAtIndex(_active > 0 ? _active : 0)); } void ShareBox::Inner::resizeEvent(QResizeEvent *e) { _columnSkip = (width() - _columnCount * st::sharePhotoCheckbox.imageRadius * 2) / float64(_columnCount + 1); _rowWidthReal = st::sharePhotoCheckbox.imageRadius * 2 + _columnSkip; _rowsLeft = qFloor(_columnSkip / 2); _rowWidth = qFloor(_rowWidthReal); update(); } void ShareBox::Inner::changeCheckState(Chat *chat) { if (!chat) return; if (!_filter.isEmpty()) { const auto history = chat->peer->owner().history(chat->peer); auto row = _chatsIndexed->getRow(history); if (!row) { row = _chatsIndexed->addToEnd(history).main; } chat = getChat(row); if (!chat->checkbox.checked()) { _chatsIndexed->moveToTop(history); } } changePeerCheckState(chat, !chat->checkbox.checked()); } void ShareBox::Inner::peerUnselected(not_null peer) { if (const auto i = _dataMap.find(peer); i != end(_dataMap)) { changePeerCheckState( i->second.get(), false, ChangeStateWay::SkipCallback); } } void ShareBox::Inner::setPeerSelectedChangedCallback( Fn callback) { _peerSelectedChangedCallback = std::move(callback); } void ShareBox::Inner::changePeerCheckState( not_null chat, bool checked, ChangeStateWay useCallback) { chat->checkbox.setChecked(checked); if (checked) { _selected.insert(chat->peer); setActive(chatIndex(chat->peer)); } else { _selected.remove(chat->peer); } if (useCallback != ChangeStateWay::SkipCallback && _peerSelectedChangedCallback) { _peerSelectedChangedCallback(chat->peer, checked); } } bool ShareBox::Inner::hasSelected() const { return _selected.size(); } void ShareBox::Inner::updateFilter(QString filter) { _lastQuery = filter.toLower().trimmed(); auto words = TextUtilities::PrepareSearchWords(_lastQuery); filter = words.isEmpty() ? QString() : words.join(' '); if (_filter != filter) { _filter = filter; _byUsernameFiltered.clear(); d_byUsernameFiltered.clear(); if (_filter.isEmpty()) { refresh(); } else { _filtered = _chatsIndexed->filtered(words); refresh(); _searching = true; _searchRequests.fire({}); } setActive(-1); update(); loadProfilePhotos(0); } } rpl::producer ShareBox::Inner::scrollToRequests() const { return _scrollToRequests.events(); } rpl::producer<> ShareBox::Inner::searchRequests() const { return _searchRequests.events(); } void ShareBox::Inner::peopleReceived( const QString &query, const QVector &my, const QVector &people) { _lastQuery = query.toLower().trimmed(); if (_lastQuery.at(0) == '@') { _lastQuery = _lastQuery.mid(1); } int32 already = _byUsernameFiltered.size(); _byUsernameFiltered.reserve(already + my.size() + people.size()); d_byUsernameFiltered.reserve(already + my.size() + people.size()); const auto feedList = [&](const QVector &list) { for (const auto &data : list) { if (const auto peer = _navigation->session().data().peerLoaded(peerFromMTP(data))) { const auto history = _navigation->session().data().historyLoaded(peer); if (!_filterCallback(peer)) { continue; } else if (history && _chatsIndexed->getRow(history)) { continue; } else if (base::contains(_byUsernameFiltered, peer)) { continue; } _byUsernameFiltered.push_back(peer); d_byUsernameFiltered.push_back(std::make_unique( peer, [=] { repaintChat(peer); })); updateChatName(d_byUsernameFiltered.back().get(), peer); } } }; feedList(my); feedList(people); _searching = false; refresh(); } void ShareBox::Inner::refresh() { auto count = displayedChatsCount(); if (count) { auto rows = (count / _columnCount) + (count % _columnCount ? 1 : 0); resize(width(), _rowsTop + rows * _rowHeight); } else { resize(width(), st::noContactsHeight); } update(); } std::vector> ShareBox::Inner::selected() const { auto result = std::vector>(); result.reserve(_dataMap.size()); for (const auto &[peer, chat] : _dataMap) { if (chat->checkbox.checked()) { result.push_back(peer); } } return result; } QString AppendShareGameScoreUrl( not_null session, const QString &url, const FullMsgId &fullId) { auto shareHashData = QByteArray(0x10, Qt::Uninitialized); auto shareHashDataInts = reinterpret_cast(shareHashData.data()); auto channel = fullId.channel ? session->data().channelLoaded(fullId.channel) : static_cast(nullptr); auto channelAccessHash = channel ? channel->access : 0ULL; auto channelAccessHashInts = reinterpret_cast(&channelAccessHash); shareHashDataInts[0] = session->userId(); shareHashDataInts[1] = fullId.channel; shareHashDataInts[2] = fullId.msg; shareHashDataInts[3] = channelAccessHashInts[0]; // Count SHA1() of data. auto key128Size = 0x10; auto shareHashEncrypted = QByteArray(key128Size + shareHashData.size(), Qt::Uninitialized); hashSha1(shareHashData.constData(), shareHashData.size(), shareHashEncrypted.data()); // Mix in channel access hash to the first 64 bits of SHA1 of data. *reinterpret_cast(shareHashEncrypted.data()) ^= *reinterpret_cast(channelAccessHashInts); // Encrypt data. if (!session->local().encrypt(shareHashData.constData(), shareHashEncrypted.data() + key128Size, shareHashData.size(), shareHashEncrypted.constData())) { return url; } auto shareHash = shareHashEncrypted.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); auto shareUrl = qsl("tg://share_game_score?hash=") + QString::fromLatin1(shareHash); auto shareComponent = qsl("tgShareScoreUrl=") + qthelp::url_encode(shareUrl); auto hashPosition = url.indexOf('#'); if (hashPosition < 0) { return url + '#' + shareComponent; } auto hash = url.mid(hashPosition + 1); if (hash.indexOf('=') >= 0 || hash.indexOf('?') >= 0) { return url + '&' + shareComponent; } if (!hash.isEmpty()) { return url + '?' + shareComponent; } return url + shareComponent; } void ShareGameScoreByHash( not_null session, const QString &hash) { auto key128Size = 0x10; auto hashEncrypted = QByteArray::fromBase64(hash.toLatin1(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); if (hashEncrypted.size() <= key128Size || (hashEncrypted.size() % 0x10) != 0) { Ui::show(Box(tr::lng_confirm_phone_link_invalid(tr::now))); return; } // Decrypt data. auto hashData = QByteArray(hashEncrypted.size() - key128Size, Qt::Uninitialized); if (!session->local().decrypt(hashEncrypted.constData() + key128Size, hashData.data(), hashEncrypted.size() - key128Size, hashEncrypted.constData())) { return; } // Count SHA1() of data. char dataSha1[20] = { 0 }; hashSha1(hashData.constData(), hashData.size(), dataSha1); // Mix out channel access hash from the first 64 bits of SHA1 of data. auto channelAccessHash = *reinterpret_cast(hashEncrypted.data()) ^ *reinterpret_cast(dataSha1); // Check next 64 bits of SHA1() of data. auto skipSha1Part = sizeof(channelAccessHash); if (memcmp(dataSha1 + skipSha1Part, hashEncrypted.constData() + skipSha1Part, key128Size - skipSha1Part) != 0) { Ui::show(Box(tr::lng_share_wrong_user(tr::now))); return; } auto hashDataInts = reinterpret_cast(hashData.data()); if (hashDataInts[0] != session->userId()) { Ui::show(Box(tr::lng_share_wrong_user(tr::now))); return; } // Check first 32 bits of channel access hash. auto channelAccessHashInts = reinterpret_cast(&channelAccessHash); if (channelAccessHashInts[0] != hashDataInts[3]) { Ui::show(Box(tr::lng_share_wrong_user(tr::now))); return; } auto channelId = hashDataInts[1]; auto msgId = hashDataInts[2]; if (!channelId && channelAccessHash) { // If there is no channel id, there should be no channel access_hash. Ui::show(Box(tr::lng_share_wrong_user(tr::now))); return; } if (const auto item = session->data().message(channelId, msgId)) { FastShareMessage(item); } else { auto resolveMessageAndShareScore = [=](ChannelData *channel) { session->api().requestMessageData(channel, msgId, [=]( ChannelData *channel, MsgId msgId) { if (const auto item = session->data().message(channel, msgId)) { FastShareMessage(item); } else { Ui::show(Box(tr::lng_edit_deleted(tr::now))); } }); }; const auto channel = channelId ? session->data().channelLoaded(channelId) : nullptr; if (channel || !channelId) { resolveMessageAndShareScore(channel); } else { session->api().request(MTPchannels_GetChannels( MTP_vector( 1, MTP_inputChannel( MTP_int(channelId), MTP_long(channelAccessHash))) )).done([=](const MTPmessages_Chats &result) { result.match([&](const auto &data) { session->data().processChats(data.vchats()); }); if (const auto channel = session->data().channelLoaded(channelId)) { resolveMessageAndShareScore(channel); } }).send(); } } }