Support pinned messages bar in topics.

This commit is contained in:
John Preston 2022-10-28 09:19:27 +04:00
parent da1e784803
commit 8dc27339b4
40 changed files with 811 additions and 206 deletions

View File

@ -1511,9 +1511,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_topic_created_inside" = "Topic created";
"lng_action_topic_closed_inside" = "Topic closed";
"lng_action_topic_reopened_inside" = "Topic reopened";
"lng_action_topic_created" = "{topic} — was created";
"lng_action_topic_closed" = "{topic} — was closed";
"lng_action_topic_reopened" = "{topic} — was reopened";
"lng_action_topic_created" = "«{topic}» was created";
"lng_action_topic_closed" = "«{topic}» was closed";
"lng_action_topic_reopened" = "«{topic}» was reopened";
"lng_action_topic_placeholder" = "topic";
"lng_action_topic_renamed" = "{from} renamed the {link} to «{title}»";
"lng_action_topic_icon_changed" = "{from} changed the {link} icon to {emoji}";

View File

@ -3011,6 +3011,10 @@ void ApiWrap::sharedMediaDone(
MsgId topicRootId,
SharedMediaType type,
Api::SearchResult &&parsed) {
const auto topic = peer->forumTopicFor(topicRootId);
if (topicRootId && !topic) {
return;
}
_session->storage().add(Storage::SharedMediaAddSlice(
peer->id,
topicRootId,
@ -3021,6 +3025,9 @@ void ApiWrap::sharedMediaDone(
));
if (type == SharedMediaType::Pinned && !parsed.messageIds.empty()) {
peer->owner().history(peer)->setHasPinnedMessages(true);
if (topic) {
topic->setHasPinnedMessages(true);
}
}
}
@ -3451,11 +3458,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
action.generateLocal = true;
sendAction(action);
const auto replyToId = message.action.replyTo;
const auto replyToId = action.replyTo;
const auto replyTo = replyToId
? peer->owner().message(peer, replyToId)
: nullptr;
const auto topic = replyTo ? replyTo->topic() : nullptr;
const auto topicRootId = replyTo ? replyTo->topicRootId() : replyToId;
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
if (!(topic ? topic->canWrite() : peer->canWrite())
|| Api::SendDice(message)) {
return;

View File

@ -130,9 +130,8 @@ struct HistoryUpdate {
BotKeyboard = (1U << 12),
CloudDraft = (1U << 13),
LocalDraftSet = (1U << 14),
PinnedMessages = (1U << 15),
LastUsedBit = (1U << 15),
LastUsedBit = (1U << 14),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }
@ -156,7 +155,7 @@ struct TopicUpdate {
CloudDraft = (1U << 8),
Closed = (1U << 9),
LastUsedBit = (1U << 8),
LastUsedBit = (1U << 9),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }
@ -196,8 +195,9 @@ struct EntryUpdate {
None = 0,
Repaint = (1U << 0),
HasPinnedMessages = (1U << 1),
LastUsedBit = (1U << 0),
LastUsedBit = (1U << 1),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View File

@ -994,7 +994,7 @@ void ApplyChannelUpdate(
}
}
if (const auto pinned = update.vpinned_msg_id()) {
SetTopPinnedMessageId(channel, MsgId(0), pinned->v);
SetTopPinnedMessageId(channel, pinned->v);
}
if (channel->isMegagroup()) {
auto commands = ranges::views::all(

View File

@ -475,7 +475,7 @@ void ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {
chat->session().api().inviteLinks().clearMyPermanent(chat);
}
if (const auto pinned = update.vpinned_msg_id()) {
SetTopPinnedMessageId(chat, MsgId(0), pinned->v);
SetTopPinnedMessageId(chat, pinned->v);
}
chat->checkFolder(update.vfolder_id().value_or_empty());
chat->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty()));

View File

@ -23,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/input_fields.h"
#include "storage/storage_facade.h"
#include "storage/storage_shared_media.h"
#include "window/window_session_controller.h"
#include "window/notifications_manager.h"
#include "styles/style_boxes.h"
@ -63,6 +65,12 @@ Forum::~Forum() {
if (_requestId) {
session().api().request(_requestId).cancel();
}
const auto peerId = _history->peer->id;
for (const auto &[rootId, topic] : _topics) {
session().storage().unload(Storage::SharedMediaUnloadThread(
peerId,
rootId));
}
}
Session &Forum::owner() const {
@ -170,6 +178,9 @@ void Forum::applyTopicDeleted(MsgId rootId) {
_topics.erase(i);
_history->destroyMessagesByTopic(rootId);
session().storage().unload(Storage::SharedMediaUnloadThread(
_history->peer->id,
rootId));
}
}

View File

@ -657,9 +657,7 @@ void ForumTopic::setMuted(bool muted) {
const auto notify = state.unread || state.reaction;
const auto notifier = unreadStateChangeNotifier(notify);
Thread::setMuted(muted);
session().changes().topicUpdated(
this,
Data::TopicUpdate::Flag::Notifications);
session().changes().topicUpdated(this, UpdateFlag::Notifications);
}
not_null<HistoryView::SendActionPainter*> ForumTopic::sendActionPainter() {

View File

@ -139,6 +139,7 @@ private:
enum class Flag : uchar {
Closed = (1 << 0),
My = (1 << 1),
HasPinnedMessages = (1 << 2),
};
friend inline constexpr bool is_flag_type(Flag) { return true; }
using Flags = base::flags<Flag>;

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_photo.h"
#include "data/data_folder.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "data/data_histories.h"
@ -1255,7 +1256,6 @@ std::optional<QString> RestrictionError(
void SetTopPinnedMessageId(
not_null<PeerData*> peer,
MsgId topicRootId,
MsgId messageId) {
if (const auto channel = peer->asChannel()) {
if (messageId <= channel->availableMinId()) {
@ -1265,12 +1265,15 @@ void SetTopPinnedMessageId(
auto &session = peer->session();
const auto hiddenId = session.settings().hiddenPinnedMessageId(peer->id);
if (hiddenId != 0 && hiddenId != messageId) {
session.settings().setHiddenPinnedMessageId(peer->id, 0);
session.settings().setHiddenPinnedMessageId(
peer->id,
MsgId(0), // topicRootId
0);
session.saveSettingsDelayed();
}
session.storage().add(Storage::SharedMediaAddExisting(
peer->id,
topicRootId,
MsgId(0), // topicRootId
Storage::SharedMediaType::Pinned,
messageId,
{ messageId, ServerMaxMsgId }));

View File

@ -482,15 +482,14 @@ std::optional<QString> RestrictionError(
void SetTopPinnedMessageId(
not_null<PeerData*> peer,
MsgId topicRootId,
MsgId messageId);
[[nodiscard]] FullMsgId ResolveTopPinnedId(
not_null<PeerData*> peer,
MsgId topicRootId,
PeerData *migrated);
PeerData *migrated = nullptr);
[[nodiscard]] FullMsgId ResolveMinPinnedId(
not_null<PeerData*> peer,
MsgId topicRootId,
PeerData *migrated);
PeerData *migrated = nullptr);
} // namespace Data

View File

@ -8,15 +8,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_thread.h"
#include "data/data_forum_topic.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_unread_things.h"
#include "main/main_session.h"
namespace Data {
Thread::~Thread() = default;
not_null<Thread*> Thread::migrateToOrMe() const {
const auto history = asHistory();
return history ? history->migrateToOrMe() : const_cast<Thread*>(this);
}
MsgId Thread::topicRootId() const {
if (const auto topic = asTopic()) {
return topic->rootId();
@ -157,4 +164,21 @@ void Thread::setUnreadMarkFlag(bool unread) {
}
}
[[nodiscard]] bool Thread::hasPinnedMessages() const {
return (_flags & Flag::HasPinnedMessages);
}
void Thread::setHasPinnedMessages(bool has) {
if (hasPinnedMessages() == has) {
return;
} else if (has) {
_flags |= Flag::HasPinnedMessages;
} else {
_flags &= ~Flag::HasPinnedMessages;
}
session().changes().entryUpdated(
this,
EntryUpdate::Flag::HasPinnedMessages);
}
} // namespace Data

View File

@ -58,6 +58,7 @@ public:
[[nodiscard]] virtual not_null<History*> owningHistory() = 0;
[[nodiscard]] not_null<Thread*> migrateToOrMe() const;
[[nodiscard]] not_null<const History*> owningHistory() const {
return const_cast<Thread*>(this)->owningHistory();
}
@ -110,6 +111,9 @@ public:
[[nodiscard]] virtual auto sendActionPainter()
-> not_null<HistoryView::SendActionPainter*> = 0;
[[nodiscard]] bool hasPinnedMessages() const;
void setHasPinnedMessages(bool has);
protected:
void setUnreadMarkFlag(bool unread);
@ -118,6 +122,7 @@ private:
UnreadMark = (1 << 0),
Muted = (1 << 1),
UnreadThingsKnown = (1 << 2),
HasPinnedMessages = (1 << 3),
};
friend inline constexpr bool is_flag_type(Flag) { return true; }

View File

@ -363,7 +363,7 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
user->setBotInfoVersion(-1);
}
if (const auto pinned = update.vpinned_msg_id()) {
SetTopPinnedMessageId(user, MsgId(0), pinned->v);
SetTopPinnedMessageId(user, pinned->v);
}
const auto canReceiveGifts = (update.vflags().v
& MTPDuserFull::Flag::f_premium_gifts)

View File

@ -508,16 +508,25 @@ void History::destroyMessagesByTopic(MsgId topicRootId) {
}
}
void History::unpinAllMessages() {
session().storage().remove(
Storage::SharedMediaRemoveAll(
peer->id,
Storage::SharedMediaType::Pinned));
setHasPinnedMessages(false);
for (const auto &message : _messages) {
if (message->isPinned()) {
message->setIsPinned(false);
void History::unpinMessagesFor(MsgId topicRootId) {
if (!topicRootId) {
session().storage().remove(
Storage::SharedMediaRemoveAll(
peer->id,
Storage::SharedMediaType::Pinned));
setHasPinnedMessages(false);
if (const auto forum = peer->forum()) {
forum->enumerateTopics([](not_null<Data::ForumTopic*> topic) {
topic->setHasPinnedMessages(false);
});
}
for (const auto &message : _messages) {
if (message->isPinned()) {
message->setIsPinned(false);
}
}
} else {
// #TODO forum pinned
}
}
@ -792,15 +801,29 @@ not_null<HistoryItem*> History::addNewToBack(
if (const auto sharedMediaTypes = item->sharedMediaTypes()) {
auto from = loadedAtTop() ? 0 : minMsgId();
auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId();
session().storage().add(Storage::SharedMediaAddExisting(
auto &storage = session().storage();
storage.add(Storage::SharedMediaAddExisting(
peer->id,
MsgId(0),
MsgId(0), // topicRootId
sharedMediaTypes,
item->id,
{ from, till }));
if (sharedMediaTypes.test(Storage::SharedMediaType::Pinned)) {
const auto pinned = sharedMediaTypes.test(
Storage::SharedMediaType::Pinned);
if (pinned) {
setHasPinnedMessages(true);
}
if (const auto topic = item->topic()) {
storage.add(Storage::SharedMediaAddExisting(
peer->id,
topic->rootId(),
sharedMediaTypes,
item->id,
{ item->id, item->id}));
if (pinned) {
topic->setHasPinnedMessages(true);
}
}
}
}
if (item->from()->id) {
@ -1064,11 +1087,20 @@ void History::applyServiceChanges(
if (item) {
session().storage().add(Storage::SharedMediaAddSlice(
peer->id,
MsgId(0), // topicRootId
MsgId(0),
Storage::SharedMediaType::Pinned,
{ id },
{ id, ServerMaxMsgId }));
setHasPinnedMessages(true);
if (const auto topic = item->topic()) {
session().storage().add(Storage::SharedMediaAddSlice(
peer->id,
topic->rootId(),
Storage::SharedMediaType::Pinned,
{ id },
{ id, ServerMaxMsgId }));
topic->setHasPinnedMessages(true);
}
}
});
}
@ -1464,6 +1496,7 @@ void History::checkAddAllToUnreadMentions() {
void History::addToSharedMedia(
const std::vector<not_null<HistoryItem*>> &items) {
std::vector<MsgId> medias[Storage::kSharedMediaTypeCount];
auto topicsWithPinned = base::flat_set<not_null<Data::ForumTopic*>>();
for (const auto &item : items) {
if (const auto types = item->sharedMediaTypes()) {
for (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) {
@ -1473,6 +1506,13 @@ void History::addToSharedMedia(
medias[i].reserve(items.size());
}
medias[i].push_back(item->id);
if (type == Storage::SharedMediaType::Pinned) {
if (const auto topic = item->topic()) {
if (!topic->hasPinnedMessages()) {
topicsWithPinned.emplace(topic);
}
}
}
}
}
}
@ -1493,6 +1533,9 @@ void History::addToSharedMedia(
}
}
}
for (const auto &topic : topicsWithPinned) {
topic->setHasPinnedMessages(true);
}
}
void History::calculateFirstUnreadMessage() {
@ -1750,7 +1793,7 @@ void History::setUnreadMark(bool unread) {
}
void History::setFakeUnreadWhileOpened(bool enabled) {
if (_fakeUnreadWhileOpened == enabled) {
if (fakeUnreadWhileOpened() == enabled) {
return;
} else if (enabled) {
if (!inChatList()) {
@ -1761,12 +1804,16 @@ void History::setFakeUnreadWhileOpened(bool enabled) {
return;
}
}
_fakeUnreadWhileOpened = enabled;
if (enabled) {
_flags |= Flag::FakeUnreadWhileOpened;
} else {
_flags &= ~Flag::FakeUnreadWhileOpened;
}
owner().chatsFilters().refreshHistory(this);
}
[[nodiscard]] bool History::fakeUnreadWhileOpened() const {
return _fakeUnreadWhileOpened;
return (_flags & Flag::FakeUnreadWhileOpened);
}
void History::setMuted(bool muted) {
@ -3316,15 +3363,6 @@ void History::removeBlock(not_null<HistoryBlock*> block) {
}
}
bool History::hasPinnedMessages() const {
return _hasPinnedMessages;
}
void History::setHasPinnedMessages(bool has) {
_hasPinnedMessages = has;
session().changes().historyUpdated(this, UpdateFlag::PinnedMessages);
}
void History::cacheTopPromoted(bool promoted) {
if (isTopPromoted() == promoted) {
return;

View File

@ -140,7 +140,7 @@ public:
void destroyMessagesByDates(TimeId minDate, TimeId maxDate);
void destroyMessagesByTopic(MsgId topicRootId);
void unpinAllMessages();
void unpinMessagesFor(MsgId topicRootId);
not_null<HistoryItem*> addNewMessage(
MsgId id,
@ -426,9 +426,6 @@ public:
void setInboxReadTill(MsgId upTo);
std::optional<int> countStillUnreadLocal(MsgId readTillId) const;
[[nodiscard]] bool hasPinnedMessages() const;
void setHasPinnedMessages(bool has);
[[nodiscard]] bool isTopPromoted() const;
const not_null<PeerData*> peer;
@ -461,6 +458,8 @@ private:
HasPendingResizedItems = (1 << 0),
IsTopPromoted = (1 << 1),
IsForum = (1 << 2),
FakeUnreadWhileOpened = (1 << 3),
HasPinnedMessages = (1 << 4),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) {
@ -610,9 +609,6 @@ private:
QString _chatListNameSortKey;
bool _fakeUnreadWhileOpened = false;
bool _hasPinnedMessages = false;
// A pointer to the block that is currently being built.
// We hold this pointer so we can destroy it while building
// and then create a new one if it is necessary.

View File

@ -480,13 +480,23 @@ void HistoryItem::setIsPinned(bool pinned) {
const auto changed = (isPinned() != pinned);
if (pinned) {
_flags |= MessageFlag::Pinned;
history()->session().storage().add(Storage::SharedMediaAddExisting(
auto &storage = history()->session().storage();
storage.add(Storage::SharedMediaAddExisting(
history()->peer->id,
MsgId(0), // topicRootId
Storage::SharedMediaType::Pinned,
id,
{ id, id }));
history()->setHasPinnedMessages(true);
if (const auto topic = this->topic()) {
storage.add(Storage::SharedMediaAddExisting(
history()->peer->id,
topic->rootId(),
Storage::SharedMediaType::Pinned,
id,
{ id, id }));
topic->setHasPinnedMessages(true);
}
} else {
_flags &= ~MessageFlag::Pinned;
history()->session().storage().remove(Storage::SharedMediaRemoveOne(
@ -734,6 +744,9 @@ void HistoryItem::indexAsNewItem() {
id));
if (types.test(Storage::SharedMediaType::Pinned)) {
_history->setHasPinnedMessages(true);
if (const auto topic = this->topic()) {
topic->setHasPinnedMessages(true);
}
}
}
}
@ -1423,7 +1436,12 @@ ClickHandlerPtr goToMessageClickHandler(
params.origin = Window::SectionShow::OriginMessage{
returnToId
};
controller->showPeerHistory(peer, params, msgId);
const auto item = peer->owner().message(peer, msgId);
if (const auto topic = item ? item->topic() : nullptr) {
controller->showTopic(topic, msgId, params);
} else {
controller->showPeerHistory(peer, params, msgId);
}
}
});
}

View File

@ -859,7 +859,8 @@ bool HistoryService::updateDependent(bool force) {
(dependent->peerId
? history()->owner().peer(dependent->peerId)
: history()->peer),
dependent->msgId);
dependent->msgId,
fullId());
}
auto gotDependencyItem = false;
if (!dependent->msg) {

View File

@ -73,6 +73,14 @@ float64 ElementHighlighter::progress(
void ElementHighlighter::highlight(FullMsgId itemId) {
if (const auto item = _data->message(itemId)) {
if (const auto view = _viewForItem(item)) {
if (_highlightedMessageId
&& _highlightedMessageId != itemId) {
if (const auto was = _data->message(_highlightedMessageId)) {
if (const auto view = _viewForItem(was)) {
repaintHighlightedItem(view);
}
}
}
_highlightedMessageId = itemId;
_animation.start();

View File

@ -603,6 +603,18 @@ HistoryWidget::HistoryWidget(
}
}, lifetime());
using EntryUpdateFlag = Data::EntryUpdate::Flag;
session().changes().entryUpdates(
EntryUpdateFlag::HasPinnedMessages
) | rpl::start_with_next([=](const Data::EntryUpdate &update) {
if (_pinnedTracker
&& (update.flags & EntryUpdateFlag::HasPinnedMessages)
&& (_migrated && update.entry.get() == _migrated)
|| (update.entry.get() == _history)) {
checkPinnedBarState();
}
}, lifetime());
using HistoryUpdateFlag = Data::HistoryUpdate::Flag;
session().changes().historyUpdates(
HistoryUpdateFlag::MessageSent
@ -614,14 +626,7 @@ HistoryWidget::HistoryWidget(
| HistoryUpdateFlag::UnreadView
| HistoryUpdateFlag::TopPromoted
| HistoryUpdateFlag::ClientSideMessages
| HistoryUpdateFlag::PinnedMessages
) | rpl::filter([=](const Data::HistoryUpdate &update) {
if (_migrated && update.history.get() == _migrated) {
if (_pinnedTracker
&& (update.flags & HistoryUpdateFlag::PinnedMessages)) {
checkPinnedBarState();
}
}
return (_history == update.history.get());
}) | rpl::start_with_next([=](const Data::HistoryUpdate &update) {
const auto flags = update.flags;
@ -647,9 +652,6 @@ HistoryWidget::HistoryWidget(
if (flags & HistoryUpdateFlag::UnreadView) {
unreadCountUpdated();
}
if (_pinnedTracker && (flags & HistoryUpdateFlag::PinnedMessages)) {
checkPinnedBarState();
}
if (flags & HistoryUpdateFlag::TopPromoted) {
updateHistoryGeometry();
updateControlsVisibility();
@ -3365,15 +3367,16 @@ void HistoryWidget::handleScroll() {
if (!_itemsRevealHeight) {
updatePinnedViewer();
}
const auto now = crl::now();
if (!_synteticScrollEvent) {
_lastUserScrolled = crl::now();
_lastUserScrolled = now;
}
const auto scrollTop = _scroll->scrollTop();
if (scrollTop != _lastScrollTop) {
if (!_synteticScrollEvent) {
checkLastPinnedClickedIdReset(_lastScrollTop, scrollTop);
}
_lastScrolled = crl::now();
_lastScrolled = now;
_lastScrollTop = scrollTop;
}
}
@ -6892,6 +6895,7 @@ void HistoryWidget::hidePinnedMessage() {
Window::HidePinnedBar(
controller(),
_peer,
MsgId(0), // topicRootId
crl::guard(this, callback));
}
}
@ -7765,20 +7769,6 @@ void HistoryWidget::paintEditHeader(Painter &p, const QRect &rect, int left, int
}
}
//
//void HistoryWidget::drawPinnedBar(Painter &p) {
// //if (_pinnedBar->msg) {
// // const auto media = _pinnedBar->msg->media();
// // if (media && media->hasReplyPreview()) {
// // if (const auto image = media->replyPreview()) {
// // QRect to(left, top, st::msgReplyBarSize.height(), st::msgReplyBarSize.height());
// // p.drawPixmap(to.x(), to.y(), image->pixSingle(image->width() / cIntRetinaFactor(), image->height() / cIntRetinaFactor(), to.width(), to.height(), ImageRoundRadius::Small));
// // }
// // left += st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x();
// // }
// //}
//}
bool HistoryWidget::paintShowAnimationFrame() {
auto progress = _a_show.value(1.);
if (!_a_show.animating()) {

View File

@ -680,11 +680,15 @@ bool AddPinMessageAction(
not_null<ListWidget*> list) {
const auto context = list->elementContext();
const auto item = request.item;
if (!item
|| !item->isRegular()
|| (context != Context::History && context != Context::Pinned)) {
if (!item || !item->isRegular()) {
return false;
}
const auto topic = item->topic();
if (context != Context::History && context != Context::Pinned) {
if (context != Context::Replies || !topic) {
return false;
}
}
const auto group = item->history()->owner().groups().find(item);
const auto pinItem = ((item->canPin() && item->isPinned()) || !group)
? item

View File

@ -411,7 +411,7 @@ ListWidget::ListWidget(
_selectScroll.scrolls(
) | rpl::start_with_next([=](int d) {
delegate->listScrollTo(_visibleTop + d);
delegate->listScrollTo(_visibleTop + d, false);
}, lifetime());
}
@ -1378,6 +1378,37 @@ bool ListWidget::hasSelectRestriction() const {
!= CopyRestrictionType::None;
}
auto ListWidget::findViewForPinnedTracking(int top) const
-> std::pair<Element*, int> {
const auto findScrollTopItem = [&](int top)
-> std::vector<not_null<Element*>>::const_iterator {
if (!width() || _items.empty()) {
return end(_items);
}
const auto first = ranges::lower_bound(
_items,
top,
std::less<>(),
&Element::y);
return (first == end(_items) || (*first)->y() > top)
? first - 1
: first;
};
const auto findView = [&](int top)
-> std::pair<std::vector<not_null<Element*>>::const_iterator, int> {
if (const auto i = findScrollTopItem(top); i != end(_items)) {
return { i, top - (*i)->y() };
}
return { end(_items), 0 };
};
auto [view, offset] = findView(top);
while (view != end(_items) && !(*view)->data()->isRegular()) {
offset -= (*view)->height();
++view;
}
return { (view != end(_items)) ? view->get() : nullptr, offset };
}
int ListWidget::itemMinimalHeight() const {
return st::msgMarginTopAttached
+ st::msgPhotoSize
@ -2712,7 +2743,9 @@ void ListWidget::mouseReleaseEvent(QMouseEvent *e) {
void ListWidget::touchScrollUpdated(const QPoint &screenPos) {
_touchPos = screenPos;
_delegate->listScrollTo(_visibleTop - (_touchPos - _touchPrevPos).y());
_delegate->listScrollTo(
_visibleTop - (_touchPos - _touchPrevPos).y(),
false);
touchUpdateSpeed();
}
@ -3083,15 +3116,8 @@ void ListWidget::mouseActionFinish(
mouseActionCancel();
ActivateClickHandler(window(), activated, {
button,
QVariant::fromValue(ClickHandlerContext{
.itemId = pressState.itemId,
.elementDelegate = [weak = Ui::MakeWeak(this)] {
return weak
? (ElementDelegate*)weak
: nullptr;
},
.sessionWindow = base::make_weak(_controller),
})
QVariant::fromValue(
prepareClickHandlerContext(pressState.itemId))
});
return;
}
@ -3139,6 +3165,18 @@ void ListWidget::mouseActionFinish(
}
}
ClickHandlerContext ListWidget::prepareClickHandlerContext(FullMsgId id) {
return {
.itemId = id,
.elementDelegate = [weak = Ui::MakeWeak(this)] {
return weak
? (ElementDelegate*)weak
: nullptr;
},
.sessionWindow = base::make_weak(_controller),
};
}
void ListWidget::mouseActionUpdate() {
auto mousePosition = mapFromGlobal(_mousePosition);
auto point = QPoint(

View File

@ -18,6 +18,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_view_highlight_manager.h"
#include "history/history_view_top_toast.h"
struct ClickHandlerContext;
namespace Main {
class Session;
} // namespace Main
@ -87,7 +89,7 @@ using SelectedItems = std::vector<SelectedItem>;
class ListDelegate {
public:
virtual Context listContext() = 0;
virtual bool listScrollTo(int top) = 0; // true if scroll was changed.
virtual bool listScrollTo(int top, bool syntetic = true) = 0;
virtual void listCancelRequest() = 0;
virtual void listDeleteRequest() = 0;
virtual rpl::producer<Data::MessagesSlice> listSource(
@ -246,6 +248,11 @@ public:
[[nodiscard]] bool showCopyRestrictionForSelected();
[[nodiscard]] bool hasSelectRestriction() const;
[[nodiscard]] std::pair<Element*, int> findViewForPinnedTracking(
int top) const;
[[nodiscard]] ClickHandlerContext prepareClickHandlerContext(
FullMsgId id);
// AbstractTooltipShower interface
QString tooltipText() const override;
QPoint tooltipPos() const override;

View File

@ -59,12 +59,12 @@ namespace {
} // namespace
PinnedMemento::PinnedMemento(
not_null<History*> history,
not_null<Data::Thread*> thread,
UniversalMsgId highlightId)
: _history(history)
: _thread(thread)
, _highlightId(highlightId) {
_list.setAroundPosition({
.fullId = FullMsgId(history->peer->id, highlightId),
.fullId = FullMsgId(_thread->owningHistory()->peer->id, highlightId),
.date = TimeId(0),
});
}
@ -80,7 +80,7 @@ object_ptr<Window::SectionWidget> PinnedMemento::createWidget(
auto result = object_ptr<PinnedWidget>(
parent,
controller,
_history);
_thread);
result->setInternalState(geometry, this);
return result;
}
@ -88,10 +88,13 @@ object_ptr<Window::SectionWidget> PinnedMemento::createWidget(
PinnedWidget::PinnedWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<History*> history)
: Window::SectionWidget(parent, controller, history->peer)
, _history(history->migrateToOrMe())
, _migratedPeer(_history->peer->migrateFrom())
not_null<Data::Thread*> thread)
: Window::SectionWidget(parent, controller, thread->owningHistory()->peer)
, _thread(thread->migrateToOrMe())
, _history(thread->owningHistory())
, _migratedPeer(thread->asHistory()
? thread->asHistory()->peer->migrateFrom()
: nullptr)
, _topBar(this, controller)
, _topBarShadow(this)
, _scroll(std::make_unique<Ui::ScrollArea>(
@ -113,7 +116,7 @@ PinnedWidget::PinnedWidget(
Window::ChatThemeValueFromPeer(
controller,
history->peer
thread->owningHistory()->peer
) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {
_theme = std::move(theme);
controller->setChatStyleTheme(_theme);
@ -121,7 +124,7 @@ PinnedWidget::PinnedWidget(
_topBar->setActiveChat(
TopBarWidget::ActiveChat{
.key = _history,
.key = _thread,
.section = Dialogs::EntryState::Section::Pinned,
},
nullptr);
@ -181,9 +184,10 @@ void PinnedWidget::setupClearButton() {
Window::HidePinnedBar(
controller(),
_history->peer,
_thread->topicRootId(),
crl::guard(this, callback));
} else {
Window::UnpinAllMessages(controller(), _history);
Window::UnpinAllMessages(controller(), _thread);
}
});
}
@ -194,7 +198,7 @@ void PinnedWidget::cornerButtonsShowAtPosition(
}
Data::Thread *PinnedWidget::cornerButtonsThread() {
return _history;
return _thread;
}
FullMsgId PinnedWidget::cornerButtonsCurrentId() {
@ -238,13 +242,13 @@ void PinnedWidget::updateAdaptiveLayout() {
_topBar->height());
}
not_null<History*> PinnedWidget::history() const {
return _history;
not_null<Data::Thread*> PinnedWidget::thread() const {
return _thread;
}
Dialogs::RowDescriptor PinnedWidget::activeChat() const {
return {
_history,
_thread,
FullMsgId(_history->peer->id, ShowAtUnreadMsgId)
};
}
@ -265,8 +269,8 @@ bool PinnedWidget::showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) {
if (auto logMemento = dynamic_cast<PinnedMemento*>(memento.get())) {
if (logMemento->getHistory() == history()
|| logMemento->getHistory()->migrateToOrMe() == history()) {
if (logMemento->getThread() == thread()
|| logMemento->getThread()->migrateToOrMe() == thread()) {
restoreState(logMemento);
return true;
}
@ -283,7 +287,7 @@ void PinnedWidget::setInternalState(
}
std::shared_ptr<Window::SectionMemento> PinnedWidget::createMemento() {
auto result = std::make_shared<PinnedMemento>(history());
auto result = std::make_shared<PinnedMemento>(thread());
saveState(result.get());
return result;
}
@ -431,7 +435,7 @@ Context PinnedWidget::listContext() {
return Context::Pinned;
}
bool PinnedWidget::listScrollTo(int top) {
bool PinnedWidget::listScrollTo(int top, bool syntetic) {
top = std::clamp(top, 0, _scroll->scrollTopMax());
if (_scroll->scrollTop() == top) {
updateInnerVisibleArea();
@ -462,11 +466,11 @@ rpl::producer<Data::MessagesSlice> PinnedWidget::listSource(
: (ServerMaxMsgId - 1);
return SharedMediaMergedViewer(
&_history->session(),
&_thread->session(),
SharedMediaMergedKey(
SparseIdsMergedSlice::Key(
_history->peer->id,
MsgId(0), // topicRootId
_thread->topicRootId(),
_migratedPeer ? _migratedPeer->id : 0,
messageId),
Storage::SharedMediaType::Pinned),

View File

@ -42,10 +42,10 @@ public:
PinnedWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<History*> history);
not_null<Data::Thread*> thread);
~PinnedWidget();
[[nodiscard]] not_null<History*> history() const;
[[nodiscard]] not_null<Data::Thread*> thread() const;
Dialogs::RowDescriptor activeChat() const override;
bool hasTopBarShadow() const override {
@ -79,7 +79,7 @@ public:
// ListDelegate interface.
Context listContext() override;
bool listScrollTo(int top) override;
bool listScrollTo(int top, bool syntetic = true) override;
void listCancelRequest() override;
void listDeleteRequest() override;
rpl::producer<Data::MessagesSlice> listSource(
@ -165,6 +165,7 @@ private:
void setMessagesCount(int count);
void refreshClearButtonText();
const not_null<Data::Thread*> _thread;
const not_null<History*> _history;
std::shared_ptr<Ui::ChatTheme> _theme;
PeerData *_migratedPeer = nullptr;
@ -187,7 +188,7 @@ public:
using UniversalMsgId = MsgId;
explicit PinnedMemento(
not_null<History*> history,
not_null<Data::Thread*> thread,
UniversalMsgId highlightId = 0);
object_ptr<Window::SectionWidget> createWidget(
@ -196,8 +197,8 @@ public:
Window::Column column,
const QRect &geometry) override;
[[nodiscard]] not_null<History*> getHistory() const {
return _history;
[[nodiscard]] not_null<Data::Thread*> getThread() const {
return _thread;
}
[[nodiscard]] not_null<ListMemento*> list() {
@ -208,7 +209,7 @@ public:
}
private:
const not_null<History*> _history;
const not_null<Data::Thread*> _thread;
const UniversalMsgId _highlightId = 0;
ListMemento _list;

View File

@ -27,24 +27,26 @@ constexpr auto kChangeViewerLimit = 2;
} // namespace
PinnedTracker::PinnedTracker(not_null<History*> history)
: _history(history->migrateToOrMe())
, _migratedPeer(_history->peer->migrateFrom()) {
PinnedTracker::PinnedTracker(not_null<Data::Thread*> thread)
: _thread(thread->migrateToOrMe())
, _migratedPeer(_thread->asHistory()
? _thread->asHistory()->peer->migrateFrom()
: nullptr) {
using namespace rpl::mappers;
const auto has = [&](History *history) -> rpl::producer<bool> {
auto &changes = _history->session().changes();
const auto flag = Data::HistoryUpdate::Flag::PinnedMessages;
if (!history) {
const auto has = [&](Data::Thread *thread) -> rpl::producer<bool> {
auto &changes = _thread->session().changes();
const auto flag = Data::EntryUpdate::Flag::HasPinnedMessages;
if (!thread) {
return rpl::single(false);
}
return changes.historyFlagsValue(history, flag) | rpl::map([=] {
return history->hasPinnedMessages();
return changes.entryFlagsValue(thread, flag) | rpl::map([=] {
return thread->hasPinnedMessages();
});
};
rpl::combine(
has(_history),
has(_thread),
has(_migratedPeer
? _history->owner().history(_migratedPeer).get()
? _thread->owner().history(_migratedPeer).get()
: nullptr),
_1 || _2
) | rpl::distinct_until_changed(
@ -77,12 +79,13 @@ void PinnedTracker::refreshViewer() {
}
_dataLifetime.destroy();
_viewerAroundId = _aroundId;
const auto peer = _thread->owningHistory()->peer;
SharedMediaMergedViewer(
&_history->peer->session(),
&peer->session(),
SharedMediaMergedKey(
SparseIdsMergedSlice::Key(
_history->peer->id,
MsgId(0), // topicRootId
peer->id,
_thread->topicRootId(),
_migratedPeer ? _migratedPeer->id : 0,
_viewerAroundId),
Storage::SharedMediaType::Pinned),
@ -100,9 +103,9 @@ void PinnedTracker::refreshViewer() {
}
refreshCurrentFromSlice();
if (_slice.fullCount == 0) {
_history->setHasPinnedMessages(false);
_thread->setHasPinnedMessages(false);
if (_migratedPeer) {
const auto to = _history->owner().history(_migratedPeer);
const auto to = _thread->owner().history(_migratedPeer);
to->setHasPinnedMessages(false);
}
}

View File

@ -13,6 +13,7 @@ class History;
namespace Data {
enum class LoadDirection : char;
class Thread;
} // namespace Data
namespace HistoryView {
@ -21,7 +22,7 @@ class PinnedTracker final {
public:
using UniversalMsgId = MsgId;
explicit PinnedTracker(not_null<History*> history);
explicit PinnedTracker(not_null<Data::Thread*> thread);
~PinnedTracker();
[[nodiscard]] rpl::producer<PinnedId> shownMessageId() const;
@ -44,7 +45,7 @@ private:
void refreshViewer();
void refreshCurrentFromSlice();
const not_null<History*> _history;
const not_null<Data::Thread*> _thread;
PeerData *_migratedPeer = nullptr;
rpl::variable<PinnedId> _current;

View File

@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_contact_status.h"
#include "history/view/history_view_service_message.h"
#include "history/view/history_view_pinned_tracker.h"
#include "history/view/history_view_pinned_section.h"
#include "history/history.h"
#include "history/history_drag_area.h"
#include "history/history_item_components.h"
@ -27,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/chat_style.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/layers/generic_box.h"
#include "ui/item_text_options.h"
@ -38,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
#include "ui/toasts/common_toasts.h"
#include "base/timer_rpl.h"
#include "api/api_bot.h"
#include "api/api_common.h"
#include "api/api_editing.h"
#include "api/api_sending.h"
@ -57,7 +61,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/file_utilities.h"
#include "core/application.h"
#include "core/shortcuts.h"
#include "core/click_handler_types.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "mainwidget.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -68,10 +74,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_replies_list.h"
#include "data/data_peer_values.h"
#include "data/data_changes.h"
#include "data/data_shared_media.h"
#include "data/data_send_action.h"
#include "storage/storage_media_prepare.h"
#include "storage/storage_shared_media.h"
#include "storage/storage_account.h"
#include "inline_bots/inline_bot_result.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "facades.h"
#include "styles/style_chat.h"
@ -270,6 +279,9 @@ RepliesWidget::RepliesWidget(
if (_rootView) {
_rootView->raise();
}
if (_pinnedBar) {
_pinnedBar->raise();
}
if (_topicReopenBar) {
_topicReopenBar->bar().raise();
}
@ -358,9 +370,13 @@ RepliesWidget::RepliesWidget(
}, lifetime());
}
setupComposeControls();
setupTopicViewer();
setupComposeControls();
orderWidgets();
if (_pinnedBar) {
_pinnedBar->finishAnimating();
}
}
RepliesWidget::~RepliesWidget() {
@ -384,6 +400,9 @@ void RepliesWidget::orderWidgets() {
if (_rootView) {
_rootView->raise();
}
if (_pinnedBar) {
_pinnedBar->raise();
}
_topBarShadow->raise();
_composeControls->raisePanels();
}
@ -483,9 +502,11 @@ void RepliesWidget::subscribeToTopic() {
) | rpl::start_with_next([=] {
const auto height = _topicReopenBar->bar().height();
_scrollTopDelta = (height - _topicReopenBarHeight);
_topicReopenBarHeight = height;
updateControlsGeometry();
_scrollTopDelta = 0;
if (_scrollTopDelta) {
_topicReopenBarHeight = height;
updateControlsGeometry();
_scrollTopDelta = 0;
}
}, _topicReopenBar->bar().lifetime());
using Flag = Data::TopicUpdate::Flag;
@ -510,6 +531,21 @@ void RepliesWidget::subscribeToTopic() {
anim::activation::background));
}, _topicLifetime);
if (!_topic->creating()) {
using EntryUpdateFlag = Data::EntryUpdate::Flag;
session().changes().entryUpdates(
EntryUpdateFlag::HasPinnedMessages
) | rpl::start_with_next([=](const Data::EntryUpdate &update) {
if (_pinnedTracker
&& (update.flags & EntryUpdateFlag::HasPinnedMessages)
&& (_topic == update.entry.get())) {
checkPinnedBarState();
}
}, lifetime());
setupPinnedTracker();
}
_cornerButtons.updateUnreadThingsVisibility();
}
@ -1408,6 +1444,275 @@ void RepliesWidget::refreshUnreadCountBadge(std::optional<int> count) {
}
}
void RepliesWidget::updatePinnedViewer() {
if (_scroll->isHidden() || !_topic) {
return;
}
const auto visibleBottom = _scroll->scrollTop() + _scroll->height();
auto [view, offset] = _inner->findViewForPinnedTracking(visibleBottom);
const auto lessThanId = !view
? (ServerMaxMsgId - 1)
: (view->data()->id + (offset > 0 ? 1 : 0));
const auto lastClickedId = !_pinnedClickedId
? (ServerMaxMsgId - 1)
: _pinnedClickedId.msg;
if (_pinnedClickedId
&& lessThanId <= lastClickedId
&& !_inner->animatedScrolling()) {
_pinnedClickedId = FullMsgId();
}
if (_pinnedClickedId && !_minPinnedId) {
_minPinnedId = Data::ResolveMinPinnedId(_history->peer, _rootId);
}
if (_pinnedClickedId && _minPinnedId && _minPinnedId >= _pinnedClickedId) {
// After click on the last pinned message we should the top one.
_pinnedTracker->trackAround(ServerMaxMsgId - 1);
} else {
_pinnedTracker->trackAround(std::min(lessThanId, lastClickedId));
}
}
void RepliesWidget::checkLastPinnedClickedIdReset(
int wasScrollTop,
int nowScrollTop) {
if (_scroll->isHidden() || !_topic) {
return;
}
if (wasScrollTop < nowScrollTop && _pinnedClickedId) {
// User scrolled down.
_pinnedClickedId = FullMsgId();
_minPinnedId = std::nullopt;
updatePinnedViewer();
}
}
void RepliesWidget::setupPinnedTracker() {
Expects(_topic != nullptr);
_pinnedTracker = std::make_unique<HistoryView::PinnedTracker>(_topic);
_pinnedBar = nullptr;
SharedMediaViewer(
&_topic->session(),
Storage::SharedMediaKey(
_topic->channel()->id,
_rootId,
Storage::SharedMediaType::Pinned,
ServerMaxMsgId - 1),
1,
1
) | rpl::filter([=](const SparseIdsSlice &result) {
return result.fullCount().has_value();
}) | rpl::start_with_next([=](const SparseIdsSlice &result) {
_topic->setHasPinnedMessages(*result.fullCount() != 0);
checkPinnedBarState();
}, _topicLifetime);
}
void RepliesWidget::checkPinnedBarState() {
Expects(_pinnedTracker != nullptr);
Expects(_inner != nullptr);
const auto peer = _history->peer;
const auto hiddenId = peer->canPinMessages()
? MsgId(0)
: peer->session().settings().hiddenPinnedMessageId(
peer->id,
_rootId);
const auto currentPinnedId = Data::ResolveTopPinnedId(peer, _rootId);
const auto universalPinnedId = !currentPinnedId
? int32(0)
: currentPinnedId.msg;
if (universalPinnedId == hiddenId) {
if (_pinnedBar) {
_pinnedTracker->reset();
auto qobject = base::unique_qptr{
Ui::WrapAsQObject(this, std::move(_pinnedBar)).get()
};
auto destroyer = [this, object = std::move(qobject)]() mutable {
object = nullptr;
updateControlsGeometry();
};
base::call_delayed(
st::defaultMessageBar.duration,
this,
std::move(destroyer));
}
return;
}
if (_pinnedBar || !universalPinnedId) {
return;
}
_pinnedBar = std::make_unique<Ui::PinnedBar>(this, [=] {
return controller()->isGifPausedAtLeastFor(
Window::GifPauseReason::Any);
});
auto pinnedRefreshed = Info::Profile::SharedMediaCountValue(
_history->peer,
_rootId,
nullptr,
Storage::SharedMediaType::Pinned
) | rpl::distinct_until_changed(
) | rpl::map([=](int count) {
if (_pinnedClickedId) {
_pinnedClickedId = FullMsgId();
_minPinnedId = std::nullopt;
updatePinnedViewer();
}
return (count > 1);
}) | rpl::distinct_until_changed();
auto markupRefreshed = HistoryView::PinnedBarItemWithReplyMarkup(
&session(),
_pinnedTracker->shownMessageId());
rpl::combine(
rpl::duplicate(pinnedRefreshed),
rpl::duplicate(markupRefreshed)
) | rpl::start_with_next([=](bool many, HistoryItem *item) {
refreshPinnedBarButton(many, item);
}, _pinnedBar->lifetime());
_pinnedBar->setContent(rpl::combine(
HistoryView::PinnedBarContent(
&session(),
_pinnedTracker->shownMessageId(),
[bar = _pinnedBar.get()] { bar->customEmojiRepaint(); }),
std::move(pinnedRefreshed),
std::move(markupRefreshed)
) | rpl::map([](Ui::MessageBarContent &&content, bool, HistoryItem*) {
return std::move(content);
}));
controller()->adaptive().oneColumnValue(
) | rpl::start_with_next([=](bool one) {
_pinnedBar->setShadowGeometryPostprocess([=](QRect geometry) {
if (!one) {
geometry.setLeft(geometry.left() + st::lineWidth);
}
return geometry;
});
}, _pinnedBar->lifetime());
_pinnedBar->barClicks(
) | rpl::start_with_next([=] {
const auto id = _pinnedTracker->currentMessageId();
if (const auto item = session().data().message(id.message)) {
showAtPosition(item->position());
if (const auto group = session().data().groups().find(item)) {
// Hack for the case when a non-first item of an album
// is pinned and we still want the 'show last after first'.
_pinnedClickedId = group->items.front()->fullId();
} else {
_pinnedClickedId = id.message;
}
_minPinnedId = std::nullopt;
updatePinnedViewer();
}
}, _pinnedBar->lifetime());
_pinnedBarHeight = 0;
_pinnedBar->heightValue(
) | rpl::start_with_next([=](int height) {
if (const auto delta = height - _pinnedBarHeight) {
_pinnedBarHeight = height;
setGeometryWithTopMoved(geometry(), delta);
}
}, _pinnedBar->lifetime());
orderWidgets();
if (animatingShow()) {
_pinnedBar->hide();
}
}
void RepliesWidget::refreshPinnedBarButton(bool many, HistoryItem *item) {
const auto openSection = [=] {
const auto id = _pinnedTracker
? _pinnedTracker->currentMessageId()
: HistoryView::PinnedId();
if (!id.message) {
return;
}
controller()->showSection(
std::make_shared<PinnedMemento>(_topic, id.message.msg));
};
if (const auto replyMarkup = item ? item->inlineReplyMarkup() : nullptr) {
const auto &rows = replyMarkup->data.rows;
if ((rows.size() == 1) && (rows.front().size() == 1)) {
const auto text = rows.front().front().text;
if (!text.isEmpty()) {
auto button = object_ptr<Ui::RoundButton>(
this,
rpl::single(text),
st::historyPinnedBotButton);
button->setTextTransform(
Ui::RoundButton::TextTransform::NoTransform);
button->setFullRadius(true);
button->setClickedCallback([=] {
Api::ActivateBotCommand(
_inner->prepareClickHandlerContext(item->fullId()),
0,
0);
});
if (button->width() > st::historyPinnedBotButtonMaxWidth) {
button->setFullWidth(st::historyPinnedBotButtonMaxWidth);
}
struct State {
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto state = button->lifetime().make_state<State>();
_pinnedBar->contextMenuRequested(
) | rpl::start_with_next([=, raw = button.data()] {
state->menu = base::make_unique_q<Ui::PopupMenu>(raw);
state->menu->addAction(
tr::lng_settings_events_pinned(tr::now),
openSection);
state->menu->popup(QCursor::pos());
}, button->lifetime());
_pinnedBar->setRightButton(std::move(button));
return;
}
}
}
const auto close = !many;
auto button = object_ptr<Ui::IconButton>(
this,
close ? st::historyReplyCancel : st::historyPinnedShowAll);
button->clicks(
) | rpl::start_with_next([=] {
if (close) {
hidePinnedMessage();
} else {
openSection();
}
}, button->lifetime());
_pinnedBar->setRightButton(std::move(button));
}
void RepliesWidget::hidePinnedMessage() {
Expects(_pinnedBar != nullptr);
const auto id = _pinnedTracker->currentMessageId();
if (!id.message) {
return;
}
if (_history->peer->canPinMessages()) {
Window::ToggleMessagePinned(controller(), id.message, false);
} else {
const auto callback = [=] {
if (_pinnedTracker) {
checkPinnedBarState();
}
};
Window::HidePinnedBar(
controller(),
_history->peer,
_rootId,
crl::guard(this, callback));
}
}
void RepliesWidget::cornerButtonsShowAtPosition(
Data::MessagePosition position) {
showAtPosition(position);
@ -1538,6 +1843,9 @@ QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams &pa
if (_rootView) {
_rootView->hide();
}
if (_pinnedBar) {
_pinnedBar->hide();
}
return result;
}
@ -1734,13 +2042,18 @@ void RepliesWidget::updateControlsGeometry() {
if (_rootView) {
_rootView->resizeToWidth(contentWidth);
}
if (_pinnedBar) {
_pinnedBar->move(0, _topBar->height());
_pinnedBar->resizeToWidth(contentWidth);
}
const auto bottom = height();
const auto controlsHeight = _joinGroup
? _joinGroup->height()
: _composeControls->heightCurrent();
auto top = _topBar->height()
+ _rootViewHeight;
+ _rootViewHeight
+ _pinnedBarHeight;
if (_topicReopenBar) {
_topicReopenBar->bar().move(0, top);
top += _topicReopenBar->bar().height();
@ -1807,8 +2120,15 @@ void RepliesWidget::updateInnerVisibleArea() {
const auto scrollTop = _scroll->scrollTop();
_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
updatePinnedVisibility();
updatePinnedViewer();
_cornerButtons.updateJumpDownVisibility();
_cornerButtons.updateUnreadThingsVisibility();
if (_lastScrollTop != scrollTop) {
if (!_synteticScrollEvent) {
checkLastPinnedClickedIdReset(_lastScrollTop, scrollTop);
}
_lastScrollTop = scrollTop;
}
}
void RepliesWidget::updatePinnedVisibility() {
@ -1875,6 +2195,9 @@ void RepliesWidget::showFinishedHook() {
if (_rootView) {
_rootView->show();
}
if (_pinnedBar) {
_pinnedBar->show();
}
// We should setup the drag area only after
// the section animation is finished,
@ -1895,14 +2218,17 @@ Context RepliesWidget::listContext() {
return Context::Replies;
}
bool RepliesWidget::listScrollTo(int top) {
bool RepliesWidget::listScrollTo(int top, bool syntetic) {
top = std::clamp(top, 0, _scroll->scrollTopMax());
if (_scroll->scrollTop() == top) {
const auto scrolled = (_scroll->scrollTop() != top);
_synteticScrollEvent = syntetic;
if (scrolled) {
_scroll->scrollToY(top);
} else if (syntetic) {
updateInnerVisibleArea();
return false;
}
_scroll->scrollToY(top);
return true;
_synteticScrollEvent = false;
return syntetic;
}
void RepliesWidget::listCancelRequest() {

View File

@ -67,6 +67,7 @@ class SendActionPainter;
class StickerToast;
class TopicReopenBar;
class EmptyPainter;
class PinnedTracker;
class RepliesWidget final
: public Window::SectionWidget
@ -119,7 +120,7 @@ public:
// ListDelegate interface.
Context listContext() override;
bool listScrollTo(int top) override;
bool listScrollTo(int top, bool syntetic = true) override;
void listCancelRequest() override;
void listDeleteRequest() override;
rpl::producer<Data::MessagesSlice> listSource(
@ -242,7 +243,15 @@ private:
void replyToMessage(FullMsgId itemId);
void refreshTopBarActiveChat();
void refreshUnreadCountBadge(std::optional<int> count);
void reloadUnreadCountIfNeeded();
void hidePinnedMessage();
void updatePinnedViewer();
void setupPinnedTracker();
void checkPinnedBarState();
void refreshPinnedBarButton(bool many, HistoryItem *item);
void checkLastPinnedClickedIdReset(
int wasScrollTop,
int nowScrollTop);
void uploadFile(const QByteArray &fileContent, SendMediaType type);
bool confirmSendingFiles(
@ -308,6 +317,13 @@ private:
std::unique_ptr<TopicReopenBar> _topicReopenBar;
std::unique_ptr<EmptyPainter> _emptyPainter;
bool _skipScrollEvent = false;
bool _synteticScrollEvent = false;
std::unique_ptr<PinnedTracker> _pinnedTracker;
std::unique_ptr<Ui::PinnedBar> _pinnedBar;
int _pinnedBarHeight = 0;
FullMsgId _pinnedClickedId;
std::optional<FullMsgId> _minPinnedId;
std::unique_ptr<Ui::PinnedBar> _rootView;
int _rootViewHeight = 0;
@ -321,6 +337,7 @@ private:
HistoryView::CornerButtons _cornerButtons;
rpl::lifetime _topicLifetime;
int _lastScrollTop = 0;
int _topicReopenBarHeight = 0;
int _scrollTopDelta = 0;

View File

@ -1050,7 +1050,7 @@ Context ScheduledWidget::listContext() {
return Context::History;
}
bool ScheduledWidget::listScrollTo(int top) {
bool ScheduledWidget::listScrollTo(int top, bool syntetic) {
top = std::clamp(top, 0, _scroll->scrollTopMax());
if (_scroll->scrollTop() == top) {
updateInnerVisibleArea();

View File

@ -101,7 +101,7 @@ public:
// ListDelegate interface.
Context listContext() override;
bool listScrollTo(int top) override;
bool listScrollTo(int top, bool syntetic = true) override;
void listCancelRequest() override;
void listDeleteRequest() override;
rpl::producer<Data::MessagesSlice> listSource(

View File

@ -489,9 +489,10 @@ void TopBarWidget::paintTopBar(Painter &p) {
}
const auto now = crl::now();
const auto history = _activeChat.key.history();
const auto history = _activeChat.key.owningHistory();
const auto folder = _activeChat.key.folder();
if (const auto topic = _activeChat.key.topic()) {
const auto topic = _activeChat.key.topic();
if (topic && _activeChat.section == Section::Replies) {
p.setPen(st::dialogsNameFg);
topic->chatListNameText().drawElided(
p,

View File

@ -41,48 +41,56 @@ QByteArray SessionSettings::serialize() const {
+ sizeof(qint32) * 5
+ _mediaLastPlaybackPosition.size() * 2 * sizeof(quint64)
+ sizeof(qint32) * 5
+ _hiddenPinnedMessages.size() * (sizeof(quint64) + sizeof(qint32))
+ sizeof(qint32)
+ (_mutePeriods.size() * sizeof(quint64))
+ sizeof(qint32);
+ sizeof(qint32) * 2
+ _hiddenPinnedMessages.size() * (sizeof(quint64) * 3);
auto result = QByteArray();
result.reserve(size);
{
QDataStream stream(&result, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_1);
stream << qint32(kVersionTag) << qint32(kVersion);
stream << static_cast<qint32>(_selectorTab);
stream << qint32(_groupStickersSectionHidden.size());
stream
<< qint32(kVersionTag) << qint32(kVersion)
<< static_cast<qint32>(_selectorTab)
<< qint32(_groupStickersSectionHidden.size());
for (const auto &peerId : _groupStickersSectionHidden) {
stream << SerializePeerId(peerId);
}
stream << qint32(_supportSwitch);
stream << qint32(_supportFixChatsOrder ? 1 : 0);
stream << qint32(_supportTemplatesAutocomplete ? 1 : 0);
stream << qint32(_supportChatsTimeSlice.current());
stream << autoDownload;
stream << qint32(_supportAllSearchResults.current() ? 1 : 0);
stream << qint32(_archiveCollapsed.current() ? 1 : 0);
stream << qint32(_archiveInMainMenu.current() ? 1 : 0);
stream << qint32(_skipArchiveInSearch.current() ? 1 : 0);
stream << qint32(_mediaLastPlaybackPosition.size());
stream
<< qint32(_supportSwitch)
<< qint32(_supportFixChatsOrder ? 1 : 0)
<< qint32(_supportTemplatesAutocomplete ? 1 : 0)
<< qint32(_supportChatsTimeSlice.current())
<< autoDownload
<< qint32(_supportAllSearchResults.current() ? 1 : 0)
<< qint32(_archiveCollapsed.current() ? 1 : 0)
<< qint32(_archiveInMainMenu.current() ? 1 : 0)
<< qint32(_skipArchiveInSearch.current() ? 1 : 0)
<< qint32(_mediaLastPlaybackPosition.size());
for (const auto &[id, time] : _mediaLastPlaybackPosition) {
stream << quint64(id) << qint64(time);
}
stream << qint32(0);
stream << qint32(_dialogsFiltersEnabled ? 1 : 0);
stream << qint32(_supportAllSilent ? 1 : 0);
stream << qint32(_photoEditorHintShowsCount);
stream << qint32(_hiddenPinnedMessages.size());
for (const auto &[key, value] : _hiddenPinnedMessages) {
stream << SerializePeerId(key) << qint64(value.bare);
}
stream << qint32(_mutePeriods.size());
stream
<< qint32(0) // very old _hiddenPinnedMessages.size());
<< qint32(_dialogsFiltersEnabled ? 1 : 0)
<< qint32(_supportAllSilent ? 1 : 0)
<< qint32(_photoEditorHintShowsCount)
<< qint32(0) // old _hiddenPinnedMessages.size());
<< qint32(_mutePeriods.size());
for (const auto &period : _mutePeriods) {
stream << quint64(period);
}
stream << qint32(_skipPremiumStickersSet ? 1 : 0);
stream
<< qint32(_skipPremiumStickersSet ? 1 : 0)
<< qint32(_hiddenPinnedMessages.size());
for (const auto &[key, value] : _hiddenPinnedMessages) {
stream
<< SerializePeerId(key.peerId)
<< qint64(key.topicRootId.bare)
<< qint64(value.bare);
}
}
return result;
}
@ -139,7 +147,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) {
QByteArray appVideoPipGeometry = app.videoPipGeometry();
std::vector<int> appDictionariesEnabled;
qint32 appAutoDownloadDictionaries = app.autoDownloadDictionaries() ? 1 : 0;
base::flat_map<PeerId, MsgId> hiddenPinnedMessages;
base::flat_map<ThreadId, MsgId> hiddenPinnedMessages;
qint32 dialogsFiltersEnabled = _dialogsFiltersEnabled ? 1 : 0;
qint32 supportAllSilent = _supportAllSilent ? 1 : 0;
qint32 photoEditorHintShowsCount = _photoEditorHintShowsCount;
@ -334,7 +342,9 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) {
"Bad data for SessionSettings::addFromSerialized()"));
return;
}
hiddenPinnedMessages.emplace(DeserializePeerId(key), value);
hiddenPinnedMessages.emplace(
ThreadId{ DeserializePeerId(key), MsgId(0) },
value);
}
}
}
@ -360,7 +370,9 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) {
"Bad data for SessionSettings::addFromSerialized()"));
return;
}
hiddenPinnedMessages.emplace(DeserializePeerId(key), value);
hiddenPinnedMessages.emplace(
ThreadId{ DeserializePeerId(key), MsgId(0) },
value);
}
}
}
@ -378,6 +390,26 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> skipPremiumStickersSet;
}
if (!stream.atEnd()) {
auto count = qint32(0);
stream >> count;
if (stream.status() == QDataStream::Ok) {
for (auto i = 0; i != count; ++i) {
auto keyPeerId = quint64();
auto keyTopicRootId = qint64();
auto value = qint64();
stream >> keyPeerId >> keyTopicRootId >> value;
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for SessionSettings::addFromSerialized()"));
return;
}
hiddenPinnedMessages.emplace(
ThreadId{ DeserializePeerId(keyPeerId), keyTopicRootId },
value);
}
}
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for SessionSettings::addFromSerialized()"));
@ -561,6 +593,25 @@ rpl::producer<bool> SessionSettings::skipArchiveInSearchChanges() const {
return _skipArchiveInSearch.changes();
}
MsgId SessionSettings::hiddenPinnedMessageId(
PeerId peerId,
MsgId topicRootId) const {
const auto i = _hiddenPinnedMessages.find({ peerId, topicRootId });
return (i != end(_hiddenPinnedMessages)) ? i->second : 0;
}
void SessionSettings::setHiddenPinnedMessageId(
PeerId peerId,
MsgId topicRootId,
MsgId msgId) {
const auto id = ThreadId{ peerId, topicRootId };
if (msgId) {
_hiddenPinnedMessages[id] = msgId;
} else {
_hiddenPinnedMessages.remove(id);
}
}
bool SessionSettings::photoEditorHintShown() const {
return _photoEditorHintShowsCount < kPhotoEditorHintMaxShowsCount;
}

View File

@ -101,17 +101,13 @@ public:
return _hadLegacyCallsPeerToPeerNobody;
}
[[nodiscard]] MsgId hiddenPinnedMessageId(PeerId peerId) const {
const auto i = _hiddenPinnedMessages.find(peerId);
return (i != end(_hiddenPinnedMessages)) ? i->second : 0;
}
void setHiddenPinnedMessageId(PeerId peerId, MsgId msgId) {
if (msgId) {
_hiddenPinnedMessages[peerId] = msgId;
} else {
_hiddenPinnedMessages.remove(peerId);
}
}
[[nodiscard]] MsgId hiddenPinnedMessageId(
PeerId peerId,
MsgId topicRootId = 0) const;
void setHiddenPinnedMessageId(
PeerId peerId,
MsgId topicRootId,
MsgId msgId);
[[nodiscard]] bool dialogsFiltersEnabled() const {
return _dialogsFiltersEnabled;
@ -137,6 +133,15 @@ private:
static constexpr auto kDefaultSupportChatsLimitSlice = 7 * 24 * 60 * 60;
static constexpr auto kPhotoEditorHintMaxShowsCount = 5;
struct ThreadId {
PeerId peerId;
MsgId topicRootId;
friend inline constexpr auto operator<=>(
ThreadId,
ThreadId) = default;
};
ChatHelpers::SelectorTab _selectorTab; // per-window
base::flat_set<PeerId> _groupStickersSectionHidden;
bool _hadLegacyCallsPeerToPeerNobody = false;
@ -145,7 +150,7 @@ private:
rpl::variable<bool> _archiveInMainMenu = false;
rpl::variable<bool> _skipArchiveInSearch = false;
std::vector<std::pair<DocumentId, crl::time>> _mediaLastPlaybackPosition;
base::flat_map<PeerId, MsgId> _hiddenPinnedMessages;
base::flat_map<ThreadId, MsgId> _hiddenPinnedMessages;
bool _dialogsFiltersEnabled = false;
int _photoEditorHintShowsCount = 0;
std::vector<TimeId> _mutePeriods;

View File

@ -1082,6 +1082,7 @@ bool ReadSetting(
for (auto i = v.begin(), e = v.end(); i != e; ++i) {
context.sessionSettings().setHiddenPinnedMessageId(
DeserializePeerId(i.key()),
MsgId(0), // topicRootId
MsgId(i.value()));
}
context.legacyRead = true;

View File

@ -20,6 +20,7 @@ public:
void remove(SharedMediaRemoveOne &&query);
void remove(SharedMediaRemoveAll &&query);
void invalidate(SharedMediaInvalidateBottom &&query);
void unload(SharedMediaUnloadThread &&query);
rpl::producer<SharedMediaResult> query(SharedMediaQuery &&query) const;
SharedMediaResult snapshot(const SharedMediaQuery &query) const;
bool empty(const SharedMediaKey &key) const;
@ -65,6 +66,10 @@ void Facade::Impl::invalidate(SharedMediaInvalidateBottom &&query) {
_sharedMedia.invalidate(std::move(query));
}
void Facade::Impl::unload(SharedMediaUnloadThread &&query) {
_sharedMedia.unload(std::move(query));
}
rpl::producer<SharedMediaResult> Facade::Impl::query(SharedMediaQuery &&query) const {
return _sharedMedia.query(std::move(query));
}
@ -144,6 +149,10 @@ void Facade::invalidate(SharedMediaInvalidateBottom &&query) {
_impl->invalidate(std::move(query));
}
void Facade::unload(SharedMediaUnloadThread &&query) {
_impl->unload(std::move(query));
}
rpl::producer<SharedMediaResult> Facade::query(SharedMediaQuery &&query) const {
return _impl->query(std::move(query));
}

View File

@ -24,6 +24,7 @@ struct SharedMediaAddSlice;
struct SharedMediaRemoveOne;
struct SharedMediaRemoveAll;
struct SharedMediaInvalidateBottom;
struct SharedMediaUnloadThread;
struct SharedMediaQuery;
struct SharedMediaKey;
using SharedMediaResult = SparseIdsListResult;
@ -47,6 +48,7 @@ public:
void remove(SharedMediaRemoveOne &&query);
void remove(SharedMediaRemoveAll &&query);
void invalidate(SharedMediaInvalidateBottom &&query);
void unload(SharedMediaUnloadThread &&query);
rpl::producer<SharedMediaResult> query(SharedMediaQuery &&query) const;
SharedMediaResult snapshot(const SharedMediaQuery &query) const;

View File

@ -109,6 +109,10 @@ void SharedMedia::invalidate(SharedMediaInvalidateBottom &&query) {
_bottomInvalidated.fire(std::move(query));
}
void SharedMedia::unload(SharedMediaUnloadThread &&query) {
_lists.erase({ query.peerId, query.topicRootId });
}
rpl::producer<SharedMediaResult> SharedMedia::query(SharedMediaQuery &&query) const {
Expects(IsValidSharedMediaType(query.key.type));

View File

@ -197,6 +197,18 @@ struct SharedMediaSliceUpdate {
SparseIdsSliceUpdate data;
};
struct SharedMediaUnloadThread {
SharedMediaUnloadThread(
PeerId peerId,
MsgId topicRootId)
: peerId(peerId)
, topicRootId(topicRootId) {
}
PeerId peerId = 0;
MsgId topicRootId = 0;
};
class SharedMedia {
public:
using Type = SharedMediaType;
@ -207,6 +219,7 @@ public:
void remove(SharedMediaRemoveOne &&query);
void remove(SharedMediaRemoveAll &&query);
void invalidate(SharedMediaInvalidateBottom &&query);
void unload(SharedMediaUnloadThread &&query);
rpl::producer<SharedMediaResult> query(SharedMediaQuery &&query) const;
SharedMediaResult snapshot(const SharedMediaQuery &query) const;

View File

@ -1651,19 +1651,26 @@ void ToggleMessagePinned(
void HidePinnedBar(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId,
Fn<void()> onHidden) {
const auto callback = crl::guard(navigation, [=](Fn<void()> &&close) {
close();
auto &session = peer->session();
const auto migrated = peer->migrateFrom();
const auto top = Data::ResolveTopPinnedId(peer, MsgId(0), migrated);
const auto migrated = topicRootId ? nullptr : peer->migrateFrom();
const auto top = Data::ResolveTopPinnedId(
peer,
topicRootId,
migrated);
const auto universal = !top
? MsgId(0)
: (migrated && !peerIsChannel(top.peer))
? (top.msg - ServerMaxMsgId)
: top.msg;
if (universal) {
session.settings().setHiddenPinnedMessageId(peer->id, universal);
session.settings().setHiddenPinnedMessageId(
peer->id,
topicRootId,
universal);
session.saveSettingsDelayed();
if (onHidden) {
onHidden();
@ -1683,11 +1690,18 @@ void HidePinnedBar(
void UnpinAllMessages(
not_null<Window::SessionNavigation*> navigation,
not_null<History*> history) {
not_null<Data::Thread*> thread) {
const auto weak = base::make_weak(thread);
const auto callback = crl::guard(navigation, [=](Fn<void()> &&close) {
close();
const auto api = &history->session().api();
const auto strong = weak.get();
if (!strong || !strong->asHistory()) { // #TODO forum pinned
return;
}
const auto api = &strong->session().api();
const auto sendRequest = [=](auto self) -> void {
const auto history = strong->owningHistory();
const auto topicRootId = strong->topicRootId();
api->request(MTPmessages_UnpinAllMessages(
history->peer->input
)).done([=](const MTPmessages_AffectedHistory &result) {
@ -1696,7 +1710,7 @@ void UnpinAllMessages(
if (offset > 0) {
self(self);
} else {
history->unpinAllMessages();
history->unpinMessagesFor(topicRootId);
}
}).send();
};

View File

@ -25,6 +25,7 @@ class Folder;
class Session;
struct ForwardDraft;
class ForumTopic;
class Thread;
} // namespace Data
namespace Dialogs {
@ -130,9 +131,10 @@ void ToggleMessagePinned(
void HidePinnedBar(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId,
Fn<void()> onHidden);
void UnpinAllMessages(
not_null<Window::SessionNavigation*> navigation,
not_null<History*> history);
not_null<Data::Thread*> thread);
} // namespace Window