Display jump to last topic message bubble.

This commit is contained in:
John Preston 2022-11-13 23:38:18 +04:00
parent 97356032ac
commit ede34578da
18 changed files with 486 additions and 320 deletions

View File

@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "ui/painter.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "lang/lang_keys.h"
#include "storage/storage_facade.h"
#include "core/application.h"
@ -33,9 +35,67 @@ namespace {
constexpr auto kLoadedChatsMinCount = 20;
constexpr auto kShowChatNamesCount = 8;
[[nodiscard]] TextWithEntities ComposeFolderListEntryText(
not_null<Folder*> folder) {
const auto &list = folder->lastHistories();
if (list.empty()) {
return {};
}
const auto count = std::max(
int(list.size()),
folder->chatsList()->fullSize().current());
const auto throwAwayLastName = (list.size() > 1)
&& (count == list.size() + 1);
auto &&peers = ranges::views::all(
list
) | ranges::views::take(
list.size() - (throwAwayLastName ? 1 : 0)
);
const auto wrapName = [](not_null<History*> history) {
const auto name = history->peer->name();
return TextWithEntities{
.text = name,
.entities = (history->chatListBadgesState().unread
? EntitiesInText{
{ EntityType::Semibold, 0, int(name.size()), QString() },
{ EntityType::PlainLink, 0, int(name.size()), QString() },
}
: EntitiesInText{}),
};
};
const auto shown = int(peers.size());
const auto accumulated = [&] {
Expects(shown > 0);
auto i = peers.begin();
auto result = wrapName(*i);
for (++i; i != peers.end(); ++i) {
result = tr::lng_archived_last_list(
tr::now,
lt_accumulated,
result,
lt_chat,
wrapName(*i),
Ui::Text::WithEntities);
}
return result;
}();
return (shown < count)
? tr::lng_archived_last(
tr::now,
lt_count,
(count - shown),
lt_chats,
accumulated,
Ui::Text::WithEntities)
: accumulated;
}
} // namespace
Folder::Folder(not_null<Data::Session*> owner, FolderId id)
Folder::Folder(not_null<Session*> owner, FolderId id)
: Entry(owner, Type::Folder)
, _id(id)
, _chatsList(
@ -76,29 +136,6 @@ FolderId Folder::id() const {
void Folder::indexNameParts() {
// We don't want archive to be filtered in the chats list.
//_nameWords.clear();
//_nameFirstLetters.clear();
//auto toIndexList = QStringList();
//auto appendToIndex = [&](const QString &value) {
// if (!value.isEmpty()) {
// toIndexList.push_back(TextUtilities::RemoveAccents(value));
// }
//};
//appendToIndex(_name);
//const auto appendTranslit = !toIndexList.isEmpty()
// && cRussianLetters().match(toIndexList.front()).hasMatch();
//if (appendTranslit) {
// appendToIndex(translitRusEng(toIndexList.front()));
//}
//auto toIndex = toIndexList.join(' ');
//toIndex += ' ' + rusKeyboardLayoutSwitch(toIndex);
//const auto namesList = TextUtilities::PrepareSearchWords(toIndex);
//for (const auto &name : namesList) {
// _nameWords.insert(name);
// _nameFirstLetters.insert(name[0]);
//}
}
void Folder::registerOne(not_null<History*> history) {
@ -110,7 +147,6 @@ void Folder::registerOne(not_null<History*> history) {
} else {
updateChatListEntry();
}
applyChatListMessage(history->chatListMessage());
reorderLastHistories();
}
@ -118,9 +154,6 @@ void Folder::unregisterOne(not_null<History*> history) {
if (_chatsList.empty()) {
updateChatListExistence();
}
if (_chatListMessage && _chatListMessage->history() == history) {
computeChatListMessage();
}
reorderLastHistories();
}
@ -129,47 +162,11 @@ int Folder::chatListNameVersion() const {
}
void Folder::oneListMessageChanged(HistoryItem *from, HistoryItem *to) {
if (!applyChatListMessage(to) && _chatListMessage == from) {
computeChatListMessage();
}
if (from || to) {
reorderLastHistories();
}
}
bool Folder::applyChatListMessage(HistoryItem *item) {
if (!item) {
return false;
} else if (_chatListMessage
&& _chatListMessage->date() >= item->date()) {
return false;
}
_chatListMessage = item;
updateChatListEntry();
return true;
}
void Folder::computeChatListMessage() {
auto &&items = ranges::views::all(
*_chatsList.indexed()
) | ranges::views::filter([](not_null<Dialogs::Row*> row) {
return row->entry()->chatListMessage() != nullptr;
});
const auto chatListDate = [](not_null<Dialogs::Row*> row) {
return row->entry()->chatListMessage()->date();
};
const auto top = ranges::max_element(
items,
ranges::less(),
chatListDate);
if (top == items.end()) {
_chatListMessage = nullptr;
} else {
_chatListMessage = (*top)->entry()->chatListMessage();
}
updateChatListEntry();
}
void Folder::reorderLastHistories() {
// We want first kShowChatNamesCount histories, by last message date.
const auto pred = [](not_null<History*> a, not_null<History*> b) {
@ -219,7 +216,7 @@ void Folder::loadUserpic() {
void Folder::paintUserpic(
Painter &p,
std::shared_ptr<Data::CloudImageView> &view,
std::shared_ptr<CloudImageView> &view,
const Dialogs::Ui::PaintContext &context) const {
paintUserpic(
p,
@ -288,8 +285,16 @@ const std::vector<not_null<History*>> &Folder::lastHistories() const {
return _lastHistories;
}
uint32 Folder::chatListViewVersion() const {
return _chatListViewVersion;
void Folder::validateListEntryCache() {
if (_listEntryCacheVersion == _chatListViewVersion) {
return;
}
_listEntryCacheVersion = _chatListViewVersion;
_listEntryCache.setMarkedText(
st::dialogsTextStyle,
ComposeFolderListEntryText(this),
// Use rich options as long as the entry text does not have user text.
Ui::ItemTextDefaultOptions());
}
void Folder::requestChatListMessage() {
@ -299,7 +304,7 @@ void Folder::requestChatListMessage() {
}
TimeId Folder::adjustedChatListTimeId() const {
return _chatListMessage ? _chatListMessage->date() : chatListTimeId();
return chatListTimeId();
}
void Folder::applyDialog(const MTPDdialogFolder &data) {
@ -350,7 +355,7 @@ Dialogs::BadgesState Folder::chatListBadgesState() const {
}
HistoryItem *Folder::chatListMessage() const {
return _chatListMessage;
return nullptr;
}
bool Folder::chatListMessageKnown() const {

View File

@ -69,12 +69,13 @@ public:
const style::color &overrideFg) const;
const std::vector<not_null<History*>> &lastHistories() const;
uint32 chatListViewVersion() const;
void validateListEntryCache();
[[nodiscard]] const Ui::Text::String &listEntryCache() const {
return _listEntryCache;
}
private:
void indexNameParts();
bool applyChatListMessage(HistoryItem *item);
void computeChatListMessage();
int chatListNameVersion() const override;
@ -96,8 +97,10 @@ private:
base::flat_set<QChar> _nameFirstLetters;
std::vector<not_null<History*>> _lastHistories;
HistoryItem *_chatListMessage = nullptr;
uint32 _chatListViewVersion = 0;
Ui::Text::String _listEntryCache;
int _listEntryCacheVersion = 0;
int _chatListViewVersion = 0;
//rpl::variable<MessagePosition> _unreadPosition;
rpl::lifetime _lifetime;

View File

@ -625,6 +625,10 @@ TextWithEntities ForumTopic::titleWithIcon() const {
return ForumTopicIconWithTitle(_iconId, _title);
}
int ForumTopic::titleVersion() const {
return _titleVersion;
}
void ForumTopic::applyTitle(const QString &title) {
if (_title == title) {
return;
@ -647,6 +651,7 @@ void ForumTopic::applyIconId(DocumentId iconId) {
return;
}
_iconId = iconId;
++_titleVersion;
_icon = iconId
? std::make_unique<Ui::Text::LimitedLoopsEmoji>(
owner().customEmojiManager().create(

View File

@ -114,6 +114,7 @@ public:
[[nodiscard]] QString title() const;
[[nodiscard]] TextWithEntities titleWithIcon() const;
[[nodiscard]] int titleVersion() const;
void applyTitle(const QString &title);
[[nodiscard]] DocumentId iconId() const;
void applyIconId(DocumentId iconId);

View File

@ -18,6 +18,7 @@ DialogRow {
textLeft: pixels;
textTop: pixels;
topicsSkip: pixels;
topicsSkipBig: pixels;
topicsHeight: pixels;
}
@ -77,9 +78,18 @@ forumDialogRow: DialogRow(defaultDialogRow) {
height: 80px;
textTop: 32px;
topicsSkip: 8px;
topicsHeight: 20px;
topicsSkipBig: 14px;
topicsHeight: 21px;
}
forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }};
forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }};
forumDialogJumpArrowSkip: 8px;
forumDialogJumpArrowLeft: 3px;
forumDialogJumpArrowTop: 3px;
forumDialogJumpPadding: margins(8px, 3px, 8px, 3px);
forumDialogJumpRadius: 11px;
dialogsOnlineBadgeStroke: 2px;
dialogsOnlineBadgeSize: 10px;
dialogsOnlineBadgeSkip: point(0px, 2px);
@ -483,7 +493,3 @@ chooseTopicListItem: PeerListItem(defaultPeerListItem) {
chooseTopicList: PeerList(defaultPeerList) {
item: chooseTopicListItem;
}
dialogsTopicArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgService }};
dialogsTopicArrowSkip: 13px;
dialogsTopicArrowTop: 4px;

View File

@ -514,22 +514,43 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
Window::GifPauseReason::Any);
auto fullWidth = width();
auto dialogsClip = r;
auto ms = crl::now();
const auto ms = crl::now();
const auto paintRow = [&](
not_null<Row*> row,
bool selected,
bool mayBeActive) {
const auto key = row->key();
const auto active = mayBeActive && (activeEntry.key == key);
const auto forum = key.history()
&& key.history()->peer->isForum();
if (forum && !_topicJumpCache) {
_topicJumpCache = std::make_unique<Ui::TopicJumpCache>();
}
Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), {
.st = (forum ? &st::forumDialogRow : _st.get()),
.topicJumpCache = _topicJumpCache.get(),
.folder = _openedFolder,
.forum = _openedForum,
.filter = _filterId,
.now = ms,
.width = fullWidth,
.active = active,
.selected = (_menuRow.key
? (row->key() == _menuRow.key)
: selected),
.paused = videoPaused,
.narrow = (fullWidth < st::columnMinimalWidthLeft),
});
};
if (_state == WidgetState::Default) {
paintCollapsedRows(p, r);
const auto &list = _shownList->all();
const auto shownBottom = _shownList->height() - skipTopHeight();
const auto active = activeEntry.key;
const auto selected = _menuRow.key
? _menuRow.key
: (isPressed()
? (_pressed
? _pressed->key()
: Key())
: (_selected
? _selected->key()
: Key()));
const auto selected = isPressed()
? (_pressed ? _pressed->key() : Key())
: (_selected ? _selected->key() : Key());
if (shownBottom) {
const auto skip = dialogsOffset();
const auto promoted = fixedOnTopCount();
@ -559,23 +580,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
if (xadd || yadd) {
p.translate(xadd, yadd);
}
const auto key = row->key();
const auto isActive = (key == active);
const auto isSelected = (key == selected);
const auto isForum = key.history()
&& key.history()->peer->isForum();
Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), {
.st = (isForum ? &st::forumDialogRow : _st.get()),
.folder = _openedFolder,
.forum = _openedForum,
.filter = _filterId,
.now = ms,
.width = fullWidth,
.active = isActive,
.selected = isSelected,
.paused = videoPaused,
.narrow = (fullWidth < st::columnMinimalWidthLeft),
});
paintRow(row, (row->key() == selected), true);
if (xadd || yadd) {
p.translate(-xadd, -yadd);
}
@ -677,29 +682,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
int(_filterResults.size()));
p.translate(0, filteredHeight(from));
for (; from < to; ++from) {
const auto selected = isPressed()
? (from == _filteredPressed)
: (from == _filteredSelected);
const auto row = _filterResults[from].row;
const auto key = row->key();
const auto active = (activeEntry.key == key)
&& !activeEntry.fullId;
const auto selected = _menuRow.key
? (key == _menuRow.key)
: (from == (isPressed()
? _filteredPressed
: _filteredSelected));
const auto isForum = key.history()
&& key.history()->peer->isForum();
Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), {
.st = (isForum ? &st::forumDialogRow : _st.get()),
.folder = _openedFolder,
.forum = _openedForum,
.filter = _filterId,
.now = ms,
.width = fullWidth,
.active = active,
.selected = selected,
.paused = videoPaused,
.narrow = (fullWidth < st::columnMinimalWidthLeft),
});
paintRow(row, selected, !activeEntry.fullId);
p.translate(0, row->height());
}
}

View File

@ -49,6 +49,7 @@ namespace Dialogs::Ui {
using namespace ::Ui;
class VideoUserpic;
struct PaintContext;
struct TopicJumpCache;
} // namespace Dialogs::Ui
namespace Dialogs {
@ -397,6 +398,7 @@ private:
std::vector<std::unique_ptr<CollapsedRow>> _collapsedRows;
not_null<const style::DialogRow*> _st;
mutable std::unique_ptr<Ui::TopicJumpCache> _topicJumpCache;
int _collapsedSelected = -1;
int _collapsedPressed = -1;
bool _skipTopDialog = false;

View File

@ -26,67 +26,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_dialogs.h"
namespace Dialogs {
namespace {
[[nodiscard]] TextWithEntities ComposeFolderListEntryText(
not_null<Data::Folder*> folder) {
const auto &list = folder->lastHistories();
if (list.empty()) {
return {};
}
const auto count = std::max(
int(list.size()),
folder->chatsList()->fullSize().current());
const auto throwAwayLastName = (list.size() > 1)
&& (count == list.size() + 1);
auto &&peers = ranges::views::all(
list
) | ranges::views::take(
list.size() - (throwAwayLastName ? 1 : 0)
);
const auto wrapName = [](not_null<History*> history) {
const auto name = history->peer->name();
return TextWithEntities{
.text = name,
.entities = (history->chatListBadgesState().unread
? EntitiesInText{
{ EntityType::Semibold, 0, int(name.size()), QString() },
{ EntityType::PlainLink, 0, int(name.size()), QString() },
}
: EntitiesInText{}),
};
};
const auto shown = int(peers.size());
const auto accumulated = [&] {
Expects(shown > 0);
auto i = peers.begin();
auto result = wrapName(*i);
for (++i; i != peers.end(); ++i) {
result = tr::lng_archived_last_list(
tr::now,
lt_accumulated,
result,
lt_chat,
wrapName(*i),
Ui::Text::WithEntities);
}
return result;
}();
return (shown < count)
? tr::lng_archived_last(
tr::now,
lt_count,
(count - shown),
lt_chats,
accumulated,
Ui::Text::WithEntities)
: accumulated;
}
} // namespace
BasicRow::BasicRow() = default;
BasicRow::~BasicRow() = default;
@ -160,31 +99,13 @@ uint64 Row::sortKey(FilterId filterId) const {
return _id.entry()->sortKeyInChatList(filterId);
}
void Row::validateListEntryCache() const {
const auto folder = _id.folder();
if (!folder) {
return;
}
const auto version = folder->chatListViewVersion();
if (_listEntryCacheVersion == version) {
return;
}
_listEntryCacheVersion = version;
_listEntryCache.setMarkedText(
st::dialogsTextStyle,
ComposeFolderListEntryText(folder),
// Use rich options as long as the entry text does not have user text.
Ui::ItemTextDefaultOptions());
}
void Row::setCornerBadgeShown(
bool shown,
Fn<void()> updateCallback) const {
const auto value = shown ? 1 : 0;
if (_cornerBadgeShown == value) {
if (_cornerBadgeShown == shown) {
return;
}
_cornerBadgeShown = value;
const_cast<Row*>(this)->_cornerBadgeShown = shown;
if (_cornerBadgeUserpic && _cornerBadgeUserpic->animation.animating()) {
_cornerBadgeUserpic->animation.change(
_cornerBadgeShown ? 1. : 0.,

View File

@ -119,11 +119,6 @@ public:
}
[[nodiscard]] uint64 sortKey(FilterId filterId) const;
void validateListEntryCache() const;
[[nodiscard]] const Ui::Text::String &listEntryCache() const {
return _listEntryCache;
}
// for any attached data, for example View in contacts list
void *attached = nullptr;
@ -151,13 +146,11 @@ private:
const Ui::PaintContext &context);
Key _id;
mutable Ui::Text::String _listEntryCache;
mutable std::unique_ptr<CornerBadgeUserpic> _cornerBadgeUserpic;
int _top = 0;
int _height = 0;
int _index = 0;
mutable int _listEntryCacheVersion : 31 = 0;
mutable int _cornerBadgeShown : 1 = 0;
bool _cornerBadgeShown = false;
};

View File

@ -198,22 +198,22 @@ int PaintWideCounter(
return availableWidth - used;
}
void PaintListEntryText(
void PaintFolderEntryText(
Painter &p,
not_null<const Row*> row,
not_null<Data::Folder*> folder,
const PaintContext &context,
QRect rect) {
if (rect.isEmpty()) {
return;
}
row->validateListEntryCache();
folder->validateListEntryCache();
p.setFont(st::dialogsTextFont);
p.setPen(context.active
? st::dialogsTextFgActive
: context.selected
? st::dialogsTextFgOver
: st::dialogsTextFg);
row->listEntryCache().draw(p, {
folder->listEntryCache().draw(p, {
.position = rect.topLeft(),
.availableWidth = rect.width(),
.palette = &(context.active
@ -342,7 +342,14 @@ void PaintRow(
}
}
auto texttop = context.st->textTop;
if (promoted && !history->topPromotionMessage().isEmpty()) {
if (const auto folder = entry->asFolder()) {
const auto rect = QRect(
nameleft,
texttop,
namewidth,
st::dialogsTextFont->height);
PaintFolderEntryText(p, folder, context, rect);
} else if (promoted && !history->topPromotionMessage().isEmpty()) {
auto availableWidth = namewidth;
p.setFont(st::dialogsTextFont);
if (history->cloudDraftTextCache().isEmpty()) {
@ -911,21 +918,19 @@ void RowPainter::Paint(
: thread
? &thread->lastItemDialogsView()
: nullptr;
if (const auto folder = row->folder()) {
PaintListEntryText(p, row, context, rect);
} else if (view) {
if (!view->prepared(item)) {
if (view) {
const auto forum = context.st->topicsHeight
? row->history()->peer->forum()
: nullptr;
if (!view->prepared(item, forum)) {
view->prepare(
item,
forum,
[=] { entry->updateChatListEntry(); },
{ .ignoreTopic = (!history || !peer->isForum()) });
{});
}
if (const auto topics = context.st->topicsHeight) {
view->prepareTopics(
row->history()->peer->forum(),
rect,
[=] { entry->updateChatListEntry(); });
rect.setHeight(topics + rect.height());
if (forum) {
rect.setHeight(context.st->topicsHeight + rect.height());
}
view->paint(p, rect, context);
}
@ -1015,10 +1020,10 @@ void RowPainter::Paint(
availableWidth,
st::dialogsTextFont->height);
auto &view = row->itemView();
if (!view.prepared(item)) {
view.prepare(item, row->repaint(), previewOptions);
if (!view.prepared(item, nullptr)) {
view.prepare(item, nullptr, row->repaint(), previewOptions);
}
row->itemView().paint(p, itemRect, context);
view.paint(p, itemRect, context);
};
const auto showSavedMessages = history
&& history->peer->isSelf()

View File

@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/cached_round_corners.h"
namespace style {
struct DialogRow;
} // namespace style
@ -35,8 +37,23 @@ using namespace ::Ui;
class VideoUserpic;
struct TopicJumpCorners {
Ui::CornersPixmaps normal;
Ui::CornersPixmaps inverted;
QPixmap small;
int invertedRadius = 0;
int smallKey = 0; // = `-radius` if top right else `radius`.
};
struct TopicJumpCache {
TopicJumpCorners corners;
TopicJumpCorners over;
TopicJumpCorners rippleMask;
};
struct PaintContext {
not_null<const style::DialogRow*> st;
TopicJumpCache *topicJumpCache = nullptr;
Data::Folder *folder = nullptr;
Data::Forum *forum = nullptr;
FilterId filter = 0;

View File

@ -127,15 +127,32 @@ bool MessageView::dependsOn(not_null<const HistoryItem*> item) const {
return (_textCachedFor == item.get());
}
bool MessageView::prepared(not_null<const HistoryItem*> item) const {
return (_textCachedFor == item.get());
bool MessageView::prepared(
not_null<const HistoryItem*> item,
Data::Forum *forum) const {
return (_textCachedFor == item.get())
&& (!forum
|| (_topics
&& _topics->forum() == forum
&& _topics->prepared()));
}
void MessageView::prepare(
not_null<const HistoryItem*> item,
Data::Forum *forum,
Fn<void()> customEmojiRepaint,
ToPreviewOptions options) {
const auto validateTopics = !options.ignoreTopic;
if (!forum) {
_topics = nullptr;
} else if (!_topics || _topics->forum() != forum) {
_topics = std::make_unique<TopicsView>(forum);
_topics->prepare(item->topicRootId(), customEmojiRepaint);
} else if (!_topics->prepared()) {
_topics->prepare(item->topicRootId(), customEmojiRepaint);
}
if (_textCachedFor == item.get()) {
return;
}
options.existing = &_imagesCache;
options.ignoreTopic = true;
auto preview = item->toPreview(options);
@ -183,17 +200,21 @@ void MessageView::prepare(
}
}
void MessageView::prepareTopics(
not_null<Data::Forum*> forum,
const QRect &geometry,
Fn<void()> customEmojiRepaint) {
if (!_topics || _topics->forum() != forum) {
_topics = std::make_unique<TopicsView>(forum);
int MessageView::countWidth() const {
auto result = 0;
if (!_senderCache.isEmpty()) {
result += _senderCache.maxWidth();
if (!_imagesCache.empty()) {
result += st::dialogsMiniPreviewSkip
+ st::dialogsMiniPreviewRight;
}
}
_topics->prepare(
geometry,
&st::forumDialogRow,
std::move(customEmojiRepaint));
if (!_imagesCache.empty()) {
result += (_imagesCache.size()
* (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip))
+ st::dialogsMiniPreviewRight;
}
return result + _textCache.maxWidth();
}
void MessageView::paint(
@ -223,11 +244,22 @@ void MessageView::paint(
: st::dialogsTextPalette));
auto rect = geometry;
const auto checkJump = withTopic && !context.active;
const auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0;
if (jump1) {
paintJumpToLast(p, rect, context, jump1);
}
if (withTopic) {
_topics->paint(p, rect, context);
rect.setTop(rect.top() + context.st->topicsHeight);
}
auto finalRight = rect.x() + rect.width();
if (jump1) {
rect.setWidth(rect.width() - st::forumDialogJumpArrowSkip);
finalRight -= st::forumDialogJumpArrowSkip;
}
const auto lines = rect.height() / st::dialogsTextFont->height;
if (!_senderCache.isEmpty()) {
_senderCache.draw(p, {
@ -258,20 +290,143 @@ void MessageView::paint(
if (!_imagesCache.empty()) {
rect.setLeft(rect.x() + st::dialogsMiniPreviewRight);
}
if (rect.isEmpty()) {
if (!rect.isEmpty()) {
_textCache.draw(p, {
.position = rect.topLeft(),
.availableWidth = rect.width(),
.palette = palette,
.spoiler = Text::DefaultSpoilerCache(),
.now = context.now,
.paused = context.paused,
.elisionLines = lines,
});
rect.setLeft(rect.x() + _textCache.maxWidth());
}
if (jump1) {
const auto x = (rect.width() > st::forumDialogJumpArrowSkip)
? rect.x()
: finalRight;
const auto add = st::forumDialogJumpArrowLeft;
const auto y = rect.y() + st::forumDialogJumpArrowTop;
(context.selected
? st::forumDialogJumpArrowOver
: st::forumDialogJumpArrow).paint(p, x + add, y, context.width);
}
}
void MessageView::paintJumpToLast(
Painter &p,
const QRect &rect,
const PaintContext &context,
int width1) const {
if (!context.topicJumpCache) {
return;
}
_textCache.draw(p, {
.position = rect.topLeft(),
.availableWidth = rect.width(),
.palette = palette,
.spoiler = Text::DefaultSpoilerCache(),
.now = context.now,
.paused = context.paused,
.elisionLines = lines,
FillJumpToLastBg(p, {
.st = context.st,
.corners = (context.selected
? &context.topicJumpCache->over
: &context.topicJumpCache->corners),
.geometry = rect,
.bg = (context.selected
? st::dialogsRippleBg
: st::dialogsBgOver),
.width1 = width1,
.width2 = countWidth() + st::forumDialogJumpArrowSkip,
});
}
void FillJumpToLastBg(QPainter &p, JumpToLastBg context) {
const auto availableWidth = context.geometry.width();
const auto use1 = std::min(context.width1, availableWidth);
const auto use2 = std::min(context.width2, availableWidth);
const auto padding = st::forumDialogJumpPadding;
const auto radius = st::forumDialogJumpRadius;
const auto &bg = context.bg;
auto &normal = context.corners->normal;
auto &inverted = context.corners->inverted;
auto &small = context.corners->small;
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(bg);
const auto origin = context.geometry.topLeft();
const auto delta = std::abs(use1 - use2);
if (delta <= context.st->topicsSkip / 2) {
if (normal.p[0].isNull()) {
normal = Ui::PrepareCornerPixmaps(radius, bg);
}
const auto w = std::max(use1, use2);
const auto h = context.st->topicsHeight + st::normalFont->height;
const auto fill = QRect(origin, QSize(w, h));
Ui::FillRoundRect(p, fill.marginsAdded(padding), bg, normal);
} else {
const auto h1 = context.st->topicsHeight;
const auto h2 = st::normalFont->height;
const auto hmin = std::min(h1, h2);
const auto wantedInvertedRadius = hmin - radius;
const auto invertedr = std::min(wantedInvertedRadius, delta / 2);
const auto smallr = std::min(radius, delta - invertedr);
const auto smallkey = (use1 < use2) ? smallr : (-smallr);
if (normal.p[0].isNull()) {
normal = Ui::PrepareCornerPixmaps(radius, bg);
}
if (inverted.p[0].isNull()
|| context.corners->invertedRadius != invertedr) {
context.corners->invertedRadius = invertedr;
inverted = Ui::PrepareInvertedCornerPixmaps(invertedr, bg);
}
if (smallr != radius
&& (small.isNull() || context.corners->smallKey != smallkey)) {
context.corners->smallKey = smallr;
auto pixmaps = Ui::PrepareCornerPixmaps(smallr, bg);
small = pixmaps.p[(use1 < use2) ? 1 : 3];
}
const auto rect1 = QRect(origin, QSize(use1, h1));
auto no1 = normal;
no1.p[2] = QPixmap();
if (use1 < use2) {
no1.p[3] = QPixmap();
} else if (smallr != radius) {
no1.p[3] = small;
}
auto fill1 = rect1.marginsAdded({
padding.left(),
padding.top(),
padding.right(),
(use1 < use2 ? -padding.top() : padding.bottom()),
});
Ui::FillRoundRect(p, fill1, bg, no1);
if (use1 < use2) {
p.drawPixmap(
fill1.x() + fill1.width(),
fill1.y() + fill1.height() - invertedr,
inverted.p[3]);
}
const auto add = QPoint(0, h1);
const auto rect2 = QRect(origin + add, QSize(use2, h2));
const auto fill2 = rect2.marginsAdded({
padding.left(),
(use2 < use1 ? -padding.bottom() : padding.top()),
padding.right(),
padding.bottom(),
});
auto no2 = normal;
no2.p[0] = QPixmap();
if (use2 < use1) {
no2.p[1] = QPixmap();
} else if (smallr != radius) {
no2.p[1] = small;
}
Ui::FillRoundRect(p, fill2, bg, no2);
if (use2 < use1) {
p.drawPixmap(
fill2.x() + fill2.width(),
fill2.y(),
inverted.p[0]);
}
}
}
HistoryView::ItemPreview PreviewWithSender(
HistoryView::ItemPreview &&preview,
const QString &sender,

View File

@ -13,6 +13,10 @@ class Image;
class HistoryItem;
enum class ImageRoundRadius;
namespace style {
struct DialogRow;
} // namespace style
namespace Ui {
} // namespace Ui
@ -32,6 +36,7 @@ using namespace ::Ui;
struct PaintContext;
class TopicsView;
struct TopicJumpCorners;
[[nodiscard]] TextWithEntities DialogsPreviewText(TextWithEntities text);
@ -47,17 +52,15 @@ public:
void itemInvalidated(not_null<const HistoryItem*> item);
[[nodiscard]] bool dependsOn(not_null<const HistoryItem*> item) const;
[[nodiscard]] bool prepared(not_null<const HistoryItem*> item) const;
[[nodiscard]] bool prepared(
not_null<const HistoryItem*> item,
Data::Forum *forum) const;
void prepare(
not_null<const HistoryItem*> item,
Data::Forum *forum,
Fn<void()> customEmojiRepaint,
ToPreviewOptions options);
void prepareTopics(
not_null<Data::Forum*> forum,
const QRect &geometry,
Fn<void()> customEmojiRepaint);
void paint(
Painter &p,
const QRect &geometry,
@ -66,6 +69,13 @@ public:
private:
struct LoadingContext;
[[nodiscard]] int countWidth() const;
void paintJumpToLast(
Painter &p,
const QRect &rect,
const PaintContext &context,
int width1) const;
mutable const HistoryItem *_textCachedFor = nullptr;
mutable Text::String _senderCache;
mutable std::unique_ptr<TopicsView> _topics;
@ -80,4 +90,14 @@ private:
const QString &sender,
TextWithEntities topic);
struct JumpToLastBg {
not_null<const style::DialogRow*> st;
not_null<TopicJumpCorners*> corners;
QRect geometry;
const style::color &bg;
int width1 = 0;
int width2 = 0;
};
void FillJumpToLastBg(QPainter &p, JumpToLastBg context);
} // namespace Dialogs::Ui

View File

@ -29,47 +29,76 @@ TopicsView::TopicsView(not_null<Data::Forum*> forum)
TopicsView::~TopicsView() = default;
void TopicsView::prepare(
const QRect &geometry,
not_null<const style::DialogRow*> st,
Fn<void()> customEmojiRepaint) {
bool TopicsView::prepared() const {
return (_version == _forum->recentTopicsListVersion());
}
void TopicsView::prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint) {
const auto &list = _forum->recentTopics();
_version = _forum->recentTopicsListVersion();
_titles.reserve(list.size());
auto index = 0;
auto available = geometry.width();
for (const auto &topic : _forum->recentTopics()) {
if (available <= 0) {
break;
} else if (_titles.size() == index) {
for (const auto &topic : list) {
const auto from = begin(_titles) + index;
const auto rootId = topic->rootId();
const auto i = ranges::find(
from,
end(_titles),
rootId,
&Title::topicRootId);
if (i != end(_titles)) {
if (i != from) {
ranges::rotate(from, i, i + 1);
}
} else if (index >= _titles.size()) {
_titles.emplace_back();
}
auto &title = _titles[index];
const auto rootId = topic->rootId();
auto &title = _titles[index++];
title.topicRootId = rootId;
const auto unread = topic->chatListBadgesState().unread;
if (title.topicRootId != rootId || title.unread != unread) {
const auto context = Core::MarkedTextContext{
.session = &topic->session(),
.customEmojiRepaint = customEmojiRepaint,
.customEmojiLoopLimit = kIconLoopCount,
};
auto topicTitle = topic->titleWithIcon();
title.title.setMarkedText(
st::dialogsTextStyle,
(unread
? Ui::Text::PlainLink(
Ui::Text::Wrapped(
std::move(topicTitle),
EntityType::Bold))
: std::move(topicTitle)),
DialogTextOptions(),
context);
title.topicRootId = rootId;
title.unread = unread;
if (title.unread == unread
&& title.version == topic->titleVersion()) {
continue;
}
available -= title.title.maxWidth() + st->topicsSkip;
++index;
const auto context = Core::MarkedTextContext{
.session = &topic->session(),
.customEmojiRepaint = customEmojiRepaint,
.customEmojiLoopLimit = kIconLoopCount,
};
auto topicTitle = topic->titleWithIcon();
title.version = topic->titleVersion();
title.unread = unread;
title.title.setMarkedText(
st::dialogsTextStyle,
(unread
? Ui::Text::PlainLink(
Ui::Text::Wrapped(
std::move(topicTitle),
EntityType::Bold))
: std::move(topicTitle)),
DialogTextOptions(),
context);
}
while (_titles.size() > index) {
_titles.pop_back();
}
const auto i = frontRootId
? ranges::find(_titles, frontRootId, &Title::topicRootId)
: end(_titles);
_jumpToTopic = (i != end(_titles));
if (_jumpToTopic) {
if (i != begin(_titles)) {
ranges::rotate(begin(_titles), i, i + 1);
}
if (!_titles.front().unread) {
_jumpToTopic = false;
}
}
}
int TopicsView::jumpToTopicWidth() const {
return _jumpToTopic ? _titles.front().title.maxWidth() : 0;
}
void TopicsView::paint(
@ -91,6 +120,7 @@ void TopicsView::paint(
: st::dialogsTextPaletteArchive);
auto index = 0;
auto rect = geometry;
auto skipBig = _jumpToTopic && !context.active;
for (const auto &title : _titles) {
if (rect.width() <= 0) {
break;
@ -104,8 +134,11 @@ void TopicsView::paint(
.paused = context.paused,
.elisionLines = 1,
});
rect.setLeft(
rect.left() + title.title.maxWidth() + context.st->topicsSkip);
const auto skip = skipBig
? context.st->topicsSkipBig
: context.st->topicsSkip;
rect.setLeft(rect.left() + title.title.maxWidth() + skip);
skipBig = false;
}
}

View File

@ -36,10 +36,10 @@ public:
return _forum;
}
void prepare(
const QRect &geometry,
not_null<const style::DialogRow*> st,
Fn<void()> customEmojiRepaint);
[[nodiscard]] bool prepared() const;
void prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint);
[[nodiscard]] int jumpToTopicWidth() const;
void paint(
Painter &p,
@ -54,11 +54,14 @@ private:
struct Title {
Text::String title;
MsgId topicRootId = 0;
int version = -1;
bool unread = false;
};
const not_null<Data::Forum*> _forum;
mutable std::vector<Title> _titles;
int _version = -1;
bool _jumpToTopic = false;
rpl::lifetime _lifetime;

View File

@ -220,7 +220,7 @@ const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index) {
return Corners[index];
}
CornersPixmaps PrepareCornerPixmaps(int32 radius, style::color bg, const style::color *sh) {
CornersPixmaps PrepareCornerPixmaps(int radius, style::color bg, const style::color *sh) {
auto images = PrepareCorners(radius, bg, sh);
auto result = CornersPixmaps();
for (int j = 0; j < 4; ++j) {
@ -240,6 +240,24 @@ CornersPixmaps PrepareCornerPixmaps(ImageRoundRadius radius, style::color bg, co
Unexpected("Image round radius in PrepareCornerPixmaps.");
}
CornersPixmaps PrepareInvertedCornerPixmaps(int radius, style::color bg) {
const auto size = radius * style::DevicePixelRatio();
auto circle = style::colorizeImage(
style::createInvertedCircleMask(radius * 2),
bg);
circle.setDevicePixelRatio(style::DevicePixelRatio());
auto result = CornersPixmaps();
const auto fill = [&](int index, int xoffset, int yoffset) {
result.p[index] = PixmapFromImage(
circle.copy(QRect(xoffset, yoffset, size, size)));
};
fill(0, 0, 0);
fill(1, size, 0);
fill(2, size, size);
fill(3, 0, size);
return result;
}
[[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag) {
using Radius = CachedCornerRadius;
switch (tag) {

View File

@ -36,25 +36,28 @@ enum CachedRoundCorners : int {
RoundCornersCount
};
void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, CachedRoundCorners index);
void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, CachedRoundCorners index);
inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, CachedRoundCorners index) {
FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, index);
}
[[nodiscard]] const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index);
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
int32 radius,
int radius,
style::color bg,
const style::color *sh = nullptr);
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
ImageRoundRadius radius,
style::color bg,
const style::color *sh = nullptr);
void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, const CornersPixmaps &corners);
[[nodiscard]] CornersPixmaps PrepareInvertedCornerPixmaps(
int radius,
style::color bg);
void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, const CornersPixmaps &corners);
inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, const CornersPixmaps &corners) {
return FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, corners);
}
void FillRoundShadow(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color shadow, const CornersPixmaps &corners);
void FillRoundShadow(QPainter &p, int x, int y, int w, int h, style::color shadow, const CornersPixmaps &corners);
inline void FillRoundShadow(QPainter &p, const QRect &rect, style::color shadow, const CornersPixmaps &corners) {
FillRoundShadow(p, rect.x(), rect.y(), rect.width(), rect.height(), shadow, corners);
}

View File

@ -480,20 +480,9 @@ const CornersPixmaps &ChatStyle::serviceBgCornersNormal() const {
const CornersPixmaps &ChatStyle::serviceBgCornersInverted() const {
if (_serviceBgCornersInverted.p[0].isNull()) {
const auto radius = HistoryServiceMsgInvertedRadius();
const auto size = radius * style::DevicePixelRatio();
auto circle = style::colorizeImage(
style::createInvertedCircleMask(radius * 2),
_serviceBgCornersInverted = Ui::PrepareInvertedCornerPixmaps(
HistoryServiceMsgInvertedRadius(),
msgServiceBg());
circle.setDevicePixelRatio(style::DevicePixelRatio());
const auto fill = [&](int index, int xoffset, int yoffset) {
_serviceBgCornersInverted.p[index] = PixmapFromImage(
circle.copy(QRect(xoffset, yoffset, size, size)));
};
fill(0, 0, 0);
fill(1, size, 0);
fill(2, size, size);
fill(3, 0, size);
}
return _serviceBgCornersInverted;
}