Implement vertical list of hidden story sources.
This commit is contained in:
parent
a79deb89ce
commit
451c4e3101
|
@ -2407,6 +2407,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_add_contact" = "Create";
|
"lng_add_contact" = "Create";
|
||||||
"lng_add_contact_button" = "New contact";
|
"lng_add_contact_button" = "New contact";
|
||||||
"lng_contacts_header" = "Contacts";
|
"lng_contacts_header" = "Contacts";
|
||||||
|
"lng_contacts_by_online" = "Sorted by last seen time";
|
||||||
|
"lng_contacts_by_name" = "Sorted by name";
|
||||||
|
"lng_contacts_hidden_stories" = "Hidden Stories";
|
||||||
"lng_contact_not_joined" = "Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\n\nWe will notify you about any of your contacts who join Telegram.";
|
"lng_contact_not_joined" = "Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\n\nWe will notify you about any of your contacts who join Telegram.";
|
||||||
"lng_try_other_contact" = "Try someone else";
|
"lng_try_other_contact" = "Try someone else";
|
||||||
"lng_create_group_link" = "Link";
|
"lng_create_group_link" = "Link";
|
||||||
|
|
|
@ -942,6 +942,16 @@ requestsBoxList: PeerList(peerListBox) {
|
||||||
padding: margins(0px, 12px, 0px, 12px);
|
padding: margins(0px, 12px, 0px, 12px);
|
||||||
item: requestsBoxItem;
|
item: requestsBoxItem;
|
||||||
}
|
}
|
||||||
|
contactsWithStories: PeerList(peerListBox) {
|
||||||
|
item: PeerListItem(peerListBoxItem) {
|
||||||
|
checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
|
||||||
|
check: RoundCheckbox(defaultPeerListCheck) {
|
||||||
|
size: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nameFgChecked: contactsNameFg;
|
||||||
|
}
|
||||||
|
}
|
||||||
requestsAcceptButton: RoundButton(defaultActiveButton) {
|
requestsAcceptButton: RoundButton(defaultActiveButton) {
|
||||||
width: -28px;
|
width: -28px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
|
@ -33,8 +33,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "styles/style_dialogs.h"
|
#include "styles/style_dialogs.h"
|
||||||
#include "styles/style_widgets.h"
|
#include "styles/style_widgets.h"
|
||||||
|
|
||||||
#include <rpl/range.h>
|
|
||||||
|
|
||||||
PaintRoundImageCallback PaintUserpicCallback(
|
PaintRoundImageCallback PaintUserpicCallback(
|
||||||
not_null<PeerData*> peer,
|
not_null<PeerData*> peer,
|
||||||
bool respectSavedMessagesChat) {
|
bool respectSavedMessagesChat) {
|
||||||
|
@ -887,6 +885,13 @@ void PeerListRow::setCheckedInternal(bool checked, anim::type animated) {
|
||||||
_checkbox->setChecked(checked, animated);
|
_checkbox->setChecked(checked, animated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PeerListRow::setCustomizedCheckSegments(
|
||||||
|
std::vector<Ui::RoundImageCheckboxSegment> segments) {
|
||||||
|
Expects(_checkbox != nullptr);
|
||||||
|
|
||||||
|
_checkbox->setCustomizedSegments(std::move(segments));
|
||||||
|
}
|
||||||
|
|
||||||
void PeerListRow::finishCheckedAnimation() {
|
void PeerListRow::finishCheckedAnimation() {
|
||||||
_checkbox->setChecked(_checkbox->checked(), anim::type::instant);
|
_checkbox->setChecked(_checkbox->checked(), anim::type::instant);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ class SlideWrap;
|
||||||
class FlatLabel;
|
class FlatLabel;
|
||||||
struct ScrollToRequest;
|
struct ScrollToRequest;
|
||||||
class PopupMenu;
|
class PopupMenu;
|
||||||
|
struct RoundImageCheckboxSegment;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
||||||
using PaintRoundImageCallback = Fn<void(
|
using PaintRoundImageCallback = Fn<void(
|
||||||
|
@ -202,6 +203,8 @@ public:
|
||||||
}
|
}
|
||||||
setCheckedInternal(checked, animated);
|
setCheckedInternal(checked, animated);
|
||||||
}
|
}
|
||||||
|
void setCustomizedCheckSegments(
|
||||||
|
std::vector<Ui::RoundImageCheckboxSegment> segments);
|
||||||
void setHidden(bool hidden) {
|
void setHidden(bool hidden) {
|
||||||
_hidden = hidden;
|
_hidden = hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
#include "api/api_chat_participants.h"
|
#include "api/api_chat_participants.h"
|
||||||
#include "base/random.h"
|
#include "base/random.h"
|
||||||
|
#include "boxes/filters/edit_filter_chats_list.h"
|
||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
|
#include "ui/effects/round_checkbox.h"
|
||||||
|
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
|
#include "ui/widgets/popup_menu.h"
|
||||||
#include "ui/wrap/padding_wrap.h"
|
#include "ui/wrap/padding_wrap.h"
|
||||||
#include "ui/painter.h"
|
#include "ui/painter.h"
|
||||||
#include "ui/ui_utility.h"
|
#include "ui/ui_utility.h"
|
||||||
|
@ -32,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
#include "history/history_item.h"
|
#include "history/history_item.h"
|
||||||
#include "dialogs/dialogs_main_list.h"
|
#include "dialogs/dialogs_main_list.h"
|
||||||
|
#include "ui/wrap/slide_wrap.h"
|
||||||
#include "window/window_session_controller.h" // showAddContact()
|
#include "window/window_session_controller.h" // showAddContact()
|
||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
#include "styles/style_boxes.h"
|
#include "styles/style_boxes.h"
|
||||||
|
@ -42,12 +47,302 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "dialogs/ui/dialogs_stories_content.h"
|
#include "dialogs/ui/dialogs_stories_content.h"
|
||||||
#include "dialogs/ui/dialogs_stories_list.h"
|
#include "dialogs/ui/dialogs_stories_list.h"
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
|
constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
|
||||||
constexpr auto kSearchPerPage = 50;
|
constexpr auto kSearchPerPage = 50;
|
||||||
|
|
||||||
|
class StoriesRow final : public PeerListRow {
|
||||||
|
public:
|
||||||
|
StoriesRow(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const QBrush &unread,
|
||||||
|
const Dialogs::Stories::Element &element,
|
||||||
|
int position);
|
||||||
|
|
||||||
|
void applySegments(const Dialogs::Stories::Element &element);
|
||||||
|
void updateGradient(QBrush unread);
|
||||||
|
|
||||||
|
int position = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void refreshSegments();
|
||||||
|
|
||||||
|
QBrush _unread;
|
||||||
|
int _count = 0;
|
||||||
|
int _unreadCount = 0;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class StoriesController final
|
||||||
|
: public PeerListController
|
||||||
|
, public base::has_weak_ptr {
|
||||||
|
public:
|
||||||
|
using Content = Dialogs::Stories::Content;
|
||||||
|
using Row = StoriesRow;
|
||||||
|
|
||||||
|
StoriesController(
|
||||||
|
not_null<Window::SessionController*> window,
|
||||||
|
rpl::producer<Content> content,
|
||||||
|
Fn<void(uint64)> open,
|
||||||
|
Fn<void()> loadMore);
|
||||||
|
|
||||||
|
Main::Session &session() const override;
|
||||||
|
void prepare() override;
|
||||||
|
void loadMoreRows() override;
|
||||||
|
void rowClicked(not_null<PeerListRow*> row) override;
|
||||||
|
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<PeerListRow*> row) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void refresh(const Content &content);
|
||||||
|
|
||||||
|
const not_null<Window::SessionController*> _window;
|
||||||
|
QBrush _unread;
|
||||||
|
rpl::producer<Content> _content;
|
||||||
|
Fn<void(uint64)> _open;
|
||||||
|
Fn<void()> _loadMore;
|
||||||
|
bool _positionPositive = false;
|
||||||
|
|
||||||
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
StoriesRow::StoriesRow(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const QBrush &unread,
|
||||||
|
const Dialogs::Stories::Element &element,
|
||||||
|
int position)
|
||||||
|
: PeerListRow(session->data().peer(PeerId(element.id)))
|
||||||
|
, position(position)
|
||||||
|
, _unread(unread) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoriesRow::applySegments(const Dialogs::Stories::Element &element) {
|
||||||
|
Expects(element.unreadCount <= element.count);
|
||||||
|
|
||||||
|
_count = std::max(element.count, 1);
|
||||||
|
_unreadCount = element.unreadCount;
|
||||||
|
refreshSegments();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoriesRow::updateGradient(QBrush unread) {
|
||||||
|
_unread = std::move(unread);
|
||||||
|
refreshSegments();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoriesRow::refreshSegments() {
|
||||||
|
Expects(_unreadCount <= _count);
|
||||||
|
Expects(_count > 0);
|
||||||
|
|
||||||
|
auto segments = std::vector<Ui::RoundImageCheckboxSegment>();
|
||||||
|
const auto add = [&](bool unread) {
|
||||||
|
segments.push_back({
|
||||||
|
.brush = unread ? _unread : st::dialogsUnreadBgMuted->b,
|
||||||
|
.width = (unread
|
||||||
|
? st::dialogsStoriesFull.lineTwice / 2.
|
||||||
|
: st::dialogsStoriesFull.lineReadTwice / 2.),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
segments.reserve(_count);
|
||||||
|
for (auto i = 0, count = _count - _unreadCount; i != count; ++i) {
|
||||||
|
add(false);
|
||||||
|
}
|
||||||
|
for (auto i = 0; i != _unreadCount; ++i) {
|
||||||
|
add(true);
|
||||||
|
}
|
||||||
|
setCustomizedCheckSegments(std::move(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
StoriesController::StoriesController(
|
||||||
|
not_null<Window::SessionController*> window,
|
||||||
|
rpl::producer<Content> content,
|
||||||
|
Fn<void(uint64)> open,
|
||||||
|
Fn<void()> loadMore)
|
||||||
|
: _window(window)
|
||||||
|
, _content(std::move(content))
|
||||||
|
, _open(std::move(open))
|
||||||
|
, _loadMore(std::move(loadMore)) {
|
||||||
|
const auto createGradient = [=] {
|
||||||
|
auto gradient = QLinearGradient(
|
||||||
|
QPoint(10, 0),
|
||||||
|
QPoint(0, 10));
|
||||||
|
gradient.setStops({
|
||||||
|
{ 0., st::groupCallLive1->c },
|
||||||
|
{ 1., st::groupCallMuted1->c },
|
||||||
|
});
|
||||||
|
_unread = QBrush(gradient);
|
||||||
|
};
|
||||||
|
createGradient();
|
||||||
|
style::PaletteChanged(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
createGradient();
|
||||||
|
for (auto i = 0, count = int(delegate()->peerListFullRowsCount())
|
||||||
|
; i != count
|
||||||
|
; ++i) {
|
||||||
|
const auto row = delegate()->peerListRowAt(i).get();
|
||||||
|
static_cast<Row*>(row)->updateGradient(_unread);
|
||||||
|
}
|
||||||
|
}, _lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
Main::Session &StoriesController::session() const {
|
||||||
|
return _window->session();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoriesController::prepare() {
|
||||||
|
if (_loadMore) {
|
||||||
|
_loadMore();
|
||||||
|
}
|
||||||
|
std::move(
|
||||||
|
_content
|
||||||
|
) | rpl::start_with_next([=](Content content) {
|
||||||
|
refresh(content);
|
||||||
|
}, _lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoriesController::loadMoreRows() {
|
||||||
|
if (_loadMore) {
|
||||||
|
_loadMore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoriesController::rowClicked(not_null<PeerListRow*> row) {
|
||||||
|
if (_open) {
|
||||||
|
_open(row->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base::unique_qptr<Ui::PopupMenu> StoriesController::rowContextMenu(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<PeerListRow*> row) {
|
||||||
|
auto result = base::make_unique_q<Ui::PopupMenu>(parent);
|
||||||
|
|
||||||
|
Dialogs::Stories::FillSourceMenu(_window, {
|
||||||
|
.id = row->id(),
|
||||||
|
.callback = Ui::Menu::CreateAddActionCallback(result.get()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result->empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoriesController::refresh(const Content &content) {
|
||||||
|
const auto session = &_window->session();
|
||||||
|
const auto positive = _positionPositive = !_positionPositive;
|
||||||
|
auto position = positive ? 1 : -int(content.elements.size());
|
||||||
|
for (const auto &element : content.elements) {
|
||||||
|
if (const auto row = delegate()->peerListFindRow(element.id)) {
|
||||||
|
static_cast<Row*>(row)->position = position;
|
||||||
|
static_cast<Row*>(row)->applySegments(element);
|
||||||
|
} else {
|
||||||
|
auto added = std::make_unique<Row>(
|
||||||
|
session,
|
||||||
|
_unread,
|
||||||
|
element,
|
||||||
|
position);
|
||||||
|
const auto raw = added.get();
|
||||||
|
delegate()->peerListAppendRow(std::move(added));
|
||||||
|
delegate()->peerListSetRowChecked(raw, true);
|
||||||
|
raw->applySegments(element);
|
||||||
|
}
|
||||||
|
++position;
|
||||||
|
}
|
||||||
|
auto count = delegate()->peerListFullRowsCount();
|
||||||
|
for (auto i = 0; i != count;) {
|
||||||
|
const auto row = delegate()->peerListRowAt(i);
|
||||||
|
const auto position = static_cast<Row*>(row.get())->position;
|
||||||
|
if (positive ? (position > 0) : (position < 0)) {
|
||||||
|
++i;
|
||||||
|
} else {
|
||||||
|
delegate()->peerListRemoveRow(row);
|
||||||
|
--count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate()->peerListSortRows([](
|
||||||
|
const PeerListRow &a,
|
||||||
|
const PeerListRow &b) {
|
||||||
|
return static_cast<const Row&>(a).position
|
||||||
|
< static_cast<const Row&>(b).position;
|
||||||
|
});
|
||||||
|
delegate()->peerListRefreshRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] object_ptr<Ui::RpWidget> PrepareHiddenStoriesList(
|
||||||
|
not_null<PeerListBox*> box,
|
||||||
|
not_null<Window::SessionController*> sessionController,
|
||||||
|
rpl::producer<ContactsBoxController::SortMode> mode) {
|
||||||
|
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||||
|
box,
|
||||||
|
object_ptr<Ui::VerticalLayout>(box));
|
||||||
|
const auto container = result->entity();
|
||||||
|
const auto stories = &sessionController->session().data().stories();
|
||||||
|
|
||||||
|
container->add(CreatePeerListSectionSubtitle(
|
||||||
|
container,
|
||||||
|
tr::lng_contacts_hidden_stories()));
|
||||||
|
|
||||||
|
auto &lifetime = container->lifetime();
|
||||||
|
auto list = Dialogs::Stories::ContentForSession(
|
||||||
|
&sessionController->session(),
|
||||||
|
Data::StorySourcesList::Hidden
|
||||||
|
) | rpl::start_spawning(lifetime);
|
||||||
|
const auto delegate = lifetime.make_state<
|
||||||
|
PeerListContentDelegateSimple
|
||||||
|
>();
|
||||||
|
const auto open = [=](uint64 id) {
|
||||||
|
sessionController->openPeerStories(
|
||||||
|
PeerId(int64(id)),
|
||||||
|
Data::StorySourcesList::Hidden);
|
||||||
|
};
|
||||||
|
const auto loadMore = [=] {
|
||||||
|
stories->loadMore(Data::StorySourcesList::Hidden);
|
||||||
|
};
|
||||||
|
const auto controller = lifetime.make_state<StoriesController>(
|
||||||
|
sessionController,
|
||||||
|
rpl::duplicate(
|
||||||
|
list
|
||||||
|
) | rpl::filter([](const Dialogs::Stories::Content &list) {
|
||||||
|
return !list.elements.empty();
|
||||||
|
}),
|
||||||
|
open,
|
||||||
|
loadMore);
|
||||||
|
controller->setStyleOverrides(&st::contactsWithStories);
|
||||||
|
const auto content = container->add(object_ptr<PeerListContent>(
|
||||||
|
container,
|
||||||
|
controller));
|
||||||
|
delegate->setContent(content);
|
||||||
|
controller->setDelegate(delegate);
|
||||||
|
|
||||||
|
container->add(CreatePeerListSectionSubtitle(
|
||||||
|
container,
|
||||||
|
rpl::conditional(
|
||||||
|
std::move(
|
||||||
|
mode
|
||||||
|
) | rpl::map(
|
||||||
|
rpl::mappers::_1 == ContactsBoxController::SortMode::Online
|
||||||
|
),
|
||||||
|
tr::lng_contacts_by_online(),
|
||||||
|
tr::lng_contacts_by_name())));
|
||||||
|
|
||||||
|
stories->incrementPreloadingHiddenSources();
|
||||||
|
lifetime.add([=] {
|
||||||
|
stories->decrementPreloadingHiddenSources();
|
||||||
|
});
|
||||||
|
|
||||||
|
result->toggleOn(rpl::duplicate(
|
||||||
|
list
|
||||||
|
) | rpl::map([](const Dialogs::Stories::Content &list) {
|
||||||
|
return !list.elements.empty();
|
||||||
|
}));
|
||||||
|
result->finishAnimating();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
object_ptr<Ui::BoxContent> PrepareContactsBox(
|
object_ptr<Ui::BoxContent> PrepareContactsBox(
|
||||||
|
@ -55,14 +350,14 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
|
||||||
using Mode = ContactsBoxController::SortMode;
|
using Mode = ContactsBoxController::SortMode;
|
||||||
auto controller = std::make_unique<ContactsBoxController>(
|
auto controller = std::make_unique<ContactsBoxController>(
|
||||||
&sessionController->session());
|
&sessionController->session());
|
||||||
|
controller->setStyleOverrides(&st::contactsWithStories);
|
||||||
const auto raw = controller.get();
|
const auto raw = controller.get();
|
||||||
auto init = [=](not_null<PeerListBox*> box) {
|
auto init = [=](not_null<PeerListBox*> box) {
|
||||||
using namespace Dialogs::Stories;
|
using namespace Dialogs::Stories;
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
List *stories = nullptr;
|
|
||||||
QPointer<::Ui::IconButton> toggleSort;
|
QPointer<::Ui::IconButton> toggleSort;
|
||||||
Mode mode = ContactsBoxController::SortMode::Online;
|
rpl::variable<Mode> mode = Mode::Online;
|
||||||
::Ui::Animations::Simple scrollAnimation;
|
::Ui::Animations::Simple scrollAnimation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,109 +368,20 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
|
||||||
tr::lng_profile_add_contact(),
|
tr::lng_profile_add_contact(),
|
||||||
[=] { sessionController->showAddContact(); });
|
[=] { sessionController->showAddContact(); });
|
||||||
state->toggleSort = box->addTopButton(st::contactsSortButton, [=] {
|
state->toggleSort = box->addTopButton(st::contactsSortButton, [=] {
|
||||||
const auto online = (state->mode == Mode::Online);
|
const auto online = (state->mode.current() == Mode::Online);
|
||||||
state->mode = online ? Mode::Alphabet : Mode::Online;
|
const auto mode = online ? Mode::Alphabet : Mode::Online;
|
||||||
raw->setSortMode(state->mode);
|
state->mode = mode;
|
||||||
|
raw->setSortMode(mode);
|
||||||
state->toggleSort->setIconOverride(
|
state->toggleSort->setIconOverride(
|
||||||
online ? &st::contactsSortOnlineIcon : nullptr,
|
online ? &st::contactsSortOnlineIcon : nullptr,
|
||||||
online ? &st::contactsSortOnlineIconOver : nullptr);
|
online ? &st::contactsSortOnlineIconOver : nullptr);
|
||||||
});
|
});
|
||||||
raw->setSortMode(Mode::Online);
|
raw->setSortMode(Mode::Online);
|
||||||
|
|
||||||
auto list = object_ptr<List>(
|
box->peerListSetAboveWidget(PrepareHiddenStoriesList(
|
||||||
box,
|
box,
|
||||||
st::dialogsStoriesList,
|
sessionController,
|
||||||
ContentForSession(
|
state->mode.value()));
|
||||||
&sessionController->session(),
|
|
||||||
Data::StorySourcesList::Hidden),
|
|
||||||
[=] { return state->stories->height() - box->scrollTop(); });
|
|
||||||
const auto raw = state->stories = list.data();
|
|
||||||
box->peerListSetAboveWidget(object_ptr<::Ui::PaddingWrap<>>(
|
|
||||||
box,
|
|
||||||
std::move(list),
|
|
||||||
style::margins(0, st::membersMarginTop, 0, 0)));
|
|
||||||
|
|
||||||
raw->clicks(
|
|
||||||
) | rpl::start_with_next([=](uint64 id) {
|
|
||||||
sessionController->openPeerStories(
|
|
||||||
PeerId(int64(id)),
|
|
||||||
Data::StorySourcesList::Hidden);
|
|
||||||
}, raw->lifetime());
|
|
||||||
|
|
||||||
raw->showMenuRequests(
|
|
||||||
) | rpl::start_with_next([=](const ShowMenuRequest &request) {
|
|
||||||
FillSourceMenu(sessionController, request);
|
|
||||||
}, raw->lifetime());
|
|
||||||
|
|
||||||
raw->loadMoreRequests(
|
|
||||||
) | rpl::start_with_next([=] {
|
|
||||||
stories->loadMore(Data::StorySourcesList::Hidden);
|
|
||||||
}, raw->lifetime());
|
|
||||||
|
|
||||||
const auto defaultScrollTop = [=] {
|
|
||||||
return std::max(raw->height() - st::dialogsStories.height, 0);
|
|
||||||
};
|
|
||||||
const auto scrollToDefault = [=](bool verytop) {
|
|
||||||
if (state->scrollAnimation.animating()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (verytop) {
|
|
||||||
//_scroll->verticalScrollBar()->setMinimum(0);
|
|
||||||
}
|
|
||||||
state->scrollAnimation.stop();
|
|
||||||
auto scrollTop = box->scrollTop();
|
|
||||||
const auto scrollTo = verytop ? 0 : defaultScrollTop();
|
|
||||||
if (scrollTop == scrollTo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto maxAnimatedDelta = box->height();
|
|
||||||
if (scrollTo + maxAnimatedDelta < scrollTop) {
|
|
||||||
scrollTop = scrollTo + maxAnimatedDelta;
|
|
||||||
box->scrollToY(scrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto scroll = [=] {
|
|
||||||
const auto animated = qRound(
|
|
||||||
state->scrollAnimation.value(scrollTo));
|
|
||||||
const auto animatedDelta = animated - scrollTo;
|
|
||||||
const auto realDelta = box->scrollTop() - scrollTo;
|
|
||||||
if (realDelta * animatedDelta < 0) {
|
|
||||||
// We scrolled manually to the other side of target 'scrollTo'.
|
|
||||||
state->scrollAnimation.stop();
|
|
||||||
} else if (std::abs(realDelta) > std::abs(animatedDelta)) {
|
|
||||||
// We scroll by animation only if it gets us closer to target.
|
|
||||||
box->scrollToY(animated);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
state->scrollAnimation.start(
|
|
||||||
scroll,
|
|
||||||
scrollTop,
|
|
||||||
scrollTo,
|
|
||||||
st::slideDuration,
|
|
||||||
anim::sineInOut);
|
|
||||||
};
|
|
||||||
const auto top = box->scrollTop();
|
|
||||||
raw->toggleExpandedRequests(
|
|
||||||
) | rpl::start_with_next([=](bool expanded) {
|
|
||||||
if (expanded || box->scrollTop() < defaultScrollTop()) {
|
|
||||||
scrollToDefault(expanded);
|
|
||||||
}
|
|
||||||
}, raw->lifetime());
|
|
||||||
|
|
||||||
raw->heightValue(
|
|
||||||
) | rpl::filter([=] {
|
|
||||||
return (box->scrollHeight() > 0)
|
|
||||||
&& (defaultScrollTop() > box->scrollTop());
|
|
||||||
}) | rpl::start_with_next([=] {
|
|
||||||
//refreshForDefaultScroll();
|
|
||||||
box->scrollToY(defaultScrollTop());
|
|
||||||
}, raw->lifetime());
|
|
||||||
|
|
||||||
stories->incrementPreloadingHiddenSources();
|
|
||||||
raw->lifetime().add([=] {
|
|
||||||
stories->decrementPreloadingHiddenSources();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
return Box<PeerListBox>(std::move(controller), std::move(init));
|
return Box<PeerListBox>(std::move(controller), std::move(init));
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ constexpr auto kSavedFirstPerPage = 30;
|
||||||
constexpr auto kSavedPerPage = 100;
|
constexpr auto kSavedPerPage = 100;
|
||||||
constexpr auto kMaxPreloadSources = 10;
|
constexpr auto kMaxPreloadSources = 10;
|
||||||
constexpr auto kStillPreloadFromFirst = 3;
|
constexpr auto kStillPreloadFromFirst = 3;
|
||||||
|
constexpr auto kMaxSegmentsCount = 180;
|
||||||
|
|
||||||
using UpdateFlag = StoryUpdate::Flag;
|
using UpdateFlag = StoryUpdate::Flag;
|
||||||
|
|
||||||
|
@ -74,13 +75,15 @@ StoriesSourceInfo StoriesSource::info() const {
|
||||||
return {
|
return {
|
||||||
.id = user->id,
|
.id = user->id,
|
||||||
.last = ids.empty() ? 0 : ids.back().date,
|
.last = ids.empty() ? 0 : ids.back().date,
|
||||||
.unread = unread(),
|
.count = std::min(int(ids.size()), kMaxSegmentsCount),
|
||||||
.premium = user->isPremium(),
|
.unreadCount = std::min(unreadCount(), kMaxSegmentsCount),
|
||||||
|
.premium = user->isPremium() ? 1 : 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StoriesSource::unread() const {
|
int StoriesSource::unreadCount() const {
|
||||||
return !ids.empty() && readTill < ids.back().id;
|
const auto i = ids.lower_bound(StoryIdDates{ .id = readTill + 1 });
|
||||||
|
return int(end(ids) - i);
|
||||||
}
|
}
|
||||||
|
|
||||||
StoryIdDates StoriesSource::toOpen() const {
|
StoryIdDates StoriesSource::toOpen() const {
|
||||||
|
@ -724,7 +727,7 @@ void Stories::sort(StorySourcesList list) {
|
||||||
const auto key = int64(info.last)
|
const auto key = int64(info.last)
|
||||||
+ (info.premium ? (int64(1) << 47) : 0)
|
+ (info.premium ? (int64(1) << 47) : 0)
|
||||||
+ ((info.id == changelogSenderId) ? (int64(1) << 47) : 0)
|
+ ((info.id == changelogSenderId) ? (int64(1) << 47) : 0)
|
||||||
+ (info.unread ? (int64(1) << 49) : 0)
|
+ ((info.unreadCount > 0) ? (int64(1) << 49) : 0)
|
||||||
+ ((info.id == self) ? (int64(1) << 50) : 0);
|
+ ((info.id == self) ? (int64(1) << 50) : 0);
|
||||||
return std::make_pair(key, info.id);
|
return std::make_pair(key, info.id);
|
||||||
};
|
};
|
||||||
|
@ -895,10 +898,10 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
|
||||||
sendMarkAsReadRequests();
|
sendMarkAsReadRequests();
|
||||||
}
|
}
|
||||||
_markReadPending.emplace(id.peer);
|
_markReadPending.emplace(id.peer);
|
||||||
const auto wasUnread = i->second.unread();
|
const auto wasUnreadCount = i->second.unreadCount();
|
||||||
i->second.readTill = id.story;
|
i->second.readTill = id.story;
|
||||||
const auto nowUnread = i->second.unread();
|
const auto nowUnreadCount = i->second.unreadCount();
|
||||||
if (wasUnread != nowUnread) {
|
if (wasUnreadCount != nowUnreadCount) {
|
||||||
const auto refreshInList = [&](StorySourcesList list) {
|
const auto refreshInList = [&](StorySourcesList list) {
|
||||||
auto &sources = _sources[static_cast<int>(list)];
|
auto &sources = _sources[static_cast<int>(list)];
|
||||||
const auto i = ranges::find(
|
const auto i = ranges::find(
|
||||||
|
@ -906,7 +909,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) {
|
||||||
id.peer,
|
id.peer,
|
||||||
&StoriesSourceInfo::id);
|
&StoriesSourceInfo::id);
|
||||||
if (i != end(sources)) {
|
if (i != end(sources)) {
|
||||||
i->unread = nowUnread;
|
i->unreadCount = nowUnreadCount;
|
||||||
sort(list);
|
sort(list);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,8 +41,9 @@ struct StoriesIds {
|
||||||
struct StoriesSourceInfo {
|
struct StoriesSourceInfo {
|
||||||
PeerId id = 0;
|
PeerId id = 0;
|
||||||
TimeId last = 0;
|
TimeId last = 0;
|
||||||
bool unread = false;
|
int count : 15 = 0;
|
||||||
bool premium = false;
|
int unreadCount : 15 = 0;
|
||||||
|
int premium : 1 = 0;
|
||||||
|
|
||||||
friend inline bool operator==(
|
friend inline bool operator==(
|
||||||
StoriesSourceInfo,
|
StoriesSourceInfo,
|
||||||
|
@ -56,7 +57,7 @@ struct StoriesSource {
|
||||||
bool hidden = false;
|
bool hidden = false;
|
||||||
|
|
||||||
[[nodiscard]] StoriesSourceInfo info() const;
|
[[nodiscard]] StoriesSourceInfo info() const;
|
||||||
[[nodiscard]] bool unread() const;
|
[[nodiscard]] int unreadCount() const;
|
||||||
[[nodiscard]] StoryIdDates toOpen() const;
|
[[nodiscard]] StoryIdDates toOpen() const;
|
||||||
|
|
||||||
friend inline bool operator==(StoriesSource, StoriesSource) = default;
|
friend inline bool operator==(StoriesSource, StoriesSource) = default;
|
||||||
|
|
|
@ -351,8 +351,9 @@ Content State::next() {
|
||||||
? tr::lng_stories_my_name(tr::now)
|
? tr::lng_stories_my_name(tr::now)
|
||||||
: user->shortName()),
|
: user->shortName()),
|
||||||
.thumbnail = std::move(userpic),
|
.thumbnail = std::move(userpic),
|
||||||
.unread = info.unread,
|
.count = info.count,
|
||||||
.skipSmall = user->isSelf(),
|
.unreadCount = info.unreadCount,
|
||||||
|
.skipSmall = user->isSelf() ? 1 : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -423,7 +424,8 @@ rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
|
||||||
result.elements.push_back({
|
result.elements.push_back({
|
||||||
.id = uint64(id),
|
.id = uint64(id),
|
||||||
.thumbnail = MakeStoryThumbnail(*maybe),
|
.thumbnail = MakeStoryThumbnail(*maybe),
|
||||||
.unread = (id > readTill),
|
.count = 1,
|
||||||
|
.unreadCount = (id > readTill) ? 1 : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (maybe.error() == Data::NoStory::Unknown) {
|
} else if (maybe.error() == Data::NoStory::Unknown) {
|
||||||
|
|
|
@ -108,7 +108,8 @@ void List::showContent(Content &&content) {
|
||||||
item.element.name = element.name;
|
item.element.name = element.name;
|
||||||
item.nameCache = QImage();
|
item.nameCache = QImage();
|
||||||
}
|
}
|
||||||
item.element.unread = element.unread;
|
item.element.count = element.count;
|
||||||
|
item.element.unreadCount = element.unreadCount;
|
||||||
} else {
|
} else {
|
||||||
_data.items.emplace_back(Item{ .element = element });
|
_data.items.emplace_back(Item{ .element = element });
|
||||||
}
|
}
|
||||||
|
@ -129,7 +130,7 @@ List::Summaries List::ComposeSummaries(Data &data) {
|
||||||
auto unreadInFirst = 0;
|
auto unreadInFirst = 0;
|
||||||
auto unreadTotal = 0;
|
auto unreadTotal = 0;
|
||||||
for (auto i = skip; i != total; ++i) {
|
for (auto i = skip; i != total; ++i) {
|
||||||
if (data.items[i].element.unread) {
|
if (data.items[i].element.unreadCount > 0) {
|
||||||
++unreadTotal;
|
++unreadTotal;
|
||||||
if (i < skip + kSmallThumbsShown) {
|
if (i < skip + kSmallThumbsShown) {
|
||||||
++unreadInFirst;
|
++unreadInFirst;
|
||||||
|
@ -162,7 +163,7 @@ List::Summaries List::ComposeSummaries(Data &data) {
|
||||||
}
|
}
|
||||||
if (unreadInFirst > 0 && unreadInFirst == unreadTotal) {
|
if (unreadInFirst > 0 && unreadInFirst == unreadTotal) {
|
||||||
for (auto i = skip; i != total; ++i) {
|
for (auto i = skip; i != total; ++i) {
|
||||||
if (data.items[i].element.unread) {
|
if (data.items[i].element.unreadCount > 0) {
|
||||||
append(result.unreadNames.string, i, !--unreadTotal);
|
append(result.unreadNames.string, i, !--unreadTotal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -449,8 +450,8 @@ void List::paintEvent(QPaintEvent *e) {
|
||||||
return Single{ x, indexSmall, small, indexFull, full, y };
|
return Single{ x, indexSmall, small, indexFull, full, y };
|
||||||
};
|
};
|
||||||
const auto hasUnread = [&](const Single &single) {
|
const auto hasUnread = [&](const Single &single) {
|
||||||
return (single.itemSmall && single.itemSmall->element.unread)
|
return (single.itemSmall && single.itemSmall->element.unreadCount)
|
||||||
|| (single.itemFull && single.itemFull->element.unread);
|
|| (single.itemFull && single.itemFull->element.unreadCount);
|
||||||
};
|
};
|
||||||
const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
|
const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
|
||||||
auto nextGradientPainted = false;
|
auto nextGradientPainted = false;
|
||||||
|
@ -513,8 +514,8 @@ void List::paintEvent(QPaintEvent *e) {
|
||||||
photo);
|
photo);
|
||||||
const auto small = single.itemSmall;
|
const auto small = single.itemSmall;
|
||||||
const auto itemFull = single.itemFull;
|
const auto itemFull = single.itemFull;
|
||||||
const auto smallUnread = small && small->element.unread;
|
const auto smallUnread = small && small->element.unreadCount;
|
||||||
const auto fullUnread = itemFull && itemFull->element.unread;
|
const auto fullUnread = itemFull && itemFull->element.unreadCount;
|
||||||
const auto unreadOpacity = (smallUnread && fullUnread)
|
const auto unreadOpacity = (smallUnread && fullUnread)
|
||||||
? 1.
|
? 1.
|
||||||
: smallUnread
|
: smallUnread
|
||||||
|
@ -550,8 +551,8 @@ void List::paintEvent(QPaintEvent *e) {
|
||||||
photo);
|
photo);
|
||||||
const auto small = single.itemSmall;
|
const auto small = single.itemSmall;
|
||||||
const auto itemFull = single.itemFull;
|
const auto itemFull = single.itemFull;
|
||||||
const auto smallUnread = small && small->element.unread;
|
const auto smallUnread = small && small->element.unreadCount;
|
||||||
const auto fullUnread = itemFull && itemFull->element.unread;
|
const auto fullUnread = itemFull && itemFull->element.unreadCount;
|
||||||
|
|
||||||
// White circle with possible read gray line.
|
// White circle with possible read gray line.
|
||||||
const auto hasReadLine = (itemFull && !fullUnread);
|
const auto hasReadLine = (itemFull && !fullUnread);
|
||||||
|
|
|
@ -36,8 +36,9 @@ struct Element {
|
||||||
uint64 id = 0;
|
uint64 id = 0;
|
||||||
QString name;
|
QString name;
|
||||||
std::shared_ptr<Thumbnail> thumbnail;
|
std::shared_ptr<Thumbnail> thumbnail;
|
||||||
bool unread : 1 = false;
|
int count : 15 = 0;
|
||||||
bool skipSmall : 1 = false;
|
int unreadCount : 15 = 0;
|
||||||
|
int skipSmall : 1 = 0;
|
||||||
|
|
||||||
friend inline bool operator==(
|
friend inline bool operator==(
|
||||||
const Element &a,
|
const Element &a,
|
||||||
|
|
|
@ -160,7 +160,7 @@ void InnerWidget::createButtons() {
|
||||||
self
|
self
|
||||||
) | rpl::map([=](Content &&content) {
|
) | rpl::map([=](Content &&content) {
|
||||||
for (auto &element : content.elements) {
|
for (auto &element : content.elements) {
|
||||||
element.unread = false;
|
element.unreadCount = 0;
|
||||||
}
|
}
|
||||||
return std::move(content);
|
return std::move(content);
|
||||||
}) | rpl::start_spawning(recentWrap->lifetime());
|
}) | rpl::start_spawning(recentWrap->lifetime());
|
||||||
|
|
|
@ -389,26 +389,49 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectionLevel > 0) {
|
if (selectionLevel > 0) {
|
||||||
const auto radius = _roundingRadius
|
|
||||||
? _roundingRadius(_st.imageRadius * 2)
|
|
||||||
: std::optional<int>();
|
|
||||||
PainterHighQualityEnabler hq(p);
|
PainterHighQualityEnabler hq(p);
|
||||||
p.setOpacity(std::clamp(selectionLevel, 0., 1.));
|
p.setOpacity(std::clamp(selectionLevel, 0., 1.));
|
||||||
p.setBrush(Qt::NoBrush);
|
p.setBrush(Qt::NoBrush);
|
||||||
const auto pen = QPen(
|
const auto segments = int(_segments.size());
|
||||||
_fgOverride ? (*_fgOverride) : _st.selectFg->b,
|
|
||||||
_st.selectWidth);
|
|
||||||
p.setPen(pen);
|
|
||||||
const auto rect = style::rtlrect(
|
const auto rect = style::rtlrect(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
_st.imageRadius * 2,
|
_st.imageRadius * 2,
|
||||||
_st.imageRadius * 2,
|
_st.imageRadius * 2,
|
||||||
outerWidth);
|
outerWidth);
|
||||||
if (!radius) {
|
if (segments < 2) {
|
||||||
p.drawEllipse(rect);
|
const auto radius = _roundingRadius
|
||||||
|
? _roundingRadius(_st.imageRadius * 2)
|
||||||
|
: std::optional<int>();
|
||||||
|
const auto pen = QPen(
|
||||||
|
segments ? _segments.front().brush : _st.selectFg->b,
|
||||||
|
segments ? _segments.front().width : _st.selectWidth);
|
||||||
|
p.setPen(pen);
|
||||||
|
if (!radius) {
|
||||||
|
p.drawEllipse(rect);
|
||||||
|
} else {
|
||||||
|
p.drawRoundedRect(rect, *radius, *radius);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
p.drawRoundedRect(rect, *radius, *radius);
|
const auto small = 160;
|
||||||
|
const auto full = arc::kFullLength;
|
||||||
|
const auto separator = (full > 2 * small * segments)
|
||||||
|
? small
|
||||||
|
: full / (segments * 2);
|
||||||
|
const auto left = full - (separator * segments);
|
||||||
|
const auto length = left / float64(segments);
|
||||||
|
auto start = 0. + (arc::kQuarterLength + (separator / 2));
|
||||||
|
for (const auto &segment : ranges::views::reverse(_segments)) {
|
||||||
|
p.setPen(QPen(
|
||||||
|
segment.brush,
|
||||||
|
segment.width,
|
||||||
|
Qt::SolidLine,
|
||||||
|
Qt::RoundCap));
|
||||||
|
const auto from = int(base::SafeRound(start));
|
||||||
|
const auto till = int(base::SafeRound(start + length));
|
||||||
|
p.drawArc(rect, from, till - from);
|
||||||
|
start += length + separator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p.setOpacity(1.);
|
p.setOpacity(1.);
|
||||||
}
|
}
|
||||||
|
@ -474,7 +497,18 @@ void RoundImageCheckbox::prepareWideCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoundImageCheckbox::setColorOverride(std::optional<QBrush> fg) {
|
void RoundImageCheckbox::setColorOverride(std::optional<QBrush> fg) {
|
||||||
_fgOverride = fg;
|
if (fg) {
|
||||||
|
setCustomizedSegments({
|
||||||
|
{ .brush = *fg, .width = float64(_st.selectWidth) }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCustomizedSegments({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoundImageCheckbox::setCustomizedSegments(
|
||||||
|
std::vector<Segment> segments) {
|
||||||
|
_segments = std::move(segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
|
@ -45,8 +45,14 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RoundImageCheckboxSegment {
|
||||||
|
QBrush brush;
|
||||||
|
float64 width = 0.;
|
||||||
|
};
|
||||||
|
|
||||||
class RoundImageCheckbox {
|
class RoundImageCheckbox {
|
||||||
public:
|
public:
|
||||||
|
using Segment = RoundImageCheckboxSegment;
|
||||||
using PaintRoundImage = Fn<void(Painter &p, int x, int y, int outerWidth, int size)>;
|
using PaintRoundImage = Fn<void(Painter &p, int x, int y, int outerWidth, int size)>;
|
||||||
RoundImageCheckbox(
|
RoundImageCheckbox(
|
||||||
const style::RoundImageCheckbox &st,
|
const style::RoundImageCheckbox &st,
|
||||||
|
@ -58,6 +64,7 @@ public:
|
||||||
float64 checkedAnimationRatio() const;
|
float64 checkedAnimationRatio() const;
|
||||||
|
|
||||||
void setColorOverride(std::optional<QBrush> fg);
|
void setColorOverride(std::optional<QBrush> fg);
|
||||||
|
void setCustomizedSegments(std::vector<Segment> segments);
|
||||||
|
|
||||||
bool checked() const {
|
bool checked() const {
|
||||||
return _check.checked();
|
return _check.checked();
|
||||||
|
@ -83,7 +90,8 @@ private:
|
||||||
|
|
||||||
RoundCheckbox _check;
|
RoundCheckbox _check;
|
||||||
|
|
||||||
std::optional<QBrush> _fgOverride;
|
//std::optional<QBrush> _fgOverride;
|
||||||
|
std::vector<Segment> _segments;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue