Compare commits

...

48 Commits

Author SHA1 Message Date
John Preston
7e2e510d8a Beta version 5.7.3: Fix build with Xcode. 2024-11-13 00:51:35 +04:00
John Preston
1ed34c6fa0 Beta version 5.7.3.
- Option to show folders above the chats list.
- Folders on top of recipients list box.
2024-11-13 00:19:58 +04:00
John Preston
78a0fa55b5 Send correct round video upload actions. 2024-11-12 22:58:05 +04:00
23rd
d37c040b36 Improved style of button for earn out when it is disabled. 2024-11-11 19:32:21 +03:00
23rd
e56bbf557d Added ability to open filters list in context menu from share boxes. 2024-11-11 10:18:00 +03:00
23rd
5abecec478 Fixed display of sponsored messages at bottom for bots. 2024-11-11 10:18:00 +03:00
23rd
ccb41f778e Removed selection marks from unselectable messages. 2024-11-11 10:18:00 +03:00
23rd
059a4cf0d8 Fixed ripple animation for sponsored messages.
Regression was introduced in 3f6d184435.
2024-11-11 10:18:00 +03:00
23rd
7a535a4554 Fixed display of sponsored messages with long descriptions and media. 2024-11-11 10:18:00 +03:00
23rd
f89167ef94 Fixed display of chats filters strip from dialogs widget in search mode. 2024-11-11 10:18:00 +03:00
23rd
a77777f509 Fixed display of chats filters strip from share box in search mode. 2024-11-11 10:18:00 +03:00
23rd
4a327ba584 Fixed display of chats filters strip from forward box in search mode. 2024-11-11 10:18:00 +03:00
23rd
a41e9bf67e Added preview of chats filters strip to filter link box. 2024-11-11 10:18:00 +03:00
23rd
6716973ce0 Added ability to reorder tabs of chats filters strip. 2024-11-11 10:18:00 +03:00
23rd
7cc81393d6 Adapted chats filters tabs slider for reorder feature. 2024-11-11 10:18:00 +03:00
23rd
3e413a036f Added initial implementation of class for reorder of tabs slider. 2024-11-11 10:18:00 +03:00
23rd
63a8fe7ee8 Added direct access to sections of discrete slider to derived classes. 2024-11-11 10:18:00 +03:00
23rd
146409844d Simplified geometry management of chats filters strip. 2024-11-11 10:18:00 +03:00
23rd
ba0da9f59e Slightly improved process of switching between chats filters view types. 2024-11-11 10:18:00 +03:00
23rd
81aef519d4 Added support of setting for chats filters view type to filters menu. 2024-11-11 10:18:00 +03:00
23rd
bcd84518d1 Added tracking of active chats filter to chats filters strip. 2024-11-11 10:18:00 +03:00
23rd
f205952ff2 Added chats filters strip to dialogs widget. 2024-11-11 10:18:00 +03:00
23rd
1d7622e0b5 Added condition to hide chats filters strip. 2024-11-11 10:18:00 +03:00
23rd
4d9112283d Added support of locked sections to chats filters tabs slider. 2024-11-11 10:18:00 +03:00
23rd
dc49c788a8 Added setting for chat filters view type to filters settings. 2024-11-11 10:18:00 +03:00
23rd
36741ab780 Reused setting for legacy iv zoom for chat filters view type. 2024-11-11 10:18:00 +03:00
23rd
53dffc5e88 Added context menu to chats filters strip. 2024-11-11 10:18:00 +03:00
23rd
607c7e7777 Added context menu process to chats filters tabs slider. 2024-11-11 10:18:00 +03:00
23rd
6f09e1699f Moved out display of remove filter box from chats filters menu. 2024-11-11 10:18:00 +03:00
23rd
8c35de48f3 Added process of unread states to chats filters strip. 2024-11-11 10:18:00 +03:00
23rd
b83d943841 Moved out value notification settings for filters to separated file. 2024-11-11 10:18:00 +03:00
23rd
b11b5caeb3 Moved out unread state values of main list to separated file. 2024-11-11 10:18:00 +03:00
23rd
36924da59a Replaced inner slider in chats filters strip. 2024-11-11 10:18:00 +03:00
23rd
f0a2c47613 Added initial implementation of chats filters tabs slider to td_ui. 2024-11-11 10:18:00 +03:00
23rd
5a4449f1a2 Added ability to set custom width of section to DiscreteSlider. 2024-11-11 10:18:00 +03:00
23rd
de3d7a7774 Removed redundant templates from DiscreteSlider. 2024-11-11 10:18:00 +03:00
23rd
b06dbd1c00 Added support of pinned chats from filters to forward box. 2024-11-11 10:18:00 +03:00
23rd
1fa5e424e9 Added chats filters tabs strip to share box. 2024-11-11 10:18:00 +03:00
23rd
d81c7abf1a Added chats filters strip to forward box. 2024-11-11 10:18:00 +03:00
23rd
932215c91d Added initial implementation of tabs strip for chats filters. 2024-11-11 10:18:00 +03:00
23rd
7aa1141ba5 Added ability to ignore hidden rows while searching to PeerListContent. 2024-11-11 10:18:00 +03:00
23rd
3699439506 Added ability to count sections widths of slider from derived class. 2024-11-11 10:18:00 +03:00
23rd
76b1288f77 Added access to height of multi select from peer list box. 2024-11-11 10:18:00 +03:00
xmdn
8fd9ae4e59 Replace deprecated x64 architecture identifier 2024-11-11 11:16:08 +04:00
John Preston
53abd2fe38 Follow show-panel-on-click for attach menu. 2024-11-06 17:53:09 +04:00
xiota
da8a4ba8ab Fix some ffmpeg 7.x related errors 2024-11-06 16:42:18 +04:00
John Preston
9c3990c0c1 Fix deploy scripts for Windows on ARM. 2024-11-06 16:41:56 +04:00
John Preston
1eeb46d5fc Fix build on VS 17.12 Preview 4 on ARM. 2024-11-06 12:26:12 +04:00
68 changed files with 2425 additions and 364 deletions

View File

@@ -112,6 +112,8 @@ PRIVATE
api/api_bot.h
api/api_chat_filters.cpp
api/api_chat_filters.h
api/api_chat_filters_remove_manager.cpp
api/api_chat_filters_remove_manager.h
api/api_chat_invite.cpp
api/api_chat_invite.h
api/api_chat_links.cpp
@@ -636,6 +638,8 @@ PRIVATE
data/data_thread.h
data/data_types.cpp
data/data_types.h
data/data_unread_value.cpp
data/data_unread_value.h
data/data_user.cpp
data/data_user.h
data/data_user_photos.cpp
@@ -1537,6 +1541,8 @@ PRIVATE
ui/widgets/expandable_peer_list.h
ui/widgets/label_with_custom_emoji.cpp
ui/widgets/label_with_custom_emoji.h
ui/widgets/chat_filters_tabs_strip.cpp
ui/widgets/chat_filters_tabs_strip.h
ui/countryinput.cpp
ui/countryinput.h
ui/dynamic_thumbnails.cpp

View File

@@ -5099,6 +5099,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_filters_toast_add" = "{chat} added to {folder} folder";
"lng_filters_toast_remove" = "{chat} removed from {folder} folder";
"lng_filters_shareable_status" = "shareable folder";
"lng_filters_view_subtitle" = "Tabs view";
"lng_filters_vertical" = "Tabs on the left";
"lng_filters_horizontal" = "Tabs at the top";
"lng_filters_delete_sure" = "Are you sure you want to delete this folder? This will also deactivate all the invite links created to share this folder.";
"lng_filters_link" = "Share Folder";

View File

@@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.7.2.0" />
Version="5.7.3.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View File

@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,7,2,0
PRODUCTVERSION 5,7,2,0
FILEVERSION 5,7,3,0
PRODUCTVERSION 5,7,3,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "5.7.2.0"
VALUE "FileVersion", "5.7.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "5.7.2.0"
VALUE "ProductVersion", "5.7.3.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,7,2,0
PRODUCTVERSION 5,7,2,0
FILEVERSION 5,7,3,0
PRODUCTVERSION 5,7,3,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
VALUE "FileVersion", "5.7.2.0"
VALUE "FileVersion", "5.7.3.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "5.7.2.0"
VALUE "ProductVersion", "5.7.3.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_limits_box.h"
#include "boxes/filters/edit_filter_links.h" // FilterChatStatusText
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_chat_filters.h"
@@ -152,6 +153,7 @@ void InitFilterLinkHeader(
.badge = (type == Ui::FilterLinkHeaderType::AddingChats
? std::move(count)
: rpl::single(0)),
.horizontalFilters = Core::App().settings().chatFiltersHorizontal(),
});
const auto widget = header.widget;
widget->resizeToWidth(st::boxWideWidth);

View File

@@ -0,0 +1,128 @@
/*
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 "api/api_chat_filters_remove_manager.h"
#include "api/api_chat_filters.h"
#include "apiwrap.h"
#include "data/data_chat_filters.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/ui_utility.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
namespace Api {
namespace {
void RemoveChatFilter(
not_null<Main::Session*> session,
FilterId filterId,
std::vector<not_null<PeerData*>> leave) {
const auto api = &session->api();
session->data().chatsFilters().apply(MTP_updateDialogFilter(
MTP_flags(MTPDupdateDialogFilter::Flag(0)),
MTP_int(filterId),
MTPDialogFilter()));
if (leave.empty()) {
api->request(MTPmessages_UpdateDialogFilter(
MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)),
MTP_int(filterId),
MTPDialogFilter()
)).send();
} else {
api->request(MTPchatlists_LeaveChatlist(
MTP_inputChatlistDialogFilter(MTP_int(filterId)),
MTP_vector<MTPInputPeer>(ranges::views::all(
leave
) | ranges::views::transform([](not_null<PeerData*> peer) {
return MTPInputPeer(peer->input);
}) | ranges::to<QVector<MTPInputPeer>>())
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
}).send();
}
}
} // namespace
RemoveComplexChatFilter::RemoveComplexChatFilter() = default;
void RemoveComplexChatFilter::request(
QPointer<Ui::RpWidget> widget,
base::weak_ptr<Window::SessionController> weak,
FilterId id) {
const auto session = &weak->session();
const auto &list = session->data().chatsFilters().list();
const auto i = ranges::find(list, id, &Data::ChatFilter::id);
const auto filter = (i != end(list)) ? *i : Data::ChatFilter();
const auto has = filter.hasMyLinks();
const auto confirm = [=](Fn<void()> action, bool onlyWhenHas = false) {
if (!has && onlyWhenHas) {
action();
return;
}
weak->window().show(Ui::MakeConfirmBox({
.text = (has
? tr::lng_filters_delete_sure()
: tr::lng_filters_remove_sure()),
.confirmed = [=](Fn<void()> &&close) { close(); action(); },
.confirmText = (has
? tr::lng_box_delete()
: tr::lng_filters_remove_yes()),
.confirmStyle = &st::attentionBoxButton,
}));
};
const auto simple = [=] {
confirm([=] { RemoveChatFilter(session, id, {}); });
};
const auto suggestRemoving = Api::ExtractSuggestRemoving(filter);
if (suggestRemoving.empty()) {
simple();
return;
} else if (_removingRequestId) {
if (_removingId == id) {
return;
}
session->api().request(_removingRequestId).cancel();
}
_removingId = id;
_removingRequestId = session->api().request(
MTPchatlists_GetLeaveChatlistSuggestions(
MTP_inputChatlistDialogFilter(
MTP_int(id)))
).done(crl::guard(widget, [=, this](const MTPVector<MTPPeer> &result) {
_removingRequestId = 0;
const auto suggestRemovePeers = ranges::views::all(
result.v
) | ranges::views::transform([=](const MTPPeer &peer) {
return session->data().peer(peerFromMTP(peer));
}) | ranges::to_vector;
const auto chosen = crl::guard(widget, [=](
std::vector<not_null<PeerData*>> peers) {
RemoveChatFilter(session, id, std::move(peers));
});
confirm(crl::guard(widget, [=] {
Api::ProcessFilterRemove(
weak,
filter.title(),
filter.iconEmoji(),
suggestRemoving,
suggestRemovePeers,
chosen);
}), true);
})).fail(crl::guard(widget, [=, this] {
_removingRequestId = 0;
simple();
})).send();
}
} // namespace Api

View File

@@ -0,0 +1,35 @@
/*
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
*/
#pragma once
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Api {
class RemoveComplexChatFilter final {
public:
RemoveComplexChatFilter();
void request(
QPointer<Ui::RpWidget> widget,
base::weak_ptr<Window::SessionController> weak,
FilterId id);
private:
FilterId _removingId = 0;
mtpRequestId _removingRequestId = 0;
};
} // namespace Api

View File

@@ -206,7 +206,9 @@ void PeerListBox::keyPressEvent(QKeyEvent *e) {
content()->selectSkipPage(height(), 1);
} else if (e->key() == Qt::Key_PageUp) {
content()->selectSkipPage(height(), -1);
} else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) {
} else if (e->key() == Qt::Key_Escape
&& _select
&& !_select->entity()->getQuery().isEmpty()) {
_select->entity()->clearQuery();
} else {
BoxContent::keyPressEvent(e);
@@ -215,7 +217,16 @@ void PeerListBox::keyPressEvent(QKeyEvent *e) {
void PeerListBox::searchQueryChanged(const QString &query) {
scrollToY(0);
content()->searchQueryChanged(query);
const auto isEmpty = content()->searchQueryChanged(query);
if (_specialTabsMode.enabled) {
_specialTabsMode.searchIsActive = !isEmpty;
if (_specialTabsMode.searchIsActive) {
_specialTabsMode.topSkip = _addedTopScrollSkip;
setAddedTopScrollSkip(0);
} else {
setAddedTopScrollSkip(_specialTabsMode.topSkip);
}
}
}
void PeerListBox::resizeEvent(QResizeEvent *e) {
@@ -543,6 +554,19 @@ auto PeerListBox::collectSelectedRows()
return result;
}
rpl::producer<int> PeerListBox::multiSelectHeightValue() const {
return _select ? _select->heightValue() : rpl::single(0);
}
void PeerListBox::setSpecialTabMode(bool value) {
content()->setIgnoreHiddenRowsOnSearch(value);
if (value) {
_specialTabsMode.enabled = true;
} else {
_specialTabsMode = {};
}
}
PeerListRow::PeerListRow(not_null<PeerData*> peer)
: PeerListRow(peer, peer->id.value) {
}
@@ -1385,10 +1409,12 @@ int PeerListContent::labelHeight() const {
void PeerListContent::refreshRows() {
if (!_hiddenRows.empty()) {
_filterResults.clear();
for (const auto &row : _rows) {
if (!row->hidden()) {
_filterResults.push_back(row.get());
if (!_ignoreHiddenRowsOnSearch || _normalizedSearchQuery.isEmpty()) {
_filterResults.clear();
for (const auto &row : _rows) {
if (!row->hidden()) {
_filterResults.push_back(row.get());
}
}
}
}
@@ -2050,13 +2076,16 @@ void PeerListContent::checkScrollForPreload() {
}
}
void PeerListContent::searchQueryChanged(QString query) {
PeerListContent::IsEmpty PeerListContent::searchQueryChanged(QString query) {
const auto searchWordsList = TextUtilities::PrepareSearchWords(query);
const auto normalizedQuery = searchWordsList.join(' ');
if (_ignoreHiddenRowsOnSearch && !normalizedQuery.isEmpty()) {
_filterResults.clear();
}
if (_normalizedSearchQuery != normalizedQuery) {
setSearchQuery(query, normalizedQuery);
if (_controller->searchInLocal() && !searchWordsList.isEmpty()) {
Assert(_hiddenRows.empty());
Assert(_hiddenRows.empty() || _ignoreHiddenRowsOnSearch);
auto minimalList = (const std::vector<not_null<PeerListRow*>>*)nullptr;
for (const auto &searchWord : searchWordsList) {
@@ -2104,6 +2133,7 @@ void PeerListContent::searchQueryChanged(QString query) {
}
refreshRows();
}
return _normalizedSearchQuery.isEmpty();
}
std::unique_ptr<PeerListState> PeerListContent::saveState() const {
@@ -2192,6 +2222,10 @@ void PeerListContent::dragLeft() {
clearSelection();
}
void PeerListContent::setIgnoreHiddenRowsOnSearch(bool value) {
_ignoreHiddenRowsOnSearch = value;
}
void PeerListContent::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {

View File

@@ -652,12 +652,15 @@ public:
[[nodiscard]] bool hasPressed() const;
void clearSelection();
void searchQueryChanged(QString query);
using IsEmpty = bool;
IsEmpty searchQueryChanged(QString query);
bool submitted();
PeerListRowId updateFromParentDrag(QPoint globalPosition);
void dragLeft();
void setIgnoreHiddenRowsOnSearch(bool value);
// Interface for the controller.
void appendRow(std::unique_ptr<PeerListRow> row);
void appendSearchRow(std::unique_ptr<PeerListRow> row);
@@ -879,6 +882,7 @@ private:
int _aboveHeight = 0;
int _belowHeight = 0;
bool _hideEmpty = false;
bool _ignoreHiddenRowsOnSearch = false;
object_ptr<Ui::RpWidget> _aboveWidget = { nullptr };
object_ptr<Ui::RpWidget> _aboveSearchWidget = { nullptr };
object_ptr<Ui::RpWidget> _belowWidget = { nullptr };
@@ -1102,6 +1106,9 @@ public:
[[nodiscard]] std::vector<PeerListRowId> collectSelectedIds();
[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();
[[nodiscard]] rpl::producer<int> multiSelectHeightValue() const;
void setSpecialTabMode(bool value);
void peerListSetTitle(rpl::producer<QString> title) override {
setTitle(std::move(title));
@@ -1168,4 +1175,11 @@ private:
bool _scrollBottomFixed = false;
int _addedTopScrollSkip = 0;
struct SpecialTabsMode final {
bool enabled = false;
bool searchIsActive = false;
int topSkip = 0;
};
SpecialTabsMode _specialTabsMode;
};

View File

@@ -79,7 +79,6 @@ void ShowReportMessageBox(
auto performRequest = [=](
const auto &repeatRequest,
Data::ReportInput reportInput) -> void {
constexpr auto kToastDuration = crl::time(4000);
report(reportInput, [=](const Api::ReportResult &result) {
if (!result.error.isEmpty()) {
if (result.error == u"MESSAGE_ID_REQUIRED"_q) {
@@ -199,6 +198,7 @@ void ShowReportMessageBox(
}
}));
} else if (result.successful) {
constexpr auto kToastDuration = crl::time(4000);
show->showToast(
tr::lng_report_thanks(tr::now),
kToastDuration);

View File

@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/storage_account.h"
#include "ui/boxes/confirm_box.h"
#include "apiwrap.h"
#include "ui/widgets/chat_filters_tabs_strip.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/scroll_area.h"
@@ -39,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/share_message_phrase_factory.h"
#include "data/business/data_shortcut_messages.h"
#include "data/data_channel.h"
#include "data/data_chat_filters.h"
#include "data/data_game.h"
#include "data/data_histories.h"
#include "data/data_user.h"
@@ -81,11 +83,14 @@ public:
void activateSkipColumn(int direction);
void activateSkipPage(int pageHeight, int direction);
void updateFilter(QString filter = QString());
[[nodiscard]] bool isFilterEmpty() const;
void selectActive();
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
rpl::producer<> searchRequests() const;
void applyChatFilter(FilterId id);
protected:
void visibleTopBottomUpdated(
int visibleTop,
@@ -166,7 +171,9 @@ private:
int _upon = -1;
int _visibleTop = 0;
std::unique_ptr<Dialogs::IndexedList> _chatsIndexed;
std::unique_ptr<Dialogs::IndexedList> _defaultChatsIndexed;
std::unique_ptr<Dialogs::IndexedList> _customChatsIndexed;
not_null<Dialogs::IndexedList*> _chatsIndexed;
QString _filter;
std::vector<not_null<Dialogs::Row*>> _filtered;
@@ -282,6 +289,10 @@ void ShareBox::prepare() {
_select->setQueryChangedCallback([=](const QString &query) {
applyFilterUpdate(query);
if (_chatsFilters) {
updateScrollSkips();
scrollToY(0);
}
});
_select->setItemRemovedCallback([=](uint64 itemId) {
if (const auto peer = _descriptor.session->data().peerLoaded(PeerId(itemId))) {
@@ -337,10 +348,32 @@ void ShareBox::prepare() {
{ .suggestCustomEmoji = true });
_select->raise();
{
const auto chatsFilters = AddChatFiltersTabsStrip(
this,
_descriptor.session,
[this](FilterId id) {
_inner->applyChatFilter(id);
scrollToY(0);
});
chatsFilters->lower();
chatsFilters->heightValue() | rpl::start_with_next([this](int h) {
updateScrollSkips();
scrollToY(0);
}, lifetime());
_select->heightValue() | rpl::start_with_next([=](int h) {
chatsFilters->moveToLeft(0, h);
}, chatsFilters->lifetime());
_chatsFilters = chatsFilters;
}
}
int ShareBox::getTopScrollSkip() const {
return _select->isHidden() ? 0 : _select->height();
return (_select->isHidden() ? 0 : _select->height())
+ ((_chatsFilters && _inner && _inner->isFilterEmpty())
? _chatsFilters->height()
: 0);
}
int ShareBox::getBottomScrollSkip() const {
@@ -671,9 +704,10 @@ ShareBox::Inner::Inner(
, _descriptor(descriptor)
, _show(std::move(show))
, _st(_descriptor.st ? *_descriptor.st : st::shareBoxList)
, _chatsIndexed(
, _defaultChatsIndexed(
std::make_unique<Dialogs::IndexedList>(
Dialogs::SortMode::Add)) {
Dialogs::SortMode::Add))
, _chatsIndexed(_defaultChatsIndexed.get()) {
_rowsTop = st::shareRowsTop;
_rowHeight = st::shareRowHeight;
setAttribute(Qt::WA_OpaquePaintEvent);
@@ -691,7 +725,7 @@ ShareBox::Inner::Inner(
const auto self = _descriptor.session->user();
const auto selfHistory = self->owner().history(self);
if (_descriptor.filterCallback(selfHistory)) {
_chatsIndexed->addToEnd(selfHistory);
_defaultChatsIndexed->addToEnd(selfHistory);
}
const auto addList = [&](not_null<Dialogs::IndexedList*> list) {
for (const auto &row : list->all()) {
@@ -699,7 +733,7 @@ ShareBox::Inner::Inner(
if (!history->peer->isSelf()
&& (history->asForum()
|| _descriptor.filterCallback(history))) {
_chatsIndexed->addToEnd(history);
_defaultChatsIndexed->addToEnd(history);
}
}
}
@@ -722,7 +756,7 @@ ShareBox::Inner::Inner(
_descriptor.session->changes().realtimeNameUpdates(
) | rpl::start_with_next([=](const Data::NameUpdate &update) {
_chatsIndexed->peerNameChanged(
_defaultChatsIndexed->peerNameChanged(
update.peer,
update.oldFirstLetters);
}, lifetime());
@@ -1331,6 +1365,10 @@ void ShareBox::Inner::updateFilter(QString filter) {
}
}
bool ShareBox::Inner::isFilterEmpty() const {
return _filter.isEmpty();
}
rpl::producer<Ui::ScrollToRequest> ShareBox::Inner::scrollToRequests() const {
return _scrollToRequests.events();
}
@@ -1339,6 +1377,29 @@ rpl::producer<> ShareBox::Inner::searchRequests() const {
return _searchRequests.events();
}
void ShareBox::Inner::applyChatFilter(FilterId id) {
if (!id) {
_chatsIndexed = _defaultChatsIndexed.get();
} else {
_customChatsIndexed = std::make_unique<Dialogs::IndexedList>(
Dialogs::SortMode::Add);
_chatsIndexed = _customChatsIndexed.get();
const auto addList = [&](not_null<Dialogs::IndexedList*> list) {
for (const auto &row : list->all()) {
if (const auto history = row->history()) {
if (_descriptor.filterCallback(history)) {
_customChatsIndexed->addToEnd(history);
}
}
}
};
const auto &data = _descriptor.session->data();
addList(data.chatsFilters().chatsList(id)->indexed());
}
update();
}
void ShareBox::Inner::peopleReceived(
const QString &query,
const QVector<MTPPeer> &my,

View File

@@ -174,6 +174,8 @@ private:
bool _peopleFull = false;
mtpRequestId _peopleRequest = 0;
RpWidget *_chatsFilters = nullptr;
using PeopleCache = QMap<QString, MTPcontacts_Found>;
PeopleCache _peopleCache;

View File

@@ -33,6 +33,10 @@ base::options::toggle TabbedPanelShowOnClick({
const char kOptionTabbedPanelShowOnClick[] = "tabbed-panel-show-on-click";
bool ShowPanelOnClick() {
return TabbedPanelShowOnClick.value();
}
TabbedPanel::TabbedPanel(
QWidget *parent,
not_null<Window::SessionController*> controller,

View File

@@ -25,6 +25,7 @@ namespace ChatHelpers {
class TabbedSelector;
extern const char kOptionTabbedPanelShowOnClick[];
[[nodiscard]] bool ShowPanelOnClick();
struct TabbedPanelDescriptor {
Window::SessionController *regularWindow = nullptr;

View File

@@ -396,7 +396,7 @@ QByteArray Settings::serialize() const {
<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
<< _tonsiteStorageToken
<< qint32(_includeMutedCounterFolders ? 1 : 0)
<< qint32(0) // Old IV zoom
<< qint32(_chatFiltersHorizontal.current() ? 1 : 0)
<< qint32(_skipToastsInFocus ? 1 : 0)
<< qint32(_recordVideoMessages ? 1 : 0)
<< SerializeVideoQuality(_videoQuality)
@@ -528,6 +528,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0;
qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;
quint32 videoQuality = SerializeVideoQuality(_videoQuality);
quint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0;
stream >> themesAccentColors;
if (!stream.atEnd()) {
@@ -838,8 +839,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
stream >> includeMutedCounterFolders;
}
if (!stream.atEnd()) {
qint32 oldIvZoom = 0;
stream >> oldIvZoom;
stream >> chatFiltersHorizontal;
}
if (!stream.atEnd()) {
stream >> skipToastsInFocus;
@@ -1068,6 +1068,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_skipToastsInFocus = (skipToastsInFocus == 1);
_recordVideoMessages = (recordVideoMessages == 1);
_videoQuality = DeserializeVideoQuality(videoQuality);
_chatFiltersHorizontal = (chatFiltersHorizontal == 1);
}
QString Settings::getSoundPath(const QString &key) const {
@@ -1459,6 +1460,7 @@ void Settings::resetOnLastLogout() {
_ivZoom = 100;
_recordVideoMessages = false;
_videoQuality = {};
_chatFiltersHorizontal = false;
_recentEmojiPreload.clear();
_recentEmoji.clear();
@@ -1634,4 +1636,16 @@ void Settings::setVideoQuality(Media::VideoQuality value) {
_videoQuality = value;
}
bool Settings::chatFiltersHorizontal() const {
return _chatFiltersHorizontal.current();
}
rpl::producer<bool> Settings::chatFiltersHorizontalChanges() const {
return _chatFiltersHorizontal.changes();
}
void Settings::setChatFiltersHorizontal(bool value) {
_chatFiltersHorizontal = value;
}
} // namespace Core

View File

@@ -929,6 +929,10 @@ public:
[[nodiscard]] rpl::producer<int> ivZoomValue() const;
void setIvZoom(int value);
[[nodiscard]] bool chatFiltersHorizontal() const;
[[nodiscard]] rpl::producer<bool> chatFiltersHorizontalChanges() const;
void setChatFiltersHorizontal(bool value);
[[nodiscard]] Media::VideoQuality videoQuality() const;
void setVideoQuality(Media::VideoQuality quality);
@@ -1070,6 +1074,7 @@ private:
QByteArray _tonsiteStorageToken;
rpl::variable<int> _ivZoom = 100;
Media::VideoQuality _videoQuality;
rpl::variable<bool> _chatFiltersHorizontal = false;
bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View File

@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Telegram Desktop"_cs;
constexpr auto AppFile = "Telegram"_cs;
constexpr auto AppVersion = 5007002;
constexpr auto AppVersionStr = "5.7.2";
constexpr auto AppBetaVersion = false;
constexpr auto AppVersion = 5007003;
constexpr auto AppVersionStr = "5.7.3";
constexpr auto AppBetaVersion = true;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -74,6 +74,9 @@ void SponsoredMessages::clearOldRequests() {
SponsoredMessages::AppendResult SponsoredMessages::append(
not_null<History*> history) {
if (isTopBarFor(history)) {
return SponsoredMessages::AppendResult::None;
}
const auto it = _data.find(history);
if (it == end(_data)) {
return SponsoredMessages::AppendResult::None;

View File

@@ -0,0 +1,68 @@
/*
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 "data/data_unread_value.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_chat_filters.h"
#include "data/data_folder.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "window/notifications_manager.h"
namespace Data {
namespace {
rpl::producer<Dialogs::UnreadState> MainListUnreadState(
not_null<Dialogs::MainList*> list) {
return rpl::single(rpl::empty) | rpl::then(
list->unreadStateChanges() | rpl::to_empty
) | rpl::map([=] {
return list->unreadState();
});
}
} // namespace
[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState(
not_null<Main::Session*> session,
const Dialogs::UnreadState &state) {
const auto folderId = Data::Folder::kId;
if (const auto folder = session->data().folderLoaded(folderId)) {
return state - folder->chatsList()->unreadState();
}
return state;
}
rpl::producer<Dialogs::UnreadState> UnreadStateValue(
not_null<Main::Session*> session,
FilterId filterId) {
if (filterId > 0) {
const auto filters = &session->data().chatsFilters();
return MainListUnreadState(filters->chatsList(filterId));
}
return MainListUnreadState(
session->data().chatsList()
) | rpl::map([=](const Dialogs::UnreadState &state) {
return MainListMapUnreadState(session, state);
});
}
rpl::producer<bool> IncludeMutedCounterFoldersValue() {
using namespace Window::Notifications;
return rpl::single(rpl::empty_value()) | rpl::then(
Core::App().notifications().settingsChanged(
) | rpl::filter(
rpl::mappers::_1 == ChangeType::IncludeMuted
) | rpl::to_empty
) | rpl::map([] {
return Core::App().settings().includeMutedCounterFolders();
});
}
} // namespace Data

View File

@@ -0,0 +1,30 @@
/*
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
*/
#pragma once
namespace Dialogs {
struct UnreadState;
} // namespace Dialogs
namespace Main {
class Session;
} // namespace Main
namespace Data {
[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState(
not_null<Main::Session*> session,
const Dialogs::UnreadState &state);
[[nodiscard]] rpl::producer<Dialogs::UnreadState> UnreadStateValue(
not_null<Main::Session*> session,
FilterId filterId);
[[nodiscard]] rpl::producer<bool> IncludeMutedCounterFoldersValue();
} // namespace Data

View File

@@ -11,13 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/ui/chat_search_empty.h"
#include "dialogs/ui/chat_search_in.h"
#include "dialogs/ui/dialogs_layout.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_video_userpic.h"
#include "dialogs/dialogs_indexed_list.h"
#include "dialogs/dialogs_widget.h"
#include "dialogs/dialogs_search_from_controllers.h"
#include "dialogs/dialogs_search_tags.h"
#include "history/view/history_view_chat_preview.h"
#include "history/view/history_view_context_menu.h"
#include "history/history.h"
#include "history/history_item.h"
@@ -44,7 +42,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer_values.h"
#include "data/data_histories.h"
#include "data/data_chat_filters.h"
#include "data/data_cloud_file.h"
#include "data/data_changes.h"
#include "data/data_message_reactions.h"
#include "data/data_saved_messages.h"
@@ -68,7 +65,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/loading_element.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/empty_userpic.h"
#include "ui/unread_badge.h"
#include "boxes/filters/edit_filter_box.h"
#include "boxes/peers/edit_forum_topic_box.h"

View File

@@ -16,7 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_inner_widget.h"
#include "dialogs/dialogs_search_from_controllers.h"
#include "dialogs/dialogs_key.h"
#include "dialogs/dialogs_entry.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_top_bar_widget.h"
@@ -26,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_peer_requests_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/chat_filters_tabs_strip.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/fade_wrap.h"
@@ -46,14 +46,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session_settings.h"
#include "api/api_chat_filters.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "core/update_checker.h"
#include "core/shortcuts.h"
#include "boxes/peer_list_box.h"
#include "boxes/peers/edit_participants_box.h"
#include "window/window_adaptive.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/window_slide_animation.h"
@@ -655,6 +651,11 @@ Widget::Widget(
setupMoreChatsBar();
setupDownloadBar();
}
if (session().settings().dialogsFiltersEnabled()
&& Core::App().settings().chatFiltersHorizontal()) {
toggleFiltersMenu(true);
}
}
void Widget::chosenRow(const ChosenRow &row) {
@@ -1227,6 +1228,9 @@ void Widget::updateControlsVisibility(bool fast) {
if (_moreChatsBar) {
_moreChatsBar->show();
}
if (_chatFilters) {
_chatFilters->show();
}
if (_openedFolder || _openedForum) {
_subsectionTopBar->show();
if (_forumTopShadow) {
@@ -1296,6 +1300,41 @@ void Widget::updateHasFocus(not_null<QWidget*> focused) {
}
}
void Widget::toggleFiltersMenu(bool enabled) {
if (!enabled == !_chatFilters) {
return;
} else if (enabled) {
_chatFilters = base::make_unique_q<Ui::RpWidget>(this);
const auto raw = _chatFilters.get();
const auto inner = Ui::AddChatFiltersTabsStrip(
_chatFilters.get(),
&session(),
[this](FilterId id) {
if (controller()->activeChatsFilterCurrent() != id) {
controller()->setActiveChatsFilter(id);
}
},
true);
raw->show();
raw->stackUnder(_scroll);
raw->resizeToWidth(width());
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(raw);
shadow->show();
inner->sizeValue() | rpl::start_with_next([=, this](const QSize &s) {
raw->resize(s);
shadow->setGeometry(
0,
s.height() - shadow->height(),
s.width(),
shadow->height());
updateControlsGeometry();
}, _chatFilters->lifetime());
updateControlsGeometry();
} else {
_chatFilters = nullptr;
}
}
bool Widget::cancelSearchByMouseBack() {
return _searchHasFocus
&& !_searchSuggestionsLocked
@@ -1845,6 +1884,12 @@ void Widget::scrollToDefault(bool verytop) {
this,
QPoint(),
QRect(0, top, wideGeometry.width(), skip));
if (_chatFilters) {
Ui::RenderWidget(
p,
_chatFilters,
QPoint(0, skip - _chatFilters->height()));
}
Ui::RenderWidget(p, _scroll, QPoint(0, skip));
}
if (scrollGeometry != wideGeometry) {
@@ -1860,6 +1905,9 @@ void Widget::startWidthAnimation() {
}
_widthAnimationCache = grabNonNarrowScrollFrame();
_scroll->hide();
if (_chatFilters) {
_chatFilters->hide();
}
updateStoriesVisibility();
}
@@ -1867,6 +1915,9 @@ void Widget::stopWidthAnimation() {
_widthAnimationCache = QPixmap();
if (!_showAnimation) {
_scroll->setVisible(!_suggestions);
if (_chatFilters) {
_chatFilters->setVisible(!_suggestions);
}
}
updateStoriesVisibility();
update();
@@ -1961,6 +2012,9 @@ void Widget::startSlideAnimation(
if (_moreChatsBar) {
_moreChatsBar->hide();
}
if (_chatFilters) {
_chatFilters->hide();
}
if (_forumTopShadow) {
_forumTopShadow->hide();
}
@@ -3085,6 +3139,9 @@ bool Widget::applySearchState(SearchState state) {
const auto tagsChanged = (_searchState.tags != state.tags);
const auto queryChanged = (_searchState.query != state.query);
const auto tabChanged = (_searchState.tab != state.tab);
const auto queryEmptyChanged = queryChanged
? (_searchState.query.isEmpty() != state.query.isEmpty())
: false;
if (forum) {
if (_openedForum == forum) {
@@ -3120,6 +3177,10 @@ bool Widget::applySearchState(SearchState state) {
? peer->owner().history(migrateFrom).get()
: nullptr;
_searchState = state;
if (_chatFilters && queryEmptyChanged) {
_chatFilters->setVisible(_searchState.query.isEmpty());
updateControlsGeometry();
}
_searchWithPostsPreview = computeSearchWithPostsPreview();
if (queryChanged) {
updateLockUnlockVisibility(anim::type::normal);
@@ -3507,6 +3568,9 @@ void Widget::updateControlsGeometry() {
if (_forumRequestsBar) {
_forumRequestsBar->resizeToWidth(barw);
}
if (_chatFilters) {
_chatFilters->resizeToWidth(barw);
}
_updateScrollGeometryCached = [=] {
const auto moreChatsBarTop = expandedStoriesTop
+ ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded);
@@ -3528,8 +3592,15 @@ void Widget::updateControlsGeometry() {
if (_forumReportBar) {
_forumReportBar->bar().move(0, forumReportTop);
}
const auto scrollTop = forumReportTop
const auto chatFiltersTop = forumReportTop
+ (_forumReportBar ? _forumReportBar->bar().height() : 0);
if (_chatFilters) {
_chatFilters->move(0, chatFiltersTop);
}
const auto scrollTop = chatFiltersTop
+ ((_chatFilters && _searchState.query.isEmpty())
? (_chatFilters->height() * (1. - narrowRatio))
: 0);
const auto scrollHeight = height() - scrollTop - bottomSkip;
const auto wasScrollHeight = _scroll->height();
_scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight);

View File

@@ -129,6 +129,7 @@ public:
[[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const;
[[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const;
void updateHasFocus(not_null<QWidget*> focused);
void toggleFiltersMenu(bool value);
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e) override;
@@ -317,6 +318,8 @@ private:
std::unique_ptr<Ui::RequestsBar> _forumRequestsBar;
std::unique_ptr<HistoryView::ContactStatus> _forumReportBar;
base::unique_qptr<Ui::RpWidget> _chatFilters;
object_ptr<Ui::ElasticScroll> _scroll;
QPointer<InnerWidget> _inner;
std::unique_ptr<Suggestions> _suggestions;

View File

@@ -666,7 +666,8 @@ bool InnerWidget::elementUnderCursor(
return (Element::Hovered() == view);
}
HistoryView::SelectionModeResult InnerWidget::elementInSelectionMode() {
HistoryView::SelectionModeResult InnerWidget::elementInSelectionMode(
const HistoryView::Element *) {
return {};
}

View File

@@ -94,7 +94,8 @@ public:
HistoryView::Context elementContext() override;
bool elementUnderCursor(
not_null<const HistoryView::Element*> view) override;
HistoryView::SelectionModeResult elementInSelectionMode() override;
HistoryView::SelectionModeResult elementInSelectionMode(
const HistoryView::Element *view) override;
bool elementIntersectsRange(
not_null<const HistoryView::Element*> view,
int from,

View File

@@ -155,7 +155,11 @@ public:
not_null<const Element*> view) override {
return (Element::Moused() == view);
}
HistoryView::SelectionModeResult elementInSelectionMode() override {
HistoryView::SelectionModeResult elementInSelectionMode(
const Element *view) override {
if (view && view->data()->isSponsored()) {
return HistoryView::SelectionModeResult();
}
return _widget
? _widget->inSelectionMode()
: HistoryView::SelectionModeResult();

View File

@@ -198,7 +198,8 @@ void SetupSwipeHandler(
const auto &touches = t->touchPoints();
const auto released = [&](int index) {
return (touches.size() > index)
&& (touches.at(index).state() & Qt::TouchPointReleased);
&& (int(touches.at(index).state())
& int(Qt::TouchPointReleased));
};
const auto cancel = released(0)
|| released(1)

View File

@@ -421,8 +421,16 @@ HistoryWidget::HistoryWidget(
initTabbedSelector();
_attachToggle->setClickedCallback([=] {
const auto toggle = _attachBotsMenu && _attachBotsMenu->isHidden();
base::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] {
chooseAttach();
if (_attachBotsMenu && toggle) {
_attachBotsMenu->showAnimated();
} else {
chooseAttach();
if (_attachBotsMenu) {
_attachBotsMenu->hideAnimated();
}
}
});
});
@@ -2574,6 +2582,8 @@ void HistoryWidget::showHistory(
}
}));
checkState();
} else {
requestSponsoredMessageBar();
}
} else {
_chooseForReport = nullptr;
@@ -2611,7 +2621,7 @@ void HistoryWidget::setHistory(History *history) {
if (was && !now) {
_attachToggle->removeEventFilter(_attachBotsMenu.get());
_attachBotsMenu->hideFast();
} else if (now && !was) {
} else if (now && !was && !ChatHelpers::ShowPanelOnClick()) {
_attachToggle->installEventFilter(_attachBotsMenu.get());
}
@@ -2699,7 +2709,9 @@ void HistoryWidget::refreshAttachBotsMenu() {
}
_attachBotsMenu->setOrigin(
Ui::PanelAnimation::Origin::BottomLeft);
_attachToggle->installEventFilter(_attachBotsMenu.get());
if (!ChatHelpers::ShowPanelOnClick()) {
_attachToggle->installEventFilter(_attachBotsMenu.get());
}
_attachBotsMenu->heightValue(
) | rpl::start_with_next([=] {
moveFieldControls();
@@ -4521,7 +4533,6 @@ void HistoryWidget::showFinished() {
_showAnimation = nullptr;
doneShow();
synteticScrollToY(_scroll->scrollTop());
requestSponsoredMessageBar();
}
void HistoryWidget::doneShow() {

View File

@@ -111,7 +111,8 @@ bool DefaultElementDelegate::elementUnderCursor(
return false;
}
SelectionModeResult DefaultElementDelegate::elementInSelectionMode() {
SelectionModeResult DefaultElementDelegate::elementInSelectionMode(
const Element *view) {
return {};
}

View File

@@ -80,7 +80,8 @@ class ElementDelegate {
public:
virtual Context elementContext() = 0;
virtual bool elementUnderCursor(not_null<const Element*> view) = 0;
virtual SelectionModeResult elementInSelectionMode() = 0;
virtual SelectionModeResult elementInSelectionMode(
const Element *view) = 0;
virtual bool elementIntersectsRange(
not_null<const Element*> view,
int from,
@@ -136,7 +137,7 @@ public:
class DefaultElementDelegate : public ElementDelegate {
public:
bool elementUnderCursor(not_null<const Element*> view) override;
SelectionModeResult elementInSelectionMode() override;
SelectionModeResult elementInSelectionMode(const Element *view) override;
bool elementIntersectsRange(
not_null<const Element*> view,
int from,

View File

@@ -1782,7 +1782,11 @@ bool ListWidget::elementUnderCursor(
return (_overElement == view);
}
SelectionModeResult ListWidget::elementInSelectionMode() {
SelectionModeResult ListWidget::elementInSelectionMode(
const HistoryView::Element *view) {
if (view && !_delegate->listIsItemGoodForSelection(view->data())) {
return {};
}
return inSelectionMode();
}

View File

@@ -391,7 +391,7 @@ public:
// ElementDelegate interface.
Context elementContext() override;
bool elementUnderCursor(not_null<const Element*> view) override;
SelectionModeResult elementInSelectionMode() override;
SelectionModeResult elementInSelectionMode(const Element *view) override;
bool elementIntersectsRange(
not_null<const Element*> view,
int from,

View File

@@ -1100,7 +1100,7 @@ void Message::draw(Painter &p, const PaintContext &context) const {
if (hasGesture) {
p.translate(context.gestureHorizontal.translation, 0);
}
const auto selectionModeResult = delegate()->elementInSelectionMode();
const auto selectionModeResult = delegate()->elementInSelectionMode(this);
const auto selectionTranslation = (selectionModeResult.progress > 0)
? (selectionModeResult.progress
* AdditionalSpaceForSelectionCheckbox(this, g))
@@ -3868,7 +3868,7 @@ bool Message::displayFastReply() const {
return hasFastReply()
&& data()->isRegular()
&& canSendAnything()
&& !delegate()->elementInSelectionMode().inSelectionMode;
&& !delegate()->elementInSelectionMode(this).inSelectionMode;
}
bool Message::displayRightActionComments() const {
@@ -4032,7 +4032,7 @@ void Message::drawRightAction(
ClickHandlerPtr Message::rightActionLink(
std::optional<QPoint> pressPoint) const {
if (delegate()->elementInSelectionMode().progress > 0) {
if (delegate()->elementInSelectionMode(this).progress > 0) {
return nullptr;
}
ensureRightAction();

View File

@@ -898,7 +898,7 @@ void RepliesWidget::setupSwipeReply() {
}
}, [=, show = controller()->uiShow()](int cursorTop) {
auto result = HistoryView::SwipeHandlerFinishData();
if (_inner->elementInSelectionMode().inSelectionMode) {
if (_inner->elementInSelectionMode(nullptr).inSelectionMode) {
return result;
}
const auto view = _inner->lookupItemByY(cursorTop);

View File

@@ -663,8 +663,8 @@ QSize WebPage::countCurrentSize(int newWidth) {
const auto stickerSet = stickerSetData();
const auto factcheck = factcheckData();
const auto sponsored = sponsoredData();
const auto specialRightPix = ((sponsored && !sponsored->hasMedia)
|| stickerSet);
const auto specialRightPix = (stickerSet
|| (sponsored && !sponsored->hasMedia && _data->photo));
const auto lineHeight = UnitedLineHeight();
const auto factcheckMetrics = factcheck
? computeFactcheckMetrics(_description.countHeight(innerWidth))
@@ -678,7 +678,7 @@ QSize WebPage::countCurrentSize(int newWidth) {
}
const auto linesMax = factcheck
? (factcheckMetrics.lines + 1)
: (specialRightPix || isLogEntryOriginal())
: (sponsored || isLogEntryOriginal())
? kMaxOriginalEntryLines
: 5;
const auto siteNameHeight = _siteNameLines ? lineHeight : 0;
@@ -711,7 +711,9 @@ QSize WebPage::countCurrentSize(int newWidth) {
newHeight += _titleLines * lineHeight;
}
const auto descriptionHeight = _description.countHeight(wleft);
const auto descriptionHeight = _description.countHeight(sponsored
? innerWidth
: wleft);
const auto restLines = (linesMax - _siteNameLines - _titleLines);
if (descriptionHeight < restLines * descriptionLineHeight) {
// We have height for all the lines.
@@ -1510,7 +1512,7 @@ void WebPage::clickHandlerPressedChanged(
}
return;
}
if (p == _openl) {
if ((p == _openl) || (sponsoredData() && sponsoredData()->link == p)) {
if (pressed) {
if (!_ripple) {
const auto full = Rect(currentSize());

View File

@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer_rpl.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/share_box.h"
#include "chat_helpers/tabbed_panel.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "core/local_url_handlers.h"
@@ -2039,9 +2040,6 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
not_null<PeerData*> peer,
Fn<Api::SendAction()> actionFactory,
Fn<void(bool)> attach) {
if (!Data::CanSend(peer, ChatRestriction::SendInline)) {
return nullptr;
}
auto result = std::make_unique<Ui::DropdownMenu>(
parent,
st::dropdownMenuWithIcons);
@@ -2096,8 +2094,10 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
ChooseAndSendLocation(controller, config, actionFactory());
}, &st::menuIconAddress);
}
const auto addBots = Data::CanSend(peer, ChatRestriction::SendInline);
for (const auto &bot : bots->attachBots()) {
if (!bot.inAttachMenu
if (!addBots
|| !bot.inAttachMenu
|| !PeerMatchesTypes(peer, bot.user, bot.types)) {
continue;
}
@@ -2128,7 +2128,11 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
}, action->lifetime());
raw->addAction(std::move(action));
}
if (raw->actions().size() <= minimal) {
const auto actions = raw->actions().size();
const auto onclick = ChatHelpers::ShowPanelOnClick();
if (!actions) {
return nullptr;
} else if (actions <= minimal && !onclick) {
return nullptr;
}
return result;

View File

@@ -1098,6 +1098,12 @@ void MainWidget::dialogsCancelled() {
_history->activate();
}
void MainWidget::toggleFiltersMenu(bool value) const {
if (_dialogs) {
_dialogs->toggleFiltersMenu(value);
}
}
void MainWidget::setChatBackground(
const Data::WallPaper &background,
QImage &&image) {

View File

@@ -213,6 +213,7 @@ public:
void showNonPremiumLimitToast(bool download);
void dialogsCancelled();
void toggleFiltersMenu(bool value) const;
private:
void paintEvent(QPaintEvent *e) override;

View File

@@ -1855,44 +1855,48 @@ void AddWithdrawalWidget(
}, label->lifetime());
const auto lockedColor = anim::with_alpha(stButton.textFg->c, .5);
const auto lockedLabelTop = Ui::CreateChild<Ui::FlatLabel>(
button,
tr::lng_bot_earn_balance_button_locked(),
st::botEarnLockedButtonLabel);
lockedLabelTop->setTextColorOverride(lockedColor);
lockedLabelTop->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto lockedLabelBottom = Ui::CreateChild<Ui::FlatLabel>(
button,
QString(),
st::botEarnLockedButtonLabel);
lockedLabelBottom->setTextColorOverride(lockedColor);
lockedLabelBottom->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto lockedLabel = Ui::CreateChild<Ui::RpWidget>(button);
lockedLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
struct LockedState final {
Ui::Text::String text;
bool locked = false;
bool dateIsNull = false;
rpl::lifetime dateUpdateLifetime;
};
const auto state = lockedLabel->lifetime().make_state<LockedState>();
rpl::combine(
rpl::duplicate(lockedValue),
button->sizeValue(),
lockedLabelTop->sizeValue(),
lockedLabelBottom->sizeValue()
) | rpl::start_with_next([=](
bool locked,
const QSize &b,
const QSize &top,
const QSize &bottom) {
const auto factor = locked ? 1 : -10;
const auto sumHeight = top.height() + bottom.height();
lockedLabelTop->moveToLeft(
(b.width() - top.width()) / 2,
factor * (b.height() - sumHeight) / 2);
lockedLabelBottom->moveToLeft(
(b.width() - bottom.width()) / 2,
factor * ((b.height() - sumHeight) / 2 + top.height()));
}, lockedLabelTop->lifetime());
button->sizeValue()
) | rpl::start_with_next([=](bool locked, const QSize &s) {
state->locked = locked;
lockedLabel->resize(s);
}, lockedLabel->lifetime());
lockedLabel->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(lockedLabel);
p.setPen(state->locked ? QPen(lockedColor) : stButton.textFg->p);
if (state->dateIsNull) {
p.setFont(st::channelEarnSemiboldLabel.style.font);
p.drawText(
lockedLabel->rect(),
style::al_center,
tr::lng_bot_earn_balance_button_locked(tr::now));
return;
}
state->text.draw(p, {
.position = QPoint(
0,
(lockedLabel->height() - state->text.minHeight()) / 2),
.outerWidth = lockedLabel->width(),
.availableWidth = lockedLabel->width(),
.align = style::al_center,
});
}, lockedLabel->lifetime());
const auto dateUpdateLifetime
= lockedLabelBottom->lifetime().make_state<rpl::lifetime>();
std::move(
dateValue
) | rpl::start_with_next([=](const QDateTime &dt) {
dateUpdateLifetime->destroy();
state->dateUpdateLifetime.destroy();
state->dateIsNull = dt.isNull();
if (dt.isNull()) {
return;
}
@@ -1901,7 +1905,7 @@ void AddWithdrawalWidget(
const auto context = Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { lockedLabelBottom->update(); },
.customEmojiRepaint = [=] { lockedLabel->update(); },
};
const auto emoji = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
@@ -1929,11 +1933,18 @@ void AddWithdrawalWidget(
: (u"%1:%2"_q)
.arg(minutes, 2, 10, kZero)
.arg(seconds, 2, 10, kZero);
lockedLabelBottom->setMarkedText(
base::duplicate(emoji).append(formatted),
state->text.setMarkedText(
st::botEarnLockedButtonLabel.style,
TextWithEntities()
.append(tr::lng_bot_earn_balance_button_locked(tr::now))
.append('\n')
.append(emoji)
.append(formatted),
kMarkupTextOptions,
context);
}, *dateUpdateLifetime);
}, lockedLabelBottom->lifetime());
lockedLabel->update();
}, state->dateUpdateLifetime);
}, lockedLabel->lifetime());
Api::HandleWithdrawalButton(
Api::RewardReceiver{

View File

@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
@@ -843,6 +844,34 @@ void SetupTopContent(
}
void SetupView(not_null<Ui::VerticalLayout*> content) {
Ui::AddDivider(content);
Ui::AddSkip(content);
Ui::AddSubsectionTitle(content, tr::lng_filters_view_subtitle());
const auto group = std::make_shared<Ui::RadioenumGroup<bool>>(
Core::App().settings().chatFiltersHorizontal());
const auto addSend = [&](bool value, const QString &text) {
content->add(
object_ptr<Ui::Radioenum<bool>>(
content,
group,
value,
text,
st::settingsSendType),
st::settingsSendTypePadding);
};
addSend(false, tr::lng_filters_vertical(tr::now));
addSend(true, tr::lng_filters_horizontal(tr::now));
group->setChangedCallback([=](bool value) {
Core::App().settings().setChatFiltersHorizontal(value);
Core::App().saveSettingsDelayed();
});
Ui::AddSkip(content);
Ui::AddSkip(content);
}
} // namespace
Folders::Folders(
@@ -871,6 +900,8 @@ void Folders::setupContent(not_null<Window::SessionController*> controller) {
_save = SetupFoldersContent(controller, content);
SetupView(content);
Ui::ResizeFitChild(this, content);
}

View File

@@ -229,6 +229,8 @@ void Uploader::processDocumentProgress(FullMsgId itemId) {
const auto document = media ? media->document() : nullptr;
const auto sendAction = (document && document->isVoiceMessage())
? Api::SendProgressType::UploadVoice
: (document && document->isVideoMessage())
? Api::SendProgressType::UploadRound
: Api::SendProgressType::UploadFile;
const auto progress = (document && document->uploading())
? ((document->uploadingData->offset * 100)
@@ -250,6 +252,8 @@ void Uploader::processDocumentFailed(FullMsgId itemId) {
const auto document = media ? media->document() : nullptr;
const auto sendAction = (document && document->isVoiceMessage())
? Api::SendProgressType::UploadVoice
: (document && document->isVideoMessage())
? Api::SendProgressType::UploadRound
: Api::SendProgressType::UploadFile;
sendProgressUpdate(item, sendAction, -1);
}

View File

@@ -8,11 +8,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/filter_link_header.h"
#include "lang/lang_keys.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "ui/image/image_prepare.h"
#include "ui/ui_utility.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/chat_filters_tabs_slider.h"
#include "ui/widgets/labels.h"
#include "styles/style_chat_helpers.h" // defaultEmojiSuggestions
#include "styles/style_dialogs.h" // dialogsSearchTabs
#include "styles/style_filter_icons.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
@@ -68,6 +72,7 @@ private:
QString _folderTitle;
not_null<const style::icon*> _folderIcon;
bool _horizontalFilters = false;
int _maxHeight = 0;
@@ -75,6 +80,46 @@ private:
};
[[nodiscard]] QImage GeneratePreview(
not_null<Ui::RpWidget*> parent,
const QString &title,
int badge) {
using Tabs = Ui::ChatsFiltersTabs;
auto owned = parent->lifetime().make_state<base::unique_qptr<Tabs>>(
base::make_unique_q<Tabs>(parent, st::dialogsSearchTabs));
const auto raw = owned->get();
raw->setSections({
tr::lng_filters_name_people(tr::now),
title,
tr::lng_filters_name_unread(tr::now),
});
raw->fitWidthToSections();
raw->setActiveSectionFast(1);
raw->stopAnimation();
auto result = QImage(
raw->size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(st::windowBg->c);
{
auto p = QPainter(&result);
Ui::RenderWidget(p, raw, QPoint(), raw->rect());
const auto &r = st::defaultEmojiSuggestions.fadeRight;
const auto &l = st::defaultEmojiSuggestions.fadeLeft;
const auto padding = st::filterLinkSubsectionTitlePadding.top();
const auto w = raw->width();
const auto h = raw->height();
r.fill(p, QRect(w - r.width() - padding, 0, r.width(), h));
l.fill(p, QRect(padding, 0, l.width(), h));
p.fillRect(0, 0, padding, h, st::windowBg);
p.fillRect(w - padding, 0, padding, raw->height(), st::windowBg);
}
owned->reset();
return result;
}
[[nodiscard]] QImage GeneratePreview(
const QString &title,
not_null<const style::icon*> icon,
@@ -199,17 +244,18 @@ Widget::Widget(
, _titleFont(st::boxTitle.style.font)
, _titlePadding(st::filterLinkTitlePadding)
, _folderTitle(descriptor.folderTitle)
, _folderIcon(descriptor.folderIcon) {
, _folderIcon(descriptor.folderIcon)
, _horizontalFilters(descriptor.horizontalFilters) {
setMinimumHeight(st::boxTitleHeight);
refreshTitleText();
setTitlePosition(st::boxTitlePosition.x(), st::boxTitlePosition.y());
style::PaletteChanged(
) | rpl::start_with_next([=] {
) | rpl::start_with_next([this] {
_preview = QImage();
}, lifetime());
_badge.changes() | rpl::start_with_next([=] {
_badge.changes() | rpl::start_with_next([this] {
_preview = QImage();
update();
}, lifetime());
@@ -242,7 +288,9 @@ void Widget::resizeEvent(QResizeEvent *e) {
_about->resizeToWidth(availableWidth);
const auto minHeight = minimumHeight();
const auto maxHeight = st::filterLinkAboutTop
const auto maxHeight = (_horizontalFilters
? (st::filterLinkAboutTop * 0.8)
: st::filterLinkAboutTop)
+ _about->height()
+ st::filterLinkAboutBottom;
if (maxHeight <= minHeight) {
@@ -283,12 +331,22 @@ void Widget::resizeEvent(QResizeEvent *e) {
QRectF Widget::previewRect(
float64 topProgress,
float64 sizeProgress) const {
const auto size = st::filterLinkPreview * sizeProgress;
return QRectF(
(width() - size) / 2.,
st::filterLinkPreviewTop * topProgress,
size,
size);
if (_horizontalFilters) {
const auto size = (_preview.size() / style::DevicePixelRatio())
* sizeProgress;
return QRectF(
(width() - size.width()) / 2.,
st::filterLinkPreviewTop * 1.5 * topProgress,
size.width(),
size.height());
} else {
const auto size = st::filterLinkPreview * sizeProgress;
return QRectF(
(width() - size) / 2.,
st::filterLinkPreviewTop * topProgress,
size,
size);
}
};
void Widget::paintEvent(QPaintEvent *e) {
@@ -298,10 +356,13 @@ void Widget::paintEvent(QPaintEvent *e) {
if (_progress.top) {
auto hq = PainterHighQualityEnabler(p);
if (_preview.isNull()) {
_preview = GeneratePreview(
_folderTitle,
_folderIcon,
_badge.current());
const auto badge = _badge.current();
if (_horizontalFilters) {
_preview = GeneratePreview(this, _folderTitle, badge);
Widget::resizeEvent(nullptr);
} else {
_preview = GeneratePreview(_folderTitle, _folderIcon, badge);
}
}
p.drawImage(_previewRect, _preview);
}

View File

@@ -29,6 +29,7 @@ struct FilterLinkHeaderDescriptor {
base::required<QString> folderTitle;
not_null<const style::icon*> folderIcon;
rpl::producer<int> badge;
bool horizontalFilters = false;
};
struct FilterLinkHeader {

View File

@@ -107,9 +107,15 @@ private:
std::array<int64, kMaxStreams> lastDts = { 0 };
};
#if DA_FFMPEG_CONST_WRITE_CALLBACK
static int Write(void *opaque, const uint8_t *_buf, int buf_size) {
uint8_t *buf = const_cast<uint8_t *>(_buf);
#else
static int Write(void *opaque, uint8_t *buf, int buf_size) {
#endif
return static_cast<Private*>(opaque)->write(buf, buf_size);
}
static int64_t Seek(void *opaque, int64_t offset, int whence) {
return static_cast<Private*>(opaque)->seek(offset, whence);
}
@@ -388,7 +394,6 @@ bool RoundVideoRecorder::Private::initAudio() {
_audioCodec->sample_rate = kAudioFrequency;
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
_audioCodec->ch_layout = AV_CHANNEL_LAYOUT_MONO;
_audioCodec->channels = _audioCodec->ch_layout.nb_channels;
#else
_audioCodec->channel_layout = AV_CH_LAYOUT_MONO;
_audioCodec->channels = _audioChannels;

View File

@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/ripple_animation.h"
#include "ui/chat/group_call_userpics.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"

View File

@@ -8,8 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/unique_qptr.h"
#include "ui/text/text_block.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/text/text_custom_emoji.h"
namespace Ui {

View File

@@ -0,0 +1,418 @@
/*
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 "ui/widgets/chat_filters_tabs_slider.h"
#include "ui/effects/ripple_animation.h"
#include "ui/widgets/side_bar_button.h"
#include "styles/style_widgets.h"
#include <QScrollBar>
namespace Ui {
ChatsFiltersTabs::ChatsFiltersTabs(
not_null<Ui::RpWidget*> parent,
const style::SettingsSlider &st)
: Ui::SettingsSlider(parent, st)
, _st(st)
, _unreadSt([&] {
auto st = Ui::UnreadBadgeStyle();
st.align = style::al_left;
return st;
}())
, _unreadMaxString(u"99+"_q)
, _unreadSkip(st::lineWidth * 5) {
Expects(_st.barSnapToLabel && _st.strictSkip);
if (_st.barRadius > 0) {
_bar.emplace(_st.barRadius, _st.barFg);
_barActive.emplace(_st.barRadius, _st.barFgActive);
}
{
const auto one = Ui::CountUnreadBadgeSize(u"9"_q, _unreadSt, 1);
_cachedBadgeWidths = {
one.width(),
Ui::CountUnreadBadgeSize(u"99"_q, _unreadSt, 2).width(),
Ui::CountUnreadBadgeSize(u"999"_q, _unreadSt, 2).width(),
};
_cachedBadgeHeight = one.height();
}
Ui::DiscreteSlider::setSelectOnPress(false);
}
int ChatsFiltersTabs::centerOfSection(int section) const {
const auto widths = countSectionsWidths(0);
auto result = 0;
if (section >= 0 && section < widths.size()) {
for (auto i = 0; i < section; i++) {
result += widths[i];
}
result += widths[section] / 2;
}
return result;
}
void ChatsFiltersTabs::fitWidthToSections() {
const auto widths = countSectionsWidths(0);
resizeToWidth(ranges::accumulate(widths, .0));
_lockedFromX = calculateLockedFromX();
{
_sections.clear();
enumerateSections([&](Section &section) {
_sections.push_back({ not_null{ &section }, 0, false });
return true;
});
}
}
void ChatsFiltersTabs::setUnreadCount(int index, int unreadCount) {
const auto it = _unreadCounts.find(index);
if (it == _unreadCounts.end()) {
if (unreadCount) {
_unreadCounts.emplace(index, Unread{
.cache = cacheUnreadCount(unreadCount),
.count = unreadCount,
});
}
} else {
if (unreadCount) {
it->second.count = unreadCount;
it->second.cache = cacheUnreadCount(unreadCount);
} else {
_unreadCounts.erase(it);
}
}
if (unreadCount) {
const auto widthIndex = (unreadCount < 10)
? 0
: (unreadCount < 100)
? 1
: 2;
setAdditionalContentWidthToSection(
index,
_cachedBadgeWidths[widthIndex] + _unreadSkip);
} else {
setAdditionalContentWidthToSection(index, 0);
}
}
int ChatsFiltersTabs::calculateLockedFromX() const {
if (!_lockedFrom) {
return std::numeric_limits<int>::max();
}
auto left = 0;
auto index = 0;
enumerateSections([&](const Section &section) {
const auto currentRight = section.left + section.width;
if (index == _lockedFrom) {
return false;
}
left = currentRight;
index++;
return true;
});
return left ? left : std::numeric_limits<int>::max();
}
void ChatsFiltersTabs::setLockedFrom(int index) {
_lockedFrom = index;
_lockedFromX = calculateLockedFromX();
if (!index) {
_paletteLifetime.destroy();
return;
}
_paletteLifetime = style::PaletteChanged(
) | rpl::start_with_next([this] {
_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));
});
}
QImage ChatsFiltersTabs::cacheUnreadCount(int count) const {
const auto widthIndex = (count < 10) ? 0 : (count < 100) ? 1 : 2;
auto image = QImage(
QSize(_cachedBadgeWidths[widthIndex], _cachedBadgeHeight)
* style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
const auto string = (count > 99)
? _unreadMaxString
: QString::number(count);
{
auto p = QPainter(&image);
Ui::PaintUnreadBadge(p, string, 0, 0, _unreadSt, 0);
}
return image;
}
void ChatsFiltersTabs::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto clip = e->rect();
const auto range = getCurrentActiveRange();
const auto activeIndex = activeSection();
auto index = 0;
auto raisedIndex = -1;
auto activeHorizontalShift = 0;
const auto drawSection = [&](Section &section) {
// const auto activeWidth = _st.barSnapToLabel
// ? section.contentWidth
// : section.width;
const auto horizontalShift = _sections[index].horizontalShift;
const auto shiftedLeft = section.left + horizontalShift;
if (_sections[index].raise) {
raisedIndex = index;
}
if (index == activeIndex) {
activeHorizontalShift = horizontalShift;
}
// const auto activeLeft = shiftedLeft
// + (section.width - activeWidth) / 2;
// const auto active = 1.
// - std::clamp(
// std::abs(range.left - activeLeft) / float64(range.width),
// 0.,
// 1.);
const auto active = (index == activeIndex) ? 1. : 0.;
if (section.ripple) {
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
section.ripple->paint(p, shiftedLeft, 0, width(), &color);
if (section.ripple->empty()) {
section.ripple.reset();
}
}
const auto labelLeft = shiftedLeft
+ (section.width - section.contentWidth) / 2;
const auto rect = myrtlrect(
labelLeft,
_st.labelTop,
section.contentWidth,
_st.labelStyle.font->height);
if (rect.intersects(clip)) {
const auto locked = (_lockedFrom && (index >= _lockedFrom));
if (locked) {
constexpr auto kPremiumLockedOpacity = 0.6;
p.setOpacity(kPremiumLockedOpacity);
}
p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
section.label.draw(p, {
.position = QPoint(labelLeft, _st.labelTop),
.outerWidth = width(),
.availableWidth = section.label.maxWidth(),
});
{
const auto it = _unreadCounts.find(index);
if (it != _unreadCounts.end()) {
p.drawImage(
labelLeft
+ _unreadSkip
+ section.label.maxWidth(),
_st.labelTop,
it->second.cache);
}
}
if (locked) {
if (!_lockCache) {
_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));
}
const auto size = _lockCache->size()
/ style::DevicePixelRatio();
p.drawImage(
labelLeft + (section.label.maxWidth() - size.width()) / 2,
height() - size.height() - st::lineWidth,
*_lockCache);
p.setOpacity(1.0);
}
}
index++;
return true;
};
enumerateSections(drawSection);
if (raisedIndex >= 0) {
index = raisedIndex;
drawSection(*_sections[raisedIndex].section);
}
if (_st.barSnapToLabel) {
const auto drawRect = [&](QRect rect, bool active) {
const auto &bar = active ? _barActive : _bar;
if (bar) {
bar->paint(p, rect);
} else {
p.fillRect(rect, active ? _st.barFgActive : _st.barFg);
}
};
const auto add = _st.barStroke / 2;
const auto from = std::max(range.left - add, 0);
const auto till = std::min(range.left + range.width + add, width());
if (from < till) {
drawRect(
myrtlrect(
from,
_st.barTop,
till - from,
_st.barStroke).translated(activeHorizontalShift, 0),
true);
}
}
}
void ChatsFiltersTabs::mousePressEvent(QMouseEvent *e) {
const auto mouseButton = e->button();
if (mouseButton == Qt::MouseButton::LeftButton) {
_lockedPressed = (e->pos().x() >= _lockedFromX);
if (_lockedPressed) {
Ui::RpWidget::mousePressEvent(e);
} else {
Ui::SettingsSlider::mousePressEvent(e);
}
} else {
Ui::RpWidget::mousePressEvent(e);
}
}
void ChatsFiltersTabs::mouseMoveEvent(QMouseEvent *e) {
if (_reordering) {
Ui::RpWidget::mouseMoveEvent(e);
} else {
Ui::SettingsSlider::mouseMoveEvent(e);
}
}
void ChatsFiltersTabs::mouseReleaseEvent(QMouseEvent *e) {
const auto mouseButton = e->button();
if (mouseButton == Qt::MouseButton::LeftButton) {
if (base::take(_lockedPressed)) {
_lockedPressed = false;
_lockedClicked.fire({});
} else {
if (_reordering) {
for (const auto &section : _sections) {
if (section.section->ripple) {
section.section->ripple->lastStop();
}
}
} else {
Ui::SettingsSlider::mouseReleaseEvent(e);
}
}
} else {
Ui::RpWidget::mouseReleaseEvent(e);
}
}
void ChatsFiltersTabs::contextMenuEvent(QContextMenuEvent *e) {
const auto pos = e->pos();
if (pos.x() >= _lockedFromX) {
return;
}
auto left = 0;
auto index = 0;
enumerateSections([&](const Section &section) {
const auto currentRight = section.left + section.width;
if (pos.x() > left && pos.x() < currentRight) {
return false;
}
left = currentRight;
index++;
return true;
});
_contextMenuRequested.fire_copy(index);
}
rpl::producer<int> ChatsFiltersTabs::contextMenuRequested() const {
return _contextMenuRequested.events();
}
rpl::producer<> ChatsFiltersTabs::lockedClicked() const {
return _lockedClicked.events();
}
int ChatsFiltersTabs::count() const {
return _sections.size();
}
void ChatsFiltersTabs::setHorizontalShift(int index, int shift) {
Expects(index >= 0 && index < _sections.size());
auto &section = _sections[index];
if (const auto delta = shift - section.horizontalShift) {
section.horizontalShift = shift;
update();
}
}
void ChatsFiltersTabs::setRaised(int index) {
_sections[index].raise = true;
update();
}
void ChatsFiltersTabs::reorderSections(int oldIndex, int newIndex) {
Expects(oldIndex >= 0 && oldIndex < _sections.size());
Expects(newIndex >= 0 && newIndex < _sections.size());
// Expects(!_inResize);
auto lefts = std::vector<int>();
enumerateSections([&](Section &section) {
lefts.emplace_back(section.left);
return true;
});
const auto wasActive = activeSection();
{
auto unreadCounts = base::flat_map<Index, Unread>();
for (auto &[index, unread] : _unreadCounts) {
unreadCounts.emplace(
base::reorder_index(index, oldIndex, newIndex),
std::move(unread));
}
_unreadCounts = std::move(unreadCounts);
}
base::reorder(sectionsRef(), oldIndex, newIndex);
Ui::DiscreteSlider::setActiveSectionFast(
base::reorder_index(wasActive, oldIndex, newIndex));
Ui::DiscreteSlider::stopAnimation();
{
_sections.clear();
auto left = 0;
enumerateSections([&](Section &section) {
_sections.push_back({ not_null{ &section }, 0, false });
section.left = left;
left += section.width;
return true;
});
}
update();
}
not_null<Ui::DiscreteSlider::Section*> ChatsFiltersTabs::widgetAt(
int index) const {
Expects(index >= 0 && index < count());
return _sections[index].section;
}
void ChatsFiltersTabs::setReordering(int value) {
_reordering = value;
}
int ChatsFiltersTabs::reordering() const {
return _reordering;
}
void ChatsFiltersTabs::stopAnimation() {
Ui::DiscreteSlider::stopAnimation();
}
} // namespace Ui

View File

@@ -0,0 +1,95 @@
/*
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
*/
#pragma once
#include "ui/unread_badge_paint.h"
#include "ui/widgets/discrete_sliders.h"
namespace style {
struct SettingsSlider;
} // namespace style
namespace Ui {
class RpWidget;
class SettingsSlider;
class ChatsFiltersTabsReorder;
class ChatsFiltersTabs final : public Ui::SettingsSlider {
public:
ChatsFiltersTabs(
not_null<Ui::RpWidget*> parent,
const style::SettingsSlider &st);
[[nodiscard]] int centerOfSection(int section) const;
void fitWidthToSections();
void setUnreadCount(int index, int unreadCount);
void setLockedFrom(int index);
[[nodiscard]] rpl::producer<int> contextMenuRequested() const;
[[nodiscard]] rpl::producer<> lockedClicked() const;
void setHorizontalShift(int index, int shift);
void setRaised(int index);
[[nodiscard]] int count() const;
void reorderSections(int oldIndex, int newIndex);
[[nodiscard]] not_null<DiscreteSlider::Section*> widgetAt(int i) const;
void setReordering(int value);
[[nodiscard]] int reordering() const;
void stopAnimation();
protected:
struct ShiftedSection {
not_null<Ui::DiscreteSlider::Section*> section;
int horizontalShift = 0;
bool raise = false;
};
friend class ChatsFiltersTabsReorder;
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
std::vector<ShiftedSection> _sections;
private:
[[nodiscard]] QImage cacheUnreadCount(int count) const;
[[nodiscard]] int calculateLockedFromX() const;
using Index = int;
struct Unread final {
QImage cache;
int count = 0;
};
base::flat_map<Index, Unread> _unreadCounts;
const style::SettingsSlider &_st;
const UnreadBadgeStyle _unreadSt;
const QString _unreadMaxString;
const int _unreadSkip;
std::vector<int> _cachedBadgeWidths;
int _cachedBadgeHeight = 0;
int _lockedFrom = 0;
int _lockedFromX = 0;
bool _lockedPressed = false;
std::optional<Ui::RoundRect> _bar;
std::optional<Ui::RoundRect> _barActive;
std::optional<QImage> _lockCache;
int _reordering = 0;
rpl::lifetime _paletteLifetime;
rpl::event_stream<int> _contextMenuRequested;
rpl::event_stream<> _lockedClicked;
};
} // namespace Ui

View File

@@ -0,0 +1,373 @@
/*
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 "ui/widgets/chat_filters_tabs_slider_reorder.h"
#include "ui/widgets/scroll_area.h"
#include "styles/style_basic.h"
#include <QScrollBar>
#include <QtGui/QtEvents>
#include <QtWidgets/QApplication>
namespace Ui {
namespace {
constexpr auto kScrollFactor = 0.05;
} // namespace
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout,
not_null<ScrollArea*> scroll)
: _layout(layout)
, _scroll(scroll)
, _scrollAnimation([this] { updateScrollCallback(); }) {
}
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout)
: _layout(layout) {
}
void ChatsFiltersTabsReorder::cancel() {
if (_currentWidget) {
cancelCurrent(indexOf(_currentWidget));
}
_lifetime.destroy();
for (auto i = 0, count = _layout->count(); i != count; ++i) {
_layout->setHorizontalShift(i, 0);
}
_entries.clear();
}
void ChatsFiltersTabsReorder::start() {
const auto count = _layout->count();
if (count < 2) {
return;
}
_layout->events()
| rpl::start_with_next_done([this](not_null<QEvent*> e) {
switch (e->type()) {
case QEvent::MouseMove:
mouseMove(static_cast<QMouseEvent*>(e.get())->globalPos());
break;
case QEvent::MouseButtonPress: {
const auto m = static_cast<QMouseEvent*>(e.get());
mousePress(m->button(), m->pos(), m->globalPos());
break;
}
case QEvent::MouseButtonRelease:
mouseRelease(static_cast<QMouseEvent*>(e.get())->button());
break;
}
}, [this] {
cancel();
}, _lifetime);
for (auto i = 0; i != count; ++i) {
const auto widget = _layout->widgetAt(i);
_entries.push_back({ widget });
}
}
void ChatsFiltersTabsReorder::addPinnedInterval(int from, int length) {
_pinnedIntervals.push_back({ from, length });
}
void ChatsFiltersTabsReorder::clearPinnedIntervals() {
_pinnedIntervals.clear();
}
bool ChatsFiltersTabsReorder::Interval::isIn(int index) const {
return (index >= from) && (index < (from + length));
}
bool ChatsFiltersTabsReorder::isIndexPinned(int index) const {
return ranges::any_of(_pinnedIntervals, [&](const Interval &i) {
return i.isIn(index);
});
}
void ChatsFiltersTabsReorder::checkForStart(QPoint position) {
const auto shift = position.x() - _currentStart;
const auto delta = QApplication::startDragDistance();
if (std::abs(shift) <= delta) {
return;
}
_currentState = State::Started;
_currentStart += (shift > 0) ? delta : -delta;
const auto index = indexOf(_currentWidget);
_layout->setRaised(index);
_currentDesiredIndex = index;
_updates.fire({ _currentWidget, index, index, _currentState });
updateOrder(index, position);
}
void ChatsFiltersTabsReorder::updateOrder(int index, QPoint position) {
if (isIndexPinned(index)) {
return;
}
const auto shift = position.x() - _currentStart;
auto &current = _entries[index];
current.shiftAnimation.stop();
current.shift = current.finalShift = shift;
_layout->setHorizontalShift(index, shift);
checkForScrollAnimation();
const auto count = _entries.size();
const auto currentWidth = current.widget->width;
const auto currentMiddle = current.widget->left
+ shift
+ currentWidth / 2;
_currentDesiredIndex = index;
if (shift > 0) {
for (auto next = index + 1; next != count; ++next) {
if (isIndexPinned(next)) {
return;
}
const auto &e = _entries[next];
if (currentMiddle < e.widget->left + e.widget->width / 2) {
moveToShift(next, 0);
} else {
_currentDesiredIndex = next;
moveToShift(next, -currentWidth);
}
}
for (auto prev = index - 1; prev >= 0; --prev) {
moveToShift(prev, 0);
}
} else {
for (auto next = index + 1; next != count; ++next) {
moveToShift(next, 0);
}
for (auto prev = index - 1; prev >= 0; --prev) {
if (isIndexPinned(prev)) {
return;
}
const auto &e = _entries[prev];
if (currentMiddle >= e.widget->left + e.widget->width / 2) {
moveToShift(prev, 0);
} else {
_currentDesiredIndex = prev;
moveToShift(prev, currentWidth);
}
}
}
}
void ChatsFiltersTabsReorder::mousePress(
Qt::MouseButton button,
QPoint position,
QPoint globalPosition) {
if (button != Qt::LeftButton) {
return;
}
auto widget = (ChatsFiltersTabs::ShiftedSection*)(nullptr);
for (auto i = 0; i != _layout->_sections.size(); ++i) {
auto &section = _layout->_sections[i];
if ((position.x() >= section.section->left)
&& (position.x() < (section.section->left + section.section->width))) {
widget = &section;
break;
}
}
cancelCurrent();
_currentWidget = widget->section;
_currentShiftedWidget = widget;
_currentStart = globalPosition.x();
}
void ChatsFiltersTabsReorder::mouseMove(QPoint position) {
if (!_currentWidget) {
// if (_currentWidget != widget) {
return;
} else if (_currentState != State::Started) {
checkForStart(position);
} else {
updateOrder(indexOf(_currentWidget), position);
}
}
void ChatsFiltersTabsReorder::mouseRelease(Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return;
}
finishReordering();
}
void ChatsFiltersTabsReorder::cancelCurrent() {
if (_currentWidget) {
cancelCurrent(indexOf(_currentWidget));
}
}
void ChatsFiltersTabsReorder::cancelCurrent(int index) {
Expects(_currentWidget != nullptr);
if (_currentState == State::Started) {
_currentState = State::Cancelled;
_updates.fire({ _currentWidget, index, index, _currentState });
}
_currentWidget = nullptr;
_currentShiftedWidget = nullptr;
for (auto i = 0, count = int(_entries.size()); i != count; ++i) {
moveToShift(i, 0);
}
}
void ChatsFiltersTabsReorder::finishReordering() {
if (_scroll) {
_scrollAnimation.stop();
}
finishCurrent();
}
void ChatsFiltersTabsReorder::finishCurrent() {
if (!_currentWidget) {
return;
}
const auto index = indexOf(_currentWidget);
if (_currentDesiredIndex == index || _currentState != State::Started) {
cancelCurrent(index);
return;
}
const auto result = _currentDesiredIndex;
const auto widget = _currentWidget;
_currentState = State::Cancelled;
_currentWidget = nullptr;
_currentShiftedWidget = nullptr;
auto &current = _entries[index];
const auto width = current.widget->width;
if (index < result) {
auto sum = 0;
for (auto i = index; i != result; ++i) {
auto &entry = _entries[i + 1];
const auto widget = entry.widget;
entry.deltaShift += width;
updateShift(widget, i + 1);
sum += widget->width;
}
current.finalShift -= sum;
} else if (index > result) {
auto sum = 0;
for (auto i = result; i != index; ++i) {
auto &entry = _entries[i];
const auto widget = entry.widget;
entry.deltaShift -= width;
updateShift(widget, i);
sum += widget->width;
}
current.finalShift += sum;
}
if (!(current.finalShift + current.deltaShift)) {
current.shift = 0;
_layout->setHorizontalShift(index, 0);
}
base::reorder(_entries, index, result);
_layout->reorderSections(index, _currentDesiredIndex);
for (auto i = 0; i != _layout->sectionsRef().size(); ++i) {
_entries[i].widget = &_layout->sectionsRef()[i];
moveToShift(i, 0);
}
_updates.fire({ widget, index, result, State::Applied });
}
void ChatsFiltersTabsReorder::moveToShift(int index, int shift) {
auto &entry = _entries[index];
if (entry.finalShift + entry.deltaShift == shift) {
return;
}
const auto widget = entry.widget;
entry.shiftAnimation.start(
[=, this] { updateShift(widget, index); },
entry.finalShift,
shift - entry.deltaShift,
st::slideWrapDuration);
entry.finalShift = shift - entry.deltaShift;
}
void ChatsFiltersTabsReorder::updateShift(
not_null<Section*> widget,
int indexHint) {
Expects(indexHint >= 0 && indexHint < _entries.size());
const auto index = (_entries[indexHint].widget == widget)
? indexHint
: indexOf(widget);
auto &entry = _entries[index];
entry.shift = base::SafeRound(
entry.shiftAnimation.value(entry.finalShift)
) + entry.deltaShift;
if (entry.deltaShift && !entry.shiftAnimation.animating()) {
entry.finalShift += entry.deltaShift;
entry.deltaShift = 0;
}
_layout->setHorizontalShift(index, entry.shift);
}
int ChatsFiltersTabsReorder::indexOf(not_null<Section*> widget) const {
const auto i = ranges::find(_entries, widget, &Entry::widget);
Assert(i != end(_entries));
return i - begin(_entries);
}
auto ChatsFiltersTabsReorder::updates() const -> rpl::producer<Single> {
return _updates.events();
}
void ChatsFiltersTabsReorder::updateScrollCallback() {
if (!_scroll) {
return;
}
const auto delta = deltaFromEdge();
const auto oldLeft = _scroll->scrollLeft();
_scroll->horizontalScrollBar()->setValue(oldLeft + delta);
const auto newLeft = _scroll->scrollLeft();
_currentStart += oldLeft - newLeft;
if (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) {
_scrollAnimation.stop();
}
}
void ChatsFiltersTabsReorder::checkForScrollAnimation() {
if (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) {
return;
}
_scrollAnimation.start();
}
int ChatsFiltersTabsReorder::deltaFromEdge() {
Expects(_currentWidget != nullptr);
Expects(_currentShiftedWidget != nullptr);
Expects(_scroll);
const auto globalPosition = _layout->mapToGlobal(
QPoint(
_currentWidget->left + _currentShiftedWidget->horizontalShift,
0));
const auto localLeft = _scroll->mapFromGlobal(globalPosition).x();
const auto localRight = localLeft
+ _currentWidget->width
- _scroll->width();
const auto isLeftEdge = (localLeft < 0);
const auto isRightEdge = (localRight > 0);
if (!isLeftEdge && !isRightEdge) {
_scrollAnimation.stop();
return 0;
}
return int((isRightEdge ? localRight : localLeft) * kScrollFactor);
}
} // namespace Ui

View File

@@ -0,0 +1,98 @@
/*
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
*/
#pragma once
#include "ui/effects/animations.h"
#include "ui/widgets/chat_filters_tabs_slider.h"
namespace Ui {
class ScrollArea;
class ChatsFiltersTabsReorder final {
public:
using Section = ChatsFiltersTabs::Section;
enum class State : uchar {
Started,
Applied,
Cancelled,
};
struct Single {
not_null<Section*> widget;
int oldPosition = 0;
int newPosition = 0;
State state = State::Started;
};
ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout,
not_null<ScrollArea*> scroll);
ChatsFiltersTabsReorder(not_null<ChatsFiltersTabs*> layout);
void start();
void cancel();
void finishReordering();
void addPinnedInterval(int from, int length);
void clearPinnedIntervals();
[[nodiscard]] rpl::producer<Single> updates() const;
private:
struct Entry {
not_null<Section*> widget;
Ui::Animations::Simple shiftAnimation;
int shift = 0;
int finalShift = 0;
int deltaShift = 0;
};
struct Interval {
[[nodiscard]] bool isIn(int index) const;
int from = 0;
int length = 0;
};
void mousePress(Qt::MouseButton button, QPoint position, QPoint global);
void mouseMove(QPoint position);
void mouseRelease(Qt::MouseButton button);
void checkForStart(QPoint position);
void updateOrder(int index, QPoint position);
void cancelCurrent();
void finishCurrent();
void cancelCurrent(int index);
[[nodiscard]] int indexOf(not_null<Section*> widget) const;
void moveToShift(int index, int shift);
void updateShift(not_null<Section*> widget, int indexHint);
void updateScrollCallback();
void checkForScrollAnimation();
[[nodiscard]] int deltaFromEdge();
[[nodiscard]] bool isIndexPinned(int index) const;
const not_null<ChatsFiltersTabs*> _layout;
Ui::ScrollArea *_scroll = nullptr;
Ui::Animations::Basic _scrollAnimation;
std::vector<Interval> _pinnedIntervals;
Section *_currentWidget = nullptr;
ChatsFiltersTabs::ShiftedSection *_currentShiftedWidget = nullptr;
int _currentStart = 0;
int _currentDesiredIndex = 0;
State _currentState = State::Cancelled;
std::vector<Entry> _entries;
rpl::event_stream<Single> _updates;
rpl::lifetime _lifetime;
};
} // namespace Ui

View File

@@ -0,0 +1,433 @@
/*
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 "ui/widgets/chat_filters_tabs_strip.h"
#include "api/api_chat_filters_remove_manager.h"
#include "boxes/filters/edit_filter_box.h"
#include "boxes/premium_limits_box.h"
#include "core/application.h"
#include "data/data_chat_filters.h"
#include "data/data_premium_limits.h"
#include "data/data_session.h"
#include "data/data_unread_value.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_folders.h"
#include "ui/ui_utility.h"
#include "ui/widgets/chat_filters_tabs_slider_reorder.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_controller.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_dialogs.h" // dialogsSearchTabs
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
#include "styles/style_menu_icons.h"
#include <QScrollBar>
namespace Ui {
namespace {
struct State final {
Ui::Animations::Simple animation;
std::optional<FilterId> lastFilterId = std::nullopt;
rpl::lifetime unreadLifetime;
base::unique_qptr<Ui::PopupMenu> menu;
Api::RemoveComplexChatFilter removeApi;
bool waitingSuggested = false;
std::unique_ptr<Ui::ChatsFiltersTabsReorder> reorder;
bool ignoreRefresh = false;
};
void ShowMenu(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
not_null<State*> state,
int index) {
const auto session = &controller->session();
auto id = FilterId(0);
{
const auto &list = session->data().chatsFilters().list();
if (index < 0 || index >= list.size()) {
return;
}
id = list[index].id();
}
state->menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto addAction = Ui::Menu::CreateAddActionCallback(
state->menu.get());
if (id) {
addAction(
tr::lng_filters_context_edit(tr::now),
[=] { EditExistingFilter(controller, id); },
&st::menuIconEdit);
Window::MenuAddMarkAsReadChatListAction(
controller,
[=] { return session->data().chatsFilters().chatsList(id); },
addAction);
auto showRemoveBox = [=] {
state->removeApi.request(Ui::MakeWeak(parent), controller, id);
};
addAction(
tr::lng_filters_context_remove(tr::now),
std::move(showRemoveBox),
&st::menuIconDelete);
} else {
auto customUnreadState = [=] {
return Data::MainListMapUnreadState(
session,
session->data().chatsList()->unreadState());
};
Window::MenuAddMarkAsReadChatListAction(
controller,
[=] { return session->data().chatsList(); },
addAction,
std::move(customUnreadState));
auto openFiltersSettings = [=] {
const auto filters = &session->data().chatsFilters();
if (filters->suggestedLoaded()) {
controller->showSettings(Settings::Folders::Id());
} else if (!state->waitingSuggested) {
state->waitingSuggested = true;
filters->requestSuggested();
filters->suggestedUpdated(
) | rpl::take(1) | rpl::start_with_next([=] {
controller->showSettings(Settings::Folders::Id());
}, parent->lifetime());
}
};
addAction(
tr::lng_filters_setup_menu(tr::now),
std::move(openFiltersSettings),
&st::menuIconEdit);
}
if (state->menu->empty()) {
state->menu = nullptr;
return;
}
state->menu->popup(QCursor::pos());
}
void ShowFiltersListMenu(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
not_null<State*> state,
int active,
Fn<void(int)> changeActive) {
const auto &list = session->data().chatsFilters().list();
state->menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto reorderAll = session->user()->isPremium();
const auto maxLimit = (reorderAll ? 1 : 0)
+ Data::PremiumLimits(session).dialogFiltersCurrent();
const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;
for (auto i = 0; i < list.size(); ++i) {
const auto &filter = list[i];
auto text = filter.title().isEmpty()
? tr::lng_filters_all(tr::now)
: filter.title();
const auto action = state->menu->addAction(std::move(text), [=] {
if (i != active) {
changeActive(i);
}
}, (i == active) ? &st::mediaPlayerMenuCheck : nullptr);
action->setEnabled(i < premiumFrom);
}
session->data().chatsFilters().changed() | rpl::start_with_next([=] {
state->menu->hideMenu();
}, state->menu->lifetime());
if (state->menu->empty()) {
state->menu = nullptr;
return;
}
state->menu->popup(QCursor::pos());
}
} // namespace
not_null<Ui::RpWidget*> AddChatFiltersTabsStrip(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
Fn<void(FilterId)> choose,
bool trackActiveFilterAndUnreadAndReorder) {
const auto window = Core::App().findWindow(parent);
const auto controller = window ? window->sessionController() : nullptr;
const auto &scrollSt = st::defaultScrollArea;
const auto wrap = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(
parent,
object_ptr<Ui::RpWidget>(parent));
if (!controller) {
return wrap;
}
const auto container = wrap->entity();
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(container, scrollSt);
const auto sliderPadding = st::dialogsSearchTabsPadding;
const auto slider = scroll->setOwnedWidget(
object_ptr<Ui::PaddingWrap<Ui::ChatsFiltersTabs>>(
parent,
object_ptr<Ui::ChatsFiltersTabs>(parent, st::dialogsSearchTabs),
QMargins(sliderPadding, 0, sliderPadding, 0)))->entity();
const auto state = wrap->lifetime().make_state<State>();
if (trackActiveFilterAndUnreadAndReorder) {
using Reorder = Ui::ChatsFiltersTabsReorder;
state->reorder = std::make_unique<Reorder>(slider, scroll);
const auto applyReorder = [=](
int oldPosition,
int newPosition) {
if (newPosition == oldPosition) {
return;
}
const auto filters = &session->data().chatsFilters();
const auto &list = filters->list();
if (!session->user()->isPremium()) {
if (list[0].id() != FilterId()) {
filters->moveAllToFront();
}
}
Assert(oldPosition >= 0 && oldPosition < list.size());
Assert(newPosition >= 0 && newPosition < list.size());
auto order = ranges::views::all(
list
) | ranges::views::transform(
&Data::ChatFilter::id
) | ranges::to_vector;
base::reorder(order, oldPosition, newPosition);
state->ignoreRefresh = true;
filters->saveOrder(order);
state->ignoreRefresh = false;
};
state->reorder->updates(
) | rpl::start_with_next([=](const Reorder::Single &data) {
if (data.state == Reorder::State::Started) {
slider->setReordering(slider->reordering() + 1);
} else {
Ui::PostponeCall(slider, [=] {
slider->setReordering(slider->reordering() - 1);
});
if (data.state == Reorder::State::Applied) {
applyReorder(data.oldPosition, data.newPosition);
}
}
}, slider->lifetime());
}
wrap->toggle(false, anim::type::instant);
scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
const auto pixelDelta = e->pixelDelta();
const auto angleDelta = e->angleDelta();
if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {
return false;
}
const auto bar = scroll->horizontalScrollBar();
const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();
bar->setValue(bar->value() - y);
return true;
});
const auto scrollToIndex = [=](int index, anim::type type) {
const auto to = index
? (slider->centerOfSection(index) - scroll->width() / 2)
: 0;
const auto bar = scroll->horizontalScrollBar();
state->animation.stop();
if (type == anim::type::instant) {
bar->setValue(to);
} else {
state->animation.start(
[=](float64 v) { bar->setValue(v); },
bar->value(),
std::min(to, bar->maximum()),
st::defaultTabsSlider.duration);
}
};
const auto applyFilter = [=](const Data::ChatFilter &filter) {
if (slider->reordering()) {
return;
}
choose(filter.id());
};
const auto filterByIndex = [=](int index) -> const Data::ChatFilter& {
const auto &list = session->data().chatsFilters().list();
Assert(index >= 0 && index < list.size());
return list[index];
};
const auto rebuild = [=] {
const auto &list = session->data().chatsFilters().list();
if ((list.size() <= 1) || state->ignoreRefresh) {
return;
}
auto sections = ranges::views::all(
list
) | ranges::views::transform([](const Data::ChatFilter &filter) {
return filter.title().isEmpty()
? tr::lng_filters_all(tr::now)
: filter.title();
}) | ranges::to_vector;
slider->setSections(std::move(sections));
slider->fitWidthToSections();
{
const auto reorderAll = session->user()->isPremium();
const auto maxLimit = (reorderAll ? 1 : 0)
+ Data::PremiumLimits(session).dialogFiltersCurrent();
const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;
slider->setLockedFrom((premiumFrom >= list.size())
? 0
: premiumFrom);
slider->lockedClicked() | rpl::start_with_next([=] {
controller->show(Box(FiltersLimitBox, session, std::nullopt));
}, slider->lifetime());
if (state->reorder) {
state->reorder->cancel();
if (!reorderAll) {
state->reorder->addPinnedInterval(0, 1);
}
state->reorder->addPinnedInterval(
premiumFrom,
std::max(1, int(list.size()) - maxLimit));
}
}
if (trackActiveFilterAndUnreadAndReorder) {
auto includeMuted = Data::IncludeMutedCounterFoldersValue();
state->unreadLifetime.destroy();
for (auto i = 0; i < list.size(); i++) {
rpl::combine(
Data::UnreadStateValue(session, list[i].id()),
rpl::duplicate(includeMuted)
) | rpl::start_with_next([=](
const Dialogs::UnreadState &state,
bool includeMuted) {
const auto muted = (state.chatsMuted + state.marksMuted);
const auto count = (state.chats + state.marks)
- (includeMuted ? 0 : muted);
slider->setUnreadCount(i, count);
slider->fitWidthToSections();
}, state->unreadLifetime);
}
}
[&] {
const auto lookingId = state->lastFilterId.value_or(list[0].id());
for (auto i = 0; i < list.size(); i++) {
const auto &filter = list[i];
if (filter.id() == lookingId) {
const auto wasLast = !!state->lastFilterId;
state->lastFilterId = filter.id();
slider->setActiveSectionFast(i);
scrollToIndex(
i,
wasLast ? anim::type::normal : anim::type::instant);
applyFilter(filter);
return;
}
}
if (list.size()) {
const auto index = 0;
const auto &filter = filterByIndex(index);
state->lastFilterId = filter.id();
slider->setActiveSectionFast(index);
scrollToIndex(index, anim::type::instant);
applyFilter(filter);
}
}();
if (trackActiveFilterAndUnreadAndReorder) {
controller->activeChatsFilter(
) | rpl::start_with_next([=](FilterId id) {
const auto &list = session->data().chatsFilters().list();
for (auto i = 0; i < list.size(); ++i) {
if (list[i].id() == id) {
slider->setActiveSection(i);
scrollToIndex(i, anim::type::normal);
break;
}
}
state->reorder->finishReordering();
}, slider->lifetime());
}
slider->sectionActivated() | rpl::distinct_until_changed(
) | rpl::start_with_next([=](int index) {
if (slider->reordering()) {
return;
}
const auto &filter = filterByIndex(index);
state->lastFilterId = filter.id();
scrollToIndex(index, anim::type::normal);
applyFilter(filter);
}, wrap->lifetime());
slider->contextMenuRequested() | rpl::start_with_next([=](int index) {
if (trackActiveFilterAndUnreadAndReorder) {
ShowMenu(wrap, controller, state, index);
} else {
ShowFiltersListMenu(
wrap,
session,
state,
slider->activeSection(),
[=](int i) { slider->setActiveSection(i); });
}
}, slider->lifetime());
wrap->toggle((list.size() > 1), anim::type::instant);
if (state->reorder) {
state->reorder->start();
}
};
session->data().chatsFilters().changed(
) | rpl::start_with_next(rebuild, wrap->lifetime());
rebuild();
session->data().chatsFilters().isChatlistChanged(
) | rpl::start_with_next([=](FilterId id) {
if (!id || !state->lastFilterId || (id != state->lastFilterId)) {
return;
}
for (const auto &filter : session->data().chatsFilters().list()) {
if (filter.id() == id) {
applyFilter(filter);
return;
}
}
}, wrap->lifetime());
rpl::combine(
parent->widthValue() | rpl::filter(rpl::mappers::_1 > 0),
slider->heightValue() | rpl::filter(rpl::mappers::_1 > 0)
) | rpl::start_with_next([=](int w, int h) {
scroll->resize(w, h + scrollSt.deltax * 4);
container->resize(w, h);
wrap->resize(w, h);
}, wrap->lifetime());
return wrap;
}
} // namespace Ui

View File

@@ -0,0 +1,26 @@
/*
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
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Ui {
not_null<Ui::RpWidget*> AddChatFiltersTabsStrip(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
Fn<void(FilterId)> choose,
bool trackActiveFilterAndUnreadAndReorder = false);
} // namespace Ui

View File

@@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/discrete_sliders.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "styles/style_widgets.h"
namespace Ui {
@@ -57,10 +57,21 @@ void DiscreteSlider::finishAnimating() {
}
}
void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) {
if (index >= 0 && index < _sections.size()) {
auto &section = _sections[index];
section.contentWidth = section.label.maxWidth() + w;
}
}
void DiscreteSlider::setSelectOnPress(bool selectOnPress) {
_selectOnPress = selectOnPress;
}
std::vector<DiscreteSlider::Section> &DiscreteSlider::sectionsRef() {
return _sections;
}
void DiscreteSlider::addSection(const QString &label) {
_sections.push_back(Section(label, getLabelStyle()));
resizeToWidth(width());
@@ -112,7 +123,7 @@ DiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const {
return { 0, 0 };
}
const auto width = _snapToLabel
? std::min(raw->width, raw->label.maxWidth())
? std::min(raw->width, raw->contentWidth)
: raw->width;
return { raw->left + ((raw->width - width) / 2), width };
}
@@ -125,8 +136,7 @@ DiscreteSlider::Range DiscreteSlider::getCurrentActiveRange() const {
};
}
template <typename Lambda>
void DiscreteSlider::enumerateSections(Lambda callback) {
void DiscreteSlider::enumerateSections(Fn<bool(Section&)> callback) {
for (auto &section : _sections) {
if (!callback(section)) {
return;
@@ -134,9 +144,9 @@ void DiscreteSlider::enumerateSections(Lambda callback) {
}
}
template <typename Lambda>
void DiscreteSlider::enumerateSections(Lambda callback) const {
for (auto &section : _sections) {
void DiscreteSlider::enumerateSections(
Fn<bool(const Section&)> callback) const {
for (const auto &section : _sections) {
if (!callback(section)) {
return;
}
@@ -144,7 +154,7 @@ void DiscreteSlider::enumerateSections(Lambda callback) const {
}
void DiscreteSlider::mousePressEvent(QMouseEvent *e) {
auto index = getIndexFromPosition(e->pos());
const auto index = getIndexFromPosition(e->pos());
if (_selectOnPress) {
setSelectedSection(index);
}
@@ -153,17 +163,21 @@ void DiscreteSlider::mousePressEvent(QMouseEvent *e) {
}
void DiscreteSlider::mouseMoveEvent(QMouseEvent *e) {
if (_pressed < 0) return;
if (_pressed < 0) {
return;
}
if (_selectOnPress) {
setSelectedSection(getIndexFromPosition(e->pos()));
}
}
void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) {
auto pressed = std::exchange(_pressed, -1);
if (pressed < 0) return;
const auto pressed = std::exchange(_pressed, -1);
if (pressed < 0) {
return;
}
auto index = getIndexFromPosition(e->pos());
const auto index = getIndexFromPosition(e->pos());
if (pressed < _sections.size()) {
if (_sections[pressed].ripple) {
_sections[pressed].ripple->lastStop();
@@ -175,14 +189,16 @@ void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) {
}
void DiscreteSlider::setSelectedSection(int index) {
if (index < 0 || index >= _sections.size()) return;
if (index < 0 || index >= _sections.size()) {
return;
}
if (_selected != index) {
const auto from = getFinalActiveRange();
_selected = index;
const auto to = getFinalActiveRange();
const auto duration = getAnimationDuration();
const auto updater = [=] { update(); };
const auto updater = [this] { update(); };
_a_left.start(updater, from.left, to.left, duration);
_a_width.start(updater, from.width, to.width, duration);
_callbackAfterMs = crl::now() + duration;
@@ -190,8 +206,8 @@ void DiscreteSlider::setSelectedSection(int index) {
}
int DiscreteSlider::getIndexFromPosition(QPoint pos) {
int count = _sections.size();
for (int i = 0; i != count; ++i) {
const auto count = _sections.size();
for (auto i = 0; i != count; ++i) {
if (_sections[i].left + _sections[i].width > pos.x()) {
return i;
}
@@ -202,7 +218,8 @@ int DiscreteSlider::getIndexFromPosition(QPoint pos) {
DiscreteSlider::Section::Section(
const QString &label,
const style::TextStyle &st)
: label(st, label) {
: label(st, label)
, contentWidth(Section::label.maxWidth()) {
}
DiscreteSlider::Section::Section(
@@ -210,6 +227,7 @@ DiscreteSlider::Section::Section(
const style::TextStyle &st,
const std::any &context) {
this->label.setMarkedText(st, label, kMarkupTextOptions, context);
contentWidth = Section::label.maxWidth();
}
SettingsSlider::SettingsSlider(
@@ -237,10 +255,12 @@ int SettingsSlider::getAnimationDuration() const {
}
void SettingsSlider::resizeSections(int newWidth) {
auto count = getSectionsCount();
if (!count) return;
const auto count = getSectionsCount();
if (!count) {
return;
}
auto sectionWidths = countSectionsWidths(newWidth);
const auto sectionWidths = countSectionsWidths(newWidth);
auto skip = 0;
auto x = 0.;
@@ -258,18 +278,17 @@ void SettingsSlider::resizeSections(int newWidth) {
stopAnimation();
}
std::vector<float64> SettingsSlider::countSectionsWidths(
int newWidth) const {
auto count = getSectionsCount();
auto sectionsWidth = newWidth - (count - 1) * _st.barSkip;
auto sectionWidth = sectionsWidth / float64(count);
std::vector<float64> SettingsSlider::countSectionsWidths(int newWidth) const {
const auto count = getSectionsCount();
const auto sectionsWidth = newWidth - (count - 1) * _st.barSkip;
const auto sectionWidth = sectionsWidth / float64(count);
auto result = std::vector<float64>(count, sectionWidth);
auto labelsWidth = 0;
auto commonWidth = true;
enumerateSections([&](const Section &section) {
labelsWidth += section.label.maxWidth();
if (section.label.maxWidth() >= sectionWidth) {
labelsWidth += section.contentWidth;
if (section.contentWidth >= sectionWidth) {
commonWidth = false;
}
return true;
@@ -283,7 +302,7 @@ std::vector<float64> SettingsSlider::countSectionsWidths(
enumerateSections([&](const Section &section) {
Expects(currentWidth != result.end());
*currentWidth = padding + section.label.maxWidth() + padding;
*currentWidth = padding + section.contentWidth + padding;
++currentWidth;
return true;
});
@@ -297,7 +316,9 @@ int SettingsSlider::resizeGetHeight(int newWidth) {
}
void SettingsSlider::startRipple(int sectionIndex) {
if (!_st.ripple.showDuration) return;
if (!_st.ripple.showDuration) {
return;
}
auto index = 0;
enumerateSections([this, &index, sectionIndex](Section &section) {
if (index++ == sectionIndex) {
@@ -319,13 +340,13 @@ void SettingsSlider::startRipple(int sectionIndex) {
QImage SettingsSlider::prepareRippleMask(
int sectionIndex,
const Section &section) {
auto size = QSize(section.width, height() - _st.rippleBottomSkip);
const auto size = QSize(section.width, height() - _st.rippleBottomSkip);
if (!_rippleTopRoundRadius
|| (sectionIndex > 0 && sectionIndex + 1 < getSectionsCount())) {
return RippleAnimation::RectMask(size);
}
return RippleAnimation::MaskByDrawer(size, false, [&](QPainter &p) {
auto plusRadius = _rippleTopRoundRadius + 1;
const auto plusRadius = _rippleTopRoundRadius + 1;
p.drawRoundedRect(
0,
0,
@@ -347,10 +368,10 @@ QImage SettingsSlider::prepareRippleMask(
}
void SettingsSlider::paintEvent(QPaintEvent *e) {
Painter p(this);
auto p = QPainter(this);
auto clip = e->rect();
auto range = getCurrentActiveRange();
const auto clip = e->rect();
const auto range = DiscreteSlider::getCurrentActiveRange();
const auto drawRect = [&](QRect rect, bool active = false) {
const auto &bar = active ? _barActive : _bar;
@@ -362,32 +383,39 @@ void SettingsSlider::paintEvent(QPaintEvent *e) {
};
enumerateSections([&](Section &section) {
const auto activeWidth = _st.barSnapToLabel
? section.label.maxWidth()
? section.contentWidth
: section.width;
const auto activeLeft = section.left
+ (section.width - activeWidth) / 2;
auto active = 1.
const auto active = 1.
- std::clamp(
qAbs(range.left - activeLeft) / float64(range.width),
std::abs(range.left - activeLeft) / float64(range.width),
0.,
1.);
if (section.ripple) {
auto color = anim::color(_st.rippleBg, _st.rippleBgActive, active);
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
section.ripple->paint(p, section.left, 0, width(), &color);
if (section.ripple->empty()) {
section.ripple.reset();
}
}
if (!_st.barSnapToLabel) {
auto from = activeLeft, tofill = activeWidth;
auto from = activeLeft;
auto tofill = activeWidth;
if (range.left > from) {
auto fill = qMin(tofill, range.left - from);
const auto fill = std::min(tofill, range.left - from);
drawRect(myrtlrect(from, _st.barTop, fill, _st.barStroke));
from += fill;
tofill -= fill;
}
if (range.left + activeWidth > from) {
if (auto fill = qMin(tofill, range.left + activeWidth - from)) {
const auto fill = std::min(
tofill,
range.left + activeWidth - from);
if (fill) {
drawRect(
myrtlrect(from, _st.barTop, fill, _st.barStroke),
true);
@@ -399,15 +427,20 @@ void SettingsSlider::paintEvent(QPaintEvent *e) {
drawRect(myrtlrect(from, _st.barTop, tofill, _st.barStroke));
}
}
const auto labelLeft = section.left + (section.width - section.label.maxWidth()) / 2;
if (myrtlrect(labelLeft, _st.labelTop, section.label.maxWidth(), _st.labelStyle.font->height).intersects(clip)) {
const auto labelLeft = section.left
+ (section.width - section.contentWidth) / 2;
const auto rect = myrtlrect(
labelLeft,
_st.labelTop,
section.contentWidth,
_st.labelStyle.font->height);
if (rect.intersects(clip)) {
p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
section.label.drawLeft(
p,
labelLeft,
_st.labelTop,
section.label.maxWidth(),
width());
section.label.draw(p, {
.position = QPoint(labelLeft, _st.labelTop),
.outerWidth = width(),
.availableWidth = section.label.maxWidth(),
});
}
return true;
});
@@ -416,7 +449,9 @@ void SettingsSlider::paintEvent(QPaintEvent *e) {
const auto from = std::max(range.left - add, 0);
const auto till = std::min(range.left + range.width + add, width());
if (from < till) {
drawRect(myrtlrect(from, _st.barTop, till - from, _st.barStroke), true);
drawRect(
myrtlrect(from, _st.barTop, till - from, _st.barStroke),
true);
}
}
}

View File

@@ -10,7 +10,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "ui/round_rect.h"
#include "ui/effects/animations.h"
#include "styles/style_widgets.h"
namespace style {
struct TextStyle;
struct SettingsSlider;
} // namespace style
namespace st {
extern const style::SettingsSlider &defaultSettingsSlider;
} // namespace st
namespace Ui {
@@ -36,6 +44,8 @@ public:
void setActiveSectionFast(int index);
void finishAnimating();
void setAdditionalContentWidthToSection(int index, int width);
[[nodiscard]] rpl::producer<int> sectionActivated() const {
return _sectionActivated.events();
}
@@ -55,10 +65,11 @@ protected:
const style::TextStyle &st,
const std::any &context);
int left = 0;
int width = 0;
Ui::Text::String label;
std::unique_ptr<RippleAnimation> ripple;
int left = 0;
int width = 0;
int contentWidth = 0;
};
struct Range {
int left = 0;
@@ -72,11 +83,8 @@ protected:
return _sections.size();
}
template <typename Lambda>
void enumerateSections(Lambda callback);
template <typename Lambda>
void enumerateSections(Lambda callback) const;
void enumerateSections(Fn<bool(Section&)> callback);
void enumerateSections(Fn<bool(const Section&)> callback) const;
virtual void startRipple(int sectionIndex) {
}
@@ -89,6 +97,8 @@ protected:
void setSelectOnPress(bool selectOnPress);
std::vector<Section> &sectionsRef();
private:
void activateCallback();
virtual const style::TextStyle &getLabelStyle() const = 0;
@@ -116,7 +126,9 @@ private:
class SettingsSlider : public DiscreteSlider {
public:
SettingsSlider(QWidget *parent, const style::SettingsSlider &st = st::defaultSettingsSlider);
SettingsSlider(
QWidget *parent,
const style::SettingsSlider &st = st::defaultSettingsSlider);
void setRippleTopRoundRadius(int radius);
@@ -127,13 +139,14 @@ protected:
void startRipple(int sectionIndex) override;
std::vector<float64> countSectionsWidths(int newWidth) const;
private:
const style::TextStyle &getLabelStyle() const override;
int getAnimationDuration() const override;
QImage prepareRippleMask(int sectionIndex, const Section &section);
void resizeSections(int newWidth);
std::vector<float64> countSectionsWidths(int newWidth) const;
const style::SettingsSlider &_st;
std::optional<Ui::RoundRect> _bar;

View File

@@ -229,7 +229,8 @@ void Controller::setupSideBar() {
sideBarChanged();
}, _sessionController->lifetime());
if (_sessionController->session().settings().dialogsFiltersEnabled()) {
if (_sessionController->session().settings().dialogsFiltersEnabled()
&& !Core::App().settings().chatFiltersHorizontal()) {
_sessionController->toggleFiltersMenu(true);
} else {
sideBarChanged();

View File

@@ -13,15 +13,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_main_menu.h"
#include "window/window_peer_menu.h"
#include "main/main_session.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "window/notifications_manager.h"
#include "data/data_session.h"
#include "data/data_chat_filters.h"
#include "data/data_folder.h"
#include "data/data_user.h"
#include "data/data_peer_values.h"
#include "data/data_premium_limits.h"
#include "data/data_unread_value.h"
#include "lang/lang_keys.h"
#include "ui/filter_icons.h"
#include "ui/wrap/vertical_layout.h"
@@ -41,42 +38,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_menu_icons.h"
namespace Window {
namespace {
[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState(
not_null<Main::Session*> session,
const Dialogs::UnreadState &state) {
const auto folderId = Data::Folder::kId;
if (const auto folder = session->data().folderLoaded(folderId)) {
return state - folder->chatsList()->unreadState();
}
return state;
}
[[nodiscard]] rpl::producer<Dialogs::UnreadState> MainListUnreadState(
not_null<Dialogs::MainList*> list) {
return rpl::single(rpl::empty) | rpl::then(
list->unreadStateChanges() | rpl::to_empty
) | rpl::map([=] {
return list->unreadState();
});
}
[[nodiscard]] rpl::producer<Dialogs::UnreadState> UnreadStateValue(
not_null<Main::Session*> session,
FilterId filterId) {
if (filterId > 0) {
const auto filters = &session->data().chatsFilters();
return MainListUnreadState(filters->chatsList(filterId));
}
return MainListUnreadState(
session->data().chatsList()
) | rpl::map([=](const Dialogs::UnreadState &state) {
return MainListMapUnreadState(session, state);
});
}
} // namespace
FiltersMenu::FiltersMenu(
not_null<Ui::RpWidget*> parent,
@@ -88,14 +49,7 @@ FiltersMenu::FiltersMenu(
, _scroll(&_outer)
, _container(
_scroll.setOwnedWidget(
object_ptr<Ui::VerticalLayout>(&_scroll)))
, _includeMuted(Core::App().settings().includeMutedCounterFolders()) {
Core::App().notifications().settingsChanged(
) | rpl::filter(
rpl::mappers::_1 == Window::Notifications::ChangeType::IncludeMuted
) | rpl::start_with_next([=] {
_includeMuted = Core::App().settings().includeMutedCounterFolders();
}, _outer.lifetime());
object_ptr<Ui::VerticalLayout>(&_scroll))) {
_drag.timer.setCallback([=] {
if (_drag.filterId >= 0) {
@@ -266,7 +220,7 @@ void FiltersMenu::setupList() {
_reorder = std::make_unique<Ui::VerticalLayoutReorder>(_list, &_scroll);
_reorder->updates(
) | rpl::start_with_next([=](Ui::VerticalLayoutReorder::Single data) {
) | rpl::start_with_next([=](Ui::VerticalLayoutReorder::Single data) {
using State = Ui::VerticalLayoutReorder::State;
if (data.state == State::Started) {
++_reordering;
@@ -310,8 +264,8 @@ base::unique_qptr<Ui::SideBarButton> FiltersMenu::prepareButton(
raw->setIconOverride(icons.normal, icons.active);
if (id >= 0) {
rpl::combine(
UnreadStateValue(&_session->session(), id),
_includeMuted.value()
Data::UnreadStateValue(&_session->session(), id),
Data::IncludeMutedCounterFoldersValue()
) | rpl::start_with_next([=](
const Dialogs::UnreadState &state,
bool includeMuted) {
@@ -413,7 +367,7 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) {
if (id) {
addAction(
tr::lng_filters_context_edit(tr::now),
[=] { showEditBox(id); },
[=] { EditExistingFilter(_session, id); },
&st::menuIconEdit);
auto filteredChats = [=] {
@@ -426,12 +380,12 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) {
addAction(
tr::lng_filters_context_remove(tr::now),
[=] { showRemoveBox(id); },
[=] { _removeApi.request(Ui::MakeWeak(&_outer), _session, id); },
&st::menuIconDelete);
} else {
auto customUnreadState = [=] {
const auto session = &_session->session();
return MainListMapUnreadState(
return Data::MainListMapUnreadState(
session,
session->data().chatsList()->unreadState());
};
@@ -453,105 +407,6 @@ void FiltersMenu::showMenu(QPoint position, FilterId id) {
_popupMenu->popup(position);
}
void FiltersMenu::showEditBox(FilterId id) {
EditExistingFilter(_session, id);
}
void FiltersMenu::showRemoveBox(FilterId id) {
const auto session = &_session->session();
const auto &list = session->data().chatsFilters().list();
const auto i = ranges::find(list, id, &Data::ChatFilter::id);
const auto filter = (i != end(list)) ? *i : Data::ChatFilter();
const auto has = filter.hasMyLinks();
const auto confirm = [=](Fn<void()> action, bool onlyWhenHas = false) {
if (!has && onlyWhenHas) {
action();
return;
}
_session->window().show(Ui::MakeConfirmBox({
.text = (has
? tr::lng_filters_delete_sure()
: tr::lng_filters_remove_sure()),
.confirmed = [=](Fn<void()> &&close) { close(); action(); },
.confirmText = (has
? tr::lng_box_delete()
: tr::lng_filters_remove_yes()),
.confirmStyle = &st::attentionBoxButton,
}));
};
const auto simple = [=] {
confirm([=] { remove(id); });
};
const auto suggestRemoving = Api::ExtractSuggestRemoving(filter);
if (suggestRemoving.empty()) {
simple();
return;
} else if (_removingRequestId) {
if (_removingId == id) {
return;
}
session->api().request(_removingRequestId).cancel();
}
_removingId = id;
_removingRequestId = session->api().request(
MTPchatlists_GetLeaveChatlistSuggestions(
MTP_inputChatlistDialogFilter(
MTP_int(id)))
).done(crl::guard(&_outer, [=](const MTPVector<MTPPeer> &result) {
_removingRequestId = 0;
const auto suggestRemovePeers = ranges::views::all(
result.v
) | ranges::views::transform([=](const MTPPeer &peer) {
return session->data().peer(peerFromMTP(peer));
}) | ranges::to_vector;
const auto chosen = crl::guard(&_outer, [=](
std::vector<not_null<PeerData*>> peers) {
remove(id, std::move(peers));
});
confirm(crl::guard(&_outer, [=] {
Api::ProcessFilterRemove(
_session,
filter.title(),
filter.iconEmoji(),
suggestRemoving,
suggestRemovePeers,
chosen);
}), true);
})).fail(crl::guard(&_outer, [=] {
_removingRequestId = 0;
simple();
})).send();
}
void FiltersMenu::remove(
FilterId id,
std::vector<not_null<PeerData*>> leave) {
const auto session = &_session->session();
const auto api = &session->api();
session->data().chatsFilters().apply(MTP_updateDialogFilter(
MTP_flags(MTPDupdateDialogFilter::Flag(0)),
MTP_int(id),
MTPDialogFilter()));
if (leave.empty()) {
api->request(MTPmessages_UpdateDialogFilter(
MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)),
MTP_int(id),
MTPDialogFilter()
)).send();
} else {
api->request(MTPchatlists_LeaveChatlist(
MTP_inputChatlistDialogFilter(MTP_int(id)),
MTP_vector<MTPInputPeer>(ranges::views::all(
leave
) | ranges::views::transform([](not_null<PeerData*> peer) {
return MTPInputPeer(peer->input);
}) | ranges::to<QVector<MTPInputPeer>>())
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
}).send();
}
}
void FiltersMenu::applyReorder(
not_null<Ui::RpWidget*> widget,
int oldPosition,

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "api/api_chat_filters_remove_manager.h"
#include "base/timer.h"
#include "ui/effects/animations.h"
#include "ui/widgets/side_bar_button.h"
@@ -48,9 +49,6 @@ private:
bool toBeginning = false);
void setupMainMenuIcon();
void showMenu(QPoint position, FilterId id);
void showEditBox(FilterId id);
void showRemoveBox(FilterId id);
void remove(FilterId id, std::vector<not_null<PeerData*>> leave = {});
void scrollToButton(not_null<Ui::RpWidget*> widget);
void openFiltersSettings();
@@ -70,6 +68,8 @@ private:
bool _ignoreRefresh = false;
bool _waitingSuggested = false;
Api::RemoveComplexChatFilter _removeApi;
FilterId _removingId = 0;
mtpRequestId _removingRequestId = 0;

View File

@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "window/window_peer_menu.h"
#include "api/api_report.h"
#include "menu/menu_check_item.h"
#include "boxes/share_box.h"
#include "boxes/star_gift_box.h"
@@ -38,10 +37,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_contact_box.h"
#include "calls/calls_instance.h"
#include "inline_bots/bot_attach_web_view.h" // InlineBots::PeerType.
#include "ui/boxes/report_box_graphics.h"
#include "ui/toast/toast.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/chat_filters_tabs_strip.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/popup_menu.h"
@@ -2053,7 +2052,77 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
const auto state = [&] {
auto controller = std::make_unique<Controller>(session);
const auto controllerRaw = controller.get();
auto box = Box<ListBox>(std::move(controller), nullptr);
auto init = [=](not_null<PeerListBox*> box) {
box->setSpecialTabMode(true);
auto applyFilter = [=](FilterId id) {
box->scrollToY(0);
auto &filters = session->data().chatsFilters();
const auto &list = filters.list();
if (list.size() <= 1) {
return;
}
const auto pinned = filters.chatsList(id)->pinned()->order();
box->peerListSortRows([&](
const PeerListRow &r1,
const PeerListRow &r2) {
const auto it1 = ranges::find_if(pinned, [&](
const Dialogs::Key &k) {
return k.peer() == r1.peer();
});
const auto it2 = ranges::find_if(pinned, [&](
const Dialogs::Key &k) {
return k.peer() == r2.peer();
});
if (it1 == pinned.end() && it2 != pinned.end()) {
return false;
} else if (it2 == pinned.end() && it1 != pinned.end()) {
return true;
} else if (it1 != pinned.end() && it2 != pinned.end()) {
return it1 < it2;
}
const auto history1 = session->data().history(r1.peer());
const auto history2 = session->data().history(r2.peer());
const auto date1 = history1->lastMessage()
? history1->lastMessage()->date()
: TimeId(0);
const auto date2 = history2->lastMessage()
? history2->lastMessage()->date()
: TimeId(0);
return date1 > date2;
});
const auto filter = ranges::find(
list,
id,
&Data::ChatFilter::id);
if (filter == list.end()) {
return;
}
box->peerListPartitionRows([&](const PeerListRow &row) {
const auto rowPtr = const_cast<PeerListRow*>(&row);
if (!filter->id()) {
box->peerListSetRowHidden(rowPtr, false);
} else {
const auto result = filter->contains(
session->data().history(row.peer()));
box->peerListSetRowHidden(rowPtr, !result);
}
return false;
});
box->peerListRefreshRows();
};
const auto chatsFilters = Ui::AddChatFiltersTabsStrip(
box,
session,
std::move(applyFilter));
chatsFilters->lower();
chatsFilters->heightValue() | rpl::start_with_next([box](int h) {
box->setAddedTopScrollSkip(h);
}, box->lifetime());
box->multiSelectHeightValue() | rpl::start_with_next([=](int h) {
chatsFilters->moveToLeft(0, h);
}, chatsFilters->lifetime());
};
auto box = Box<ListBox>(std::move(controller), std::move(init));
const auto boxRaw = box.data();
boxRaw->setForwardOptions({
.sendersCount = sendersCount,

View File

@@ -1314,11 +1314,21 @@ SessionController::SessionController(
closeFolder();
}, lifetime());
session->data().chatsFilters().changed(
rpl::merge(
Core::App().settings().chatFiltersHorizontalChanges() | rpl::to_empty,
session->data().chatsFilters().changed()
) | rpl::start_with_next([=] {
checkOpenedFilter();
crl::on_main(this, [=] {
refreshFiltersMenu();
crl::on_main(this, [this] {
if (SessionNavigation::session().data().chatsFilters().has()) {
const auto isHorizontal
= Core::App().settings().chatFiltersHorizontal();
content()->toggleFiltersMenu(isHorizontal);
toggleFiltersMenu(!isHorizontal);
} else {
content()->toggleFiltersMenu(false);
toggleFiltersMenu(false);
}
});
}, lifetime());
@@ -1549,10 +1559,6 @@ void SessionController::toggleFiltersMenu(bool enabled) {
_filtersMenuChanged.fire({});
}
void SessionController::refreshFiltersMenu() {
toggleFiltersMenu(session().data().chatsFilters().has());
}
rpl::producer<> SessionController::filtersMenuChanged() const {
return _filtersMenuChanged.events();
}

View File

@@ -630,7 +630,6 @@ private:
void init();
void setupShortcuts();
void refreshFiltersMenu();
void checkOpenedFilter();
void suggestArchiveAndMute();
void activateFirstChatsFilter();

View File

@@ -70,9 +70,9 @@ else
DeployMac="1"
DeployWin="1"
DeployWin64="1"
DeployWinArm="0"
DeployWinArm="1"
DeployLinux="1"
echo "Deploying four versions of $AppVersionStrFull: for Windows 32 bit, Windows 64 bit, macOS and Linux 64 bit.."
echo "Deploying five versions of $AppVersionStrFull: for Windows 32 bit / 64 bit / on ARM, macOS and Linux 64 bit.."
fi
if [ "$BuildTarget" == "mac" ]; then
BackupPath="$HOME/Projects/backup/tdesktop"
@@ -102,7 +102,7 @@ Win64RemoteFolder="tx64"
WinArmDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tarm64"
WinArmUpdateFile="tarm64upd$AppVersion"
WinArmSetupFile="tsetup-arm64.$AppVersionStrFull.exe"
WinArmPortablefile="tportable-arm64.$AppVersionStrFull.zip"
WinArmPortableFile="tportable-arm64.$AppVersionStrFull.zip"
WinArmRemoteFolder="tarm64"
LinuxDeployPath="$BackupPath/$AppVersionStrMajor/$AppVersionStrFull/tlinux"
LinuxUpdateFile="tlinuxupd$AppVersion"
@@ -138,7 +138,7 @@ if [ "$AlphaVersion" != "0" ]; then
Win64UpdateFile="${Win64UpdateFile}_${AlphaSignature}"
Win64PortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip"
WinArmUpdateFile="${WinArmUpdateFile}_${AlphaSignature}"
WinArmPortablefile="talpha${AlphaVersion}_${AlphaSignature}.zip"
WinArmPortableFile="talpha${AlphaVersion}_${AlphaSignature}.zip"
LinuxUpdateFile="${LinuxUpdateFile}_${AlphaSignature}"
LinuxSetupFile="talpha${AlphaVersion}_${AlphaSignature}.tar.xz"
fi

View File

@@ -42,8 +42,8 @@ SignTool=sha256
#define ArchModulesFolder "arm64"
AppVerName={#MyAppName} {#MyAppVersion} arm64
#elif MyBuildTarget == "win64"
ArchitecturesAllowed="x64 arm64"
ArchitecturesInstallIn64BitMode="x64 arm64"
ArchitecturesAllowed="x64compatible"
ArchitecturesInstallIn64BitMode="x64compatible"
OutputBaseFilename=tsetup-x64.{#MyAppVersionFull}
#define ArchModulesFolder "x64"
AppVerName={#MyAppName} {#MyAppVersion} 64bit

View File

@@ -1,7 +1,7 @@
AppVersion 5007002
AppVersion 5007003
AppVersionStrMajor 5.7
AppVersionStrSmall 5.7.2
AppVersionStr 5.7.2
BetaChannel 0
AppVersionStrSmall 5.7.3
AppVersionStr 5.7.3
BetaChannel 1
AlphaVersion 0
AppVersionOriginal 5.7.2
AppVersionOriginal 5.7.3.beta

View File

@@ -421,6 +421,10 @@ PRIVATE
ui/widgets/fields/time_part_input_with_placeholder.cpp
ui/widgets/fields/time_part_input_with_placeholder.h
ui/widgets/chat_filters_tabs_slider.cpp
ui/widgets/chat_filters_tabs_slider.h
ui/widgets/chat_filters_tabs_slider_reorder.cpp
ui/widgets/chat_filters_tabs_slider_reorder.h
ui/widgets/color_editor.cpp
ui/widgets/color_editor.h
ui/widgets/continuous_sliders.cpp

View File

@@ -1,3 +1,8 @@
5.7.3 beta (13.11.24)
- Option to show folders above the chats list.
- Folders on top of recipients list box.
5.7.2 (05.11.24)
- Fix recompressed video playback cutoff.