init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,399 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/media/info_media_buttons.h"
#include "base/call_delayed.h"
#include "base/qt/qt_key_modifiers.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "data/components/recent_shared_media_gifts.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_stories_ids.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/history.h"
#include "history/view/history_view_chat_section.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "info/peer_gifts/info_peer_gifts_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/saved/info_saved_music_widget.h"
#include "info/stories/info_stories_widget.h"
#include "main/main_session.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_separate_id.h"
#include "window/window_session_controller.h"
#include "styles/style_info.h"
#include "styles/style_menu_icons.h"
namespace Info::Media {
namespace {
[[nodiscard]] bool SeparateSupported(Storage::SharedMediaType type) {
using Type = Storage::SharedMediaType;
return (type == Type::Photo)
|| (type == Type::Video)
|| (type == Type::File)
|| (type == Type::MusicFile)
|| (type == Type::Link)
|| (type == Type::RoundVoiceFile)
|| (type == Type::GIF);
}
[[nodiscard]] Window::SeparateId SeparateId(
not_null<PeerData*> peer,
MsgId topicRootId,
Storage::SharedMediaType type) {
if (peer->isSelf() || !SeparateSupported(type)) {
return { nullptr };
}
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
if (topicRootId && !topic) {
return { nullptr };
}
const auto thread = topic
? (Data::Thread*)topic
: peer->owner().history(peer);
return { thread, type };
}
void AddContextMenuToButton(
not_null<Ui::AbstractButton*> button,
Fn<void()> openInWindow) {
if (!openInWindow) {
return;
}
button->setAcceptBoth();
struct State final {
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto state = button->lifetime().make_state<State>();
button->addClickHandler([=](Qt::MouseButton mouse) {
if (mouse != Qt::RightButton) {
return;
}
state->menu = base::make_unique_q<Ui::PopupMenu>(
button.get(),
st::popupMenuWithIcons);
state->menu->addAction(tr::lng_context_new_window(tr::now), [=] {
base::call_delayed(
st::popupMenuWithIcons.showDuration,
crl::guard(button, openInWindow));
}, &st::menuIconNewWindow);
state->menu->popup(QCursor::pos());
});
}
} // namespace
tr::phrase<lngtag_count> MediaTextPhrase(Type type) {
switch (type) {
case Type::Photo: return tr::lng_profile_photos;
case Type::GIF: return tr::lng_profile_gifs;
case Type::Video: return tr::lng_profile_videos;
case Type::File: return tr::lng_profile_files;
case Type::MusicFile: return tr::lng_profile_songs;
case Type::Link: return tr::lng_profile_shared_links;
case Type::RoundVoiceFile: return tr::lng_profile_audios;
}
Unexpected("Type in MediaTextPhrase()");
};
Fn<QString(int)> MediaText(Type type) {
return [phrase = MediaTextPhrase(type)](int count) {
return phrase(tr::now, lt_count, count);
};
}
not_null<Ui::SlideWrap<Ui::SettingsButton>*> AddCountedButton(
Ui::VerticalLayout *parent,
rpl::producer<int> &&count,
Fn<QString(int)> &&textFromCount,
Ui::MultiSlideTracker &tracker) {
using namespace ::Settings;
auto forked = std::move(count)
| start_spawning(parent->lifetime());
auto text = rpl::duplicate(
forked
) | rpl::map([textFromCount](int count) {
return (count > 0)
? textFromCount(count)
: QString();
});
auto button = parent->add(object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
parent,
object_ptr<Ui::SettingsButton>(
parent,
std::move(text),
st::infoSharedMediaButton))
)->setDuration(
st::infoSlideDuration
)->toggleOn(
rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0)
);
tracker.track(button);
return button;
};
not_null<Ui::SettingsButton*> AddButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId,
PeerId monoforumPeerId,
PeerData *migrated,
Type type,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::SharedMediaCountValue(
peer,
topicRootId,
monoforumPeerId,
migrated,
type),
MediaText(type),
tracker)->entity();
const auto separateId = SeparateId(peer, topicRootId, type);
const auto openInWindow = separateId
? [=] { navigation->parentController()->showInNewWindow(separateId); }
: Fn<void()>(nullptr);
AddContextMenuToButton(result, openInWindow);
result->addClickHandler([=](Qt::MouseButton mouse) {
if (mouse == Qt::RightButton) {
return;
}
if (openInWindow
&& (base::IsCtrlPressed() || mouse == Qt::MiddleButton)) {
return openInWindow();
}
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
if (topicRootId && !topic) {
return;
}
const auto separateId = SeparateId(peer, topicRootId, type);
if (Core::App().separateWindowFor(separateId) && openInWindow) {
openInWindow();
} else {
navigation->showSection(topicRootId
? std::make_shared<Info::Memento>(topic, Section(type))
: std::make_shared<Info::Memento>(peer, Section(type)));
}
});
return result;
};
not_null<Ui::SettingsButton*> AddCommonGroupsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> user,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::CommonGroupsCountValue(user),
[](int count) {
return tr::lng_profile_common_groups(tr::now, lt_count, count);
},
tracker)->entity();
result->addClickHandler([=] {
navigation->showSection(
std::make_shared<Info::Memento>(
user,
Section::Type::CommonGroups));
});
return result;
}
not_null<Ui::SettingsButton*> AddSimilarPeersButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::SimilarPeersCountValue(peer),
[=](int count) {
return peer->isBroadcast()
? tr::lng_profile_similar_channels(tr::now, lt_count, count)
: tr::lng_profile_similar_bots(tr::now, lt_count, count);
},
tracker)->entity();
result->addClickHandler([=] {
navigation->showSection(
std::make_shared<Info::Memento>(
peer,
Section::Type::SimilarPeers));
});
return result;
}
not_null<Ui::SettingsButton*> AddStoriesButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto count = rpl::single(0) | rpl::then(Data::AlbumStoriesIds(
peer,
0, // = Data::kStoriesAlbumIdSaved
ServerMaxStoryId - 1,
0
) | rpl::map([](const Data::StoriesIdsSlice &slice) {
return slice.fullCount().value_or(0);
}));
const auto phrase = peer->isChannel() ? (+[](int count) {
return tr::lng_profile_posts(tr::now, lt_count, count);
}) : (+[](int count) {
return tr::lng_profile_saved_stories(tr::now, lt_count, count);
});
auto result = AddCountedButton(
parent,
std::move(count),
phrase,
tracker)->entity();
result->addClickHandler([=] {
navigation->showSection(Info::Stories::Make(peer));
});
return result;
}
not_null<Ui::SettingsButton*> AddSavedSublistButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::SavedSublistCountValue(peer),
[](int count) {
return tr::lng_profile_saved_messages(tr::now, lt_count, count);
},
tracker)->entity();
result->addClickHandler([=] {
using namespace HistoryView;
const auto sublist = peer->owner().savedMessages().sublist(peer);
navigation->showSection(
std::make_shared<ChatMemento>(ChatViewId{
.history = sublist->owningHistory(),
.sublist = sublist,
}));
});
return result;
}
not_null<Ui::SettingsButton*> AddPeerGiftsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto count = Profile::PeerGiftsCountValue(peer);
auto textFromCount = [](int count) {
return tr::lng_profile_peer_gifts(tr::now, lt_count, count);
};
using namespace ::Settings;
auto forked = std::move(count)
| start_spawning(parent->lifetime());
auto text = rpl::duplicate(
forked
) | rpl::map([textFromCount](int count) {
return (count > 0)
? textFromCount(count)
: QString();
});
struct State final {
std::vector<std::unique_ptr<Ui::Text::CustomEmoji>> emojiList;
rpl::event_stream<> textRefreshed;
QPointer<Ui::SettingsButton> button;
rpl::lifetime appearedLifetime;
};
const auto state = parent->lifetime().make_state<State>();
const auto refresh = [=] {
if (state->button) {
state->button->update();
}
};
auto customs = state->textRefreshed.events(
) | rpl::map([=]() -> TextWithEntities {
auto result = TextWithEntities();
for (const auto &custom : state->emojiList) {
result.append(Ui::Text::SingleCustomEmoji(custom->entityData()));
}
return result;
});
const auto wrap = parent->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
parent,
object_ptr<Ui::SettingsButton>(
parent,
rpl::combine(
std::move(text),
std::move(customs)
) | rpl::map([=](QString text, TextWithEntities customs) {
return TextWithEntities()
.append(std::move(text))
.append(QChar(' '))
.append(std::move(customs));
}),
st::infoSharedMediaButton,
Core::TextContext({
.session = &navigation->session(),
.details = { .session = &navigation->session() },
.repaint = refresh,
.customEmojiLoopLimit = 1,
}))));
wrap->setDuration(st::infoSlideDuration);
wrap->toggleOn(rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0));
tracker.track(wrap);
rpl::duplicate(forked) | rpl::filter(
rpl::mappers::_1 > 0
) | rpl::on_next([=] {
state->appearedLifetime.destroy();
const auto requestDone = crl::guard(wrap, [=](
std::vector<Data::SavedStarGift> gifts) {
state->emojiList.clear();
for (const auto &gift : gifts) {
state->emojiList.push_back(
peer->owner().customEmojiManager().create(
gift.info.document->id,
refresh));
}
state->textRefreshed.fire({});
});
navigation->session().recentSharedGifts().request(peer, requestDone);
}, state->appearedLifetime);
state->button = wrap->entity();
wrap->entity()->addClickHandler([=] {
if (navigation->showFrozenError()) {
return;
}
navigation->showSection(Info::PeerGifts::Make(peer));
});
return wrap->entity();
}
} // namespace Info::Media

View File

@@ -0,0 +1,80 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "lang/lang_keys.h"
#include "storage/storage_shared_media.h"
namespace Ui {
class AbstractButton;
class MultiSlideTracker;
class SettingsButton;
class VerticalLayout;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Info::Media {
using Type = Storage::SharedMediaType;
[[nodiscard]] tr::phrase<lngtag_count> MediaTextPhrase(Type type);
[[nodiscard]] Fn<QString(int)> MediaText(Type type);
[[nodiscard]] not_null<Ui::SlideWrap<Ui::SettingsButton>*> AddCountedButton(
Ui::VerticalLayout *parent,
rpl::producer<int> &&count,
Fn<QString(int)> &&textFromCount,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId,
PeerId monoforumPeerId,
PeerData *migrated,
Type type,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddCommonGroupsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> user,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddSimilarPeersButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddStoriesButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddSavedSublistButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddPeerGiftsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
} // namespace Info::Media

View File

@@ -0,0 +1,94 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/media/info_media_common.h"
#include "history/history_item.h"
#include "storage/storage_shared_media.h"
#include "styles/style_info.h"
#include "styles/style_overview.h"
namespace Info::Media {
UniversalMsgId GetUniversalId(FullMsgId itemId) {
return peerIsChannel(itemId.peer)
? UniversalMsgId(itemId.msg)
: UniversalMsgId(itemId.msg - ServerMaxMsgId);
}
UniversalMsgId GetUniversalId(not_null<const HistoryItem*> item) {
return GetUniversalId(item->fullId());
}
UniversalMsgId GetUniversalId(not_null<const BaseLayout*> layout) {
return GetUniversalId(layout->getItem()->fullId());
}
bool ChangeItemSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> item,
ListItemSelectionData selectionData,
int limit) {
if (!limit) {
limit = MaxSelectedItems;
}
const auto changeExisting = [&](auto it) {
if (it == selected.cend()) {
return false;
} else if (it->second != selectionData) {
it->second = selectionData;
return true;
}
return false;
};
if (selected.size() < limit) {
const auto &[i, ok] = selected.try_emplace(item, selectionData);
if (ok) {
return true;
}
return changeExisting(i);
}
return changeExisting(selected.find(item));
}
int MinItemHeight(Type type, int width) {
auto &songSt = st::overviewFileLayout;
switch (type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile: {
auto itemsLeft = st::infoMediaSkip;
auto itemsInRow = (width - itemsLeft)
/ (st::infoMediaMinGridSize + st::infoMediaSkip);
return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;
} break;
case Type::RoundVoiceFile:
return songSt.songPadding.top()
+ songSt.songThumbSize
+ songSt.songPadding.bottom()
+ st::lineWidth;
case Type::File:
return songSt.filePadding.top()
+ songSt.fileThumbSize
+ songSt.filePadding.bottom()
+ st::lineWidth;
case Type::MusicFile:
return songSt.songPadding.top()
+ songSt.songThumbSize
+ songSt.songPadding.bottom();
case Type::Link:
return st::linksPhotoSize
+ st::linksMargin.top()
+ st::linksMargin.bottom()
+ st::linksBorder;
}
Unexpected("Type in MinItemHeight()");
}
} // namespace Info::Media

View File

@@ -0,0 +1,185 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "overview/overview_layout.h"
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Info::Media {
using Type = Storage::SharedMediaType;
using BaseLayout = Overview::Layout::ItemBase;
class Memento;
class ListSection;
inline constexpr auto kPreloadIfLessThanScreens = 2;
struct ListItemSelectionData {
explicit ListItemSelectionData(TextSelection text) : text(text) {
}
TextSelection text;
bool canDelete = false;
bool canForward = false;
bool canToggleStoryPin = false;
bool canUnpinStory = false;
bool storyInProfile = false;
friend inline bool operator==(
ListItemSelectionData,
ListItemSelectionData) = default;
};
using ListSelectedMap = base::flat_map<
not_null<const HistoryItem*>,
ListItemSelectionData,
std::less<>>;
enum class ListDragSelectAction {
None,
Selecting,
Deselecting,
};
struct ListContext {
Overview::Layout::PaintContext layoutContext;
not_null<ListSelectedMap*> selected;
not_null<ListSelectedMap*> dragSelected;
ListDragSelectAction dragSelectAction = ListDragSelectAction::None;
BaseLayout *draggedItem = nullptr;
};
struct ListScrollTopState {
int64 position = 0; // ListProvider-specific.
HistoryItem *item = nullptr;
int shift = 0;
};
struct ListFoundItem {
not_null<BaseLayout*> layout;
QRect geometry;
bool exact = false;
};
struct ListFoundItemWithSection {
ListFoundItem item;
not_null<const ListSection*> section;
};
struct CachedItem {
CachedItem(std::unique_ptr<BaseLayout> item) : item(std::move(item)) {
};
CachedItem(CachedItem &&other) = default;
CachedItem &operator=(CachedItem &&other) = default;
~CachedItem() = default;
std::unique_ptr<BaseLayout> item;
bool stale = false;
};
using UniversalMsgId = MsgId;
[[nodiscard]] UniversalMsgId GetUniversalId(FullMsgId itemId);
[[nodiscard]] UniversalMsgId GetUniversalId(
not_null<const HistoryItem*> item);
[[nodiscard]] UniversalMsgId GetUniversalId(
not_null<const BaseLayout*> layout);
bool ChangeItemSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> item,
ListItemSelectionData selectionData,
int limit = 0);
class ListSectionDelegate {
public:
[[nodiscard]] virtual bool sectionHasFloatingHeader() = 0;
[[nodiscard]] virtual QString sectionTitle(
not_null<const BaseLayout*> item) = 0;
[[nodiscard]] virtual bool sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) = 0;
[[nodiscard]] not_null<ListSectionDelegate*> sectionDelegate() {
return this;
}
};
class ListProvider {
public:
[[nodiscard]] virtual Type type() = 0;
[[nodiscard]] virtual bool hasSelectRestriction() = 0;
[[nodiscard]] virtual auto hasSelectRestrictionChanges()
->rpl::producer<bool> = 0;
[[nodiscard]] virtual bool isPossiblyMyItem(
not_null<const HistoryItem*> item) = 0;
[[nodiscard]] virtual std::optional<int> fullCount() = 0;
virtual void restart() = 0;
virtual void checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) = 0;
virtual void refreshViewer() = 0;
[[nodiscard]] virtual rpl::producer<> refreshed() = 0;
[[nodiscard]] virtual std::vector<ListSection> fillSections(
not_null<Overview::Layout::Delegate*> delegate) = 0;
[[nodiscard]] virtual auto layoutRemoved()
-> rpl::producer<not_null<BaseLayout*>> = 0;
[[nodiscard]] virtual BaseLayout *lookupLayout(
const HistoryItem *item) = 0;
[[nodiscard]] virtual bool isMyItem(
not_null<const HistoryItem*> item) = 0;
[[nodiscard]] virtual bool isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) = 0;
[[nodiscard]] virtual ListItemSelectionData computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) = 0;
virtual void applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) = 0;
[[nodiscard]] virtual bool allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) = 0;
[[nodiscard]] virtual QString showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) = 0;
virtual void setSearchQuery(QString query) = 0;
[[nodiscard]] virtual int64 scrollTopStatePosition(
not_null<HistoryItem*> item) = 0;
[[nodiscard]] virtual HistoryItem *scrollTopStateItem(
ListScrollTopState state) = 0;
virtual void saveState(
not_null<Memento*> memento,
ListScrollTopState scrollState) = 0;
virtual void restoreState(
not_null<Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) = 0;
virtual ~ListProvider() = default;
};
[[nodiscard]] int MinItemHeight(Type type, int width);
} // namespace Info::Media

View File

@@ -0,0 +1,106 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/media/info_media_empty_widget.h"
#include "ui/widgets/labels.h"
#include "styles/style_info.h"
#include "lang/lang_keys.h"
namespace Info {
namespace Media {
EmptyWidget::EmptyWidget(QWidget *parent)
: RpWidget(parent)
, _text(this, st::infoEmptyLabel) {
}
void EmptyWidget::setFullHeight(rpl::producer<int> fullHeightValue) {
std::move(
fullHeightValue
) | rpl::on_next([this](int fullHeight) {
// Make icon center be on 1/3 height.
auto iconCenter = fullHeight / 3;
auto iconHeight = st::infoEmptyFile.height();
auto iconTop = iconCenter - iconHeight / 2;
_height = iconTop + st::infoEmptyIconTop;
resizeToWidth(width());
}, lifetime());
}
void EmptyWidget::setType(Type type) {
_type = type;
_icon = [&] {
switch (_type) {
case Type::Photo:
case Type::GIF: return &st::infoEmptyPhoto;
case Type::Video: return &st::infoEmptyVideo;
case Type::MusicFile: return &st::infoEmptyAudio;
case Type::File: return &st::infoEmptyFile;
case Type::Link: return &st::infoEmptyLink;
case Type::RoundVoiceFile: return &st::infoEmptyVoice;
}
Unexpected("Bad type in EmptyWidget::setType()");
}();
update();
}
void EmptyWidget::setSearchQuery(const QString &query) {
_text->setText([&] {
switch (_type) {
case Type::Photo:
return tr::lng_media_photo_empty(tr::now);
case Type::GIF:
return tr::lng_media_gif_empty(tr::now);
case Type::Video:
return tr::lng_media_video_empty(tr::now);
case Type::MusicFile:
return query.isEmpty()
? tr::lng_media_song_empty(tr::now)
: tr::lng_media_song_empty_search(tr::now);
case Type::File:
return query.isEmpty()
? tr::lng_media_file_empty(tr::now)
: tr::lng_media_file_empty_search(tr::now);
case Type::Link:
return query.isEmpty()
? tr::lng_media_link_empty(tr::now)
: tr::lng_media_link_empty_search(tr::now);
case Type::RoundVoiceFile:
return tr::lng_media_audio_empty(tr::now);
}
Unexpected("Bad type in EmptyWidget::setSearchQuery()");
}());
resizeToWidth(width());
}
void EmptyWidget::paintEvent(QPaintEvent *e) {
if (!_icon) {
return;
}
auto p = QPainter(this);
auto iconLeft = (width() - _icon->width()) / 2;
auto iconTop = height() - st::infoEmptyIconTop;
_icon->paint(p, iconLeft, iconTop, width());
}
int EmptyWidget::resizeGetHeight(int newWidth) {
auto labelTop = _height - st::infoEmptyLabelTop;
auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip;
_text->resizeToNaturalWidth(labelWidth);
auto labelLeft = (newWidth - _text->width()) / 2;
_text->moveToLeft(labelLeft, labelTop, newWidth);
update();
return _height;
}
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,42 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "info/media/info_media_widget.h"
namespace Ui {
class FlatLabel;
} // namespace Ui
namespace Info {
namespace Media {
class EmptyWidget : public Ui::RpWidget {
public:
EmptyWidget(QWidget *parent);
void setFullHeight(rpl::producer<int> fullHeightValue);
void setType(Type type);
void setSearchQuery(const QString &query);
protected:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int newWidth) override;
private:
object_ptr<Ui::FlatLabel> _text;
Type _type = Type::kCount;
const style::icon *_icon = nullptr;
int _height = 0;
};
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,249 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/media/info_media_inner_widget.h"
#include <rpl/flatten_latest.h>
#include "boxes/abstract_box.h"
#include "info/media/info_media_list_widget.h"
#include "info/media/info_media_buttons.h"
#include "info/media/info_media_empty_widget.h"
#include "info/profile/info_profile_icon.h"
#include "info/info_controller.h"
#include "data/data_forum_topic.h"
#include "data/data_peer.h"
#include "data/data_saved_sublist.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/search_field_controller.h"
#include "styles/style_info.h"
#include "lang/lang_keys.h"
namespace Info {
namespace Media {
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller)
: RpWidget(parent)
, _controller(controller)
, _empty(this) {
_empty->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
_empty->lifetime());
_list = setupList();
}
// Allows showing additional shared media links and tabs.
// Used for shared media in Saved Messages.
void InnerWidget::setupOtherTypes() {
if (_controller->key().peer()->sharedMediaInfo() && _isStackBottom) {
createOtherTypes();
} else {
_otherTypes.destroy();
refreshHeight();
}
}
void InnerWidget::createOtherTypes() {
_otherTypes.create(this);
_otherTypes->show();
createTypeButtons();
_otherTypes->add(object_ptr<Ui::BoxContentDivider>(_otherTypes));
_otherTypes->resizeToWidth(width());
_otherTypes->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
_otherTypes->lifetime());
}
void InnerWidget::createTypeButtons() {
auto wrap = _otherTypes->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
_otherTypes,
object_ptr<Ui::VerticalLayout>(_otherTypes)));
auto content = wrap->entity();
content->add(object_ptr<Ui::FixedHeightWidget>(
content,
st::infoProfileSkip));
auto tracker = Ui::MultiSlideTracker();
const auto peer = _controller->key().peer();
const auto topic = _controller->key().topic();
const auto sublist = _controller->key().sublist();
const auto topicRootId = topic ? topic->rootId() : MsgId();
const auto monoforumPeerId = sublist
? sublist->sublistPeer()->id
: PeerId();
const auto migrated = _controller->migrated();
const auto addMediaButton = [&](
Type buttonType,
const style::icon &icon) {
if (buttonType == type()) {
return;
}
auto result = AddButton(
content,
_controller,
peer,
topicRootId,
monoforumPeerId,
migrated,
buttonType,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition)->show();
};
addMediaButton(Type::Photo, st::infoIconMediaPhoto);
addMediaButton(Type::Video, st::infoIconMediaVideo);
addMediaButton(Type::File, st::infoIconMediaFile);
addMediaButton(Type::MusicFile, st::infoIconMediaAudio);
addMediaButton(Type::Link, st::infoIconMediaLink);
addMediaButton(Type::RoundVoiceFile, st::infoIconMediaVoice);
addMediaButton(Type::GIF, st::infoIconMediaGif);
content->add(object_ptr<Ui::FixedHeightWidget>(
content,
st::infoProfileSkip));
wrap->toggleOn(tracker.atLeastOneShownValue());
wrap->finishAnimating();
}
Type InnerWidget::type() const {
return _controller->section().mediaType();
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
}
bool InnerWidget::showInternal(not_null<Memento*> memento) {
if (!_controller->validateMementoPeer(memento)) {
return false;
}
auto mementoType = memento->section().mediaType();
if (mementoType == type()) {
restoreState(memento);
return true;
}
return false;
}
object_ptr<ListWidget> InnerWidget::setupList() {
auto result = object_ptr<ListWidget>(this, _controller);
result->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
result->lifetime());
using namespace rpl::mappers;
result->scrollToRequests(
) | rpl::map([widget = result.data()](int to) {
return Ui::ScrollToRequest {
widget->y() + to,
-1
};
}) | rpl::start_to_stream(
_scrollToRequests,
result->lifetime());
_selectedLists.fire(result->selectedListValue());
_listTops.fire(result->topValue());
_empty->setType(_controller->section().mediaType());
_controller->mediaSourceQueryValue(
) | rpl::on_next([this](const QString &query) {
_empty->setSearchQuery(query);
}, result->lifetime());
return result;
}
void InnerWidget::saveState(not_null<Memento*> memento) {
_list->saveState(memento);
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_list->restoreState(memento);
}
rpl::producer<SelectedItems> InnerWidget::selectedListValue() const {
return _selectedLists.events_starting_with(
_list->selectedListValue()
) | rpl::flatten_latest();
}
void InnerWidget::selectionAction(SelectionAction action) {
_list->selectionAction(action);
}
InnerWidget::~InnerWidget() = default;
int InnerWidget::resizeGetHeight(int newWidth) {
_inResize = true;
auto guard = gsl::finally([this] { _inResize = false; });
if (_otherTypes) {
_otherTypes->resizeToWidth(newWidth);
}
_list->resizeToWidth(newWidth);
_empty->resizeToWidth(newWidth);
return recountHeight();
}
void InnerWidget::refreshHeight() {
if (_inResize) {
return;
}
resize(width(), recountHeight());
}
int InnerWidget::recountHeight() {
auto top = 0;
if (_otherTypes) {
_otherTypes->moveToLeft(0, top);
top += _otherTypes->heightNoMargins() - st::lineWidth;
}
auto listHeight = 0;
if (_list) {
_list->moveToLeft(0, top);
listHeight = _list->heightNoMargins();
top += listHeight;
}
if (listHeight > 0) {
_empty->hide();
} else {
_empty->show();
_empty->moveToLeft(0, top);
top += _empty->heightNoMargins();
}
return top;
}
void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
using namespace rpl::mappers;
_empty->setFullHeight(rpl::combine(
std::move(value),
_listTops.events_starting_with(
_list->topValue()
) | rpl::flatten_latest(),
_1 - _2));
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,88 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/widgets/scroll_area.h"
#include "base/unique_qptr.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_list_widget.h"
namespace Ui {
class VerticalLayout;
class SearchFieldController;
} // namespace Ui
namespace Info {
class Controller;
} // namespace Info
namespace Info::Media {
class Memento;
class ListWidget;
class EmptyWidget;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller);
bool showInternal(not_null<Memento*> memento);
void setIsStackBottom(bool isStackBottom) {
_isStackBottom = isStackBottom;
setupOtherTypes();
}
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void setScrollHeightValue(rpl::producer<int> value);
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
rpl::producer<SelectedItems> selectedListValue() const;
void selectionAction(SelectionAction action);
~InnerWidget();
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
int recountHeight();
void refreshHeight();
// Allows showing additional shared media links and tabs.
// Used for shared media in Saved Messages.
void setupOtherTypes();
void createOtherTypes();
void createTypeButtons();
Type type() const;
object_ptr<ListWidget> setupList();
const not_null<Controller*> _controller;
object_ptr<Ui::VerticalLayout> _otherTypes = { nullptr };
object_ptr<ListWidget> _list = { nullptr };
object_ptr<EmptyWidget> _empty;
bool _inResize = false;
bool _isStackBottom = false;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
rpl::event_stream<rpl::producer<int>> _listTops;
};
} // namespace Info::Media

View File

@@ -0,0 +1,462 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/media/info_media_list_section.h"
#include "storage/storage_shared_media.h"
#include "layout/layout_selection.h"
#include "ui/rect.h"
#include "ui/painter.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
namespace Info::Media {
namespace {
constexpr auto kFloatingHeaderAlpha = 0.9;
} // namespace
ListSection::ListSection(Type type, not_null<ListSectionDelegate*> delegate)
: _type(type)
, _delegate(delegate)
, _hasFloatingHeader(delegate->sectionHasFloatingHeader())
, _mosaic(st::emojiPanWidth - st::inlineResultsLeft) {
}
bool ListSection::empty() const {
return _items.empty();
}
UniversalMsgId ListSection::minId() const {
Expects(!empty());
return GetUniversalId(_items.back()->getItem());
}
void ListSection::setTop(int top) {
_top = top;
}
int ListSection::top() const {
return _top;
}
void ListSection::setCanReorder(bool value) {
_canReorder = value;
}
int ListSection::height() const {
return _height;
}
int ListSection::bottom() const {
return top() + height();
}
bool ListSection::isOneColumn() const {
return _itemsInRow == 1;
}
bool ListSection::addItem(not_null<BaseLayout*> item) {
if (_items.empty() || belongsHere(item)) {
if (_items.empty()) {
setHeader(item);
}
appendItem(item);
return true;
}
return false;
}
void ListSection::finishSection() {
if (_type == Type::GIF) {
_mosaic.setPadding({
st::infoMediaSkip,
headerHeight(),
st::infoMediaSkip,
st::stickerPanPadding,
});
_mosaic.setRightSkip(st::infoMediaSkip);
_mosaic.addItems(_items);
}
}
void ListSection::setHeader(not_null<BaseLayout*> item) {
_header.setText(st::infoMediaHeaderStyle, _delegate->sectionTitle(item));
}
bool ListSection::belongsHere(
not_null<BaseLayout*> item) const {
Expects(!_items.empty());
return _delegate->sectionItemBelongsHere(item, _items.back());
}
void ListSection::appendItem(not_null<BaseLayout*> item) {
_items.push_back(item);
_byItem.emplace(item->getItem(), item);
}
bool ListSection::removeItem(not_null<const HistoryItem*> item) {
if (const auto i = _byItem.find(item); i != end(_byItem)) {
_items.erase(ranges::remove(_items, i->second), end(_items));
_byItem.erase(i);
refreshHeight();
return true;
}
return false;
}
void ListSection::reorderItems(int oldPosition, int newPosition) {
base::reorder(_items, oldPosition, newPosition);
refreshHeight();
}
QRect ListSection::findItemRect(
not_null<const BaseLayout*> item) const {
const auto position = item->position();
if (!_mosaic.empty()) {
return _mosaic.findRect(position);
}
const auto top = position / _itemsInRow;
const auto indexInRow = position % _itemsInRow;
const auto left = _itemsLeft
+ indexInRow * (_itemWidth + st::infoMediaSkip);
return QRect(left, top, _itemWidth, item->height());
}
ListFoundItem ListSection::completeResult(
not_null<BaseLayout*> item,
bool exact) const {
return { item, findItemRect(item), exact };
}
ListFoundItem ListSection::findItemByPoint(QPoint point) const {
Expects(!_items.empty());
if (!_mosaic.empty()) {
const auto found = _mosaic.findByPoint(point);
Assert(found.index != -1);
const auto item = _mosaic.itemAt(found.index);
const auto rect = findItemRect(item);
return { item, rect, found.exact };
}
auto itemIt = findItemAfterTop(point.y());
if (itemIt == end(_items)) {
--itemIt;
}
auto item = *itemIt;
auto rect = findItemRect(item);
if (point.y() >= rect.top()) {
auto shift = floorclamp(
point.x(),
(_itemWidth + st::infoMediaSkip),
0,
_itemsInRow);
while (shift-- && itemIt != _items.end()) {
++itemIt;
}
if (itemIt == _items.end()) {
--itemIt;
}
item = *itemIt;
rect = findItemRect(item);
}
return { item, rect, rect.contains(point) };
}
std::optional<ListFoundItem> ListSection::findItemByItem(
not_null<const HistoryItem*> item) const {
const auto i = _byItem.find(item);
if (i != end(_byItem)) {
return ListFoundItem{ i->second, findItemRect(i->second), true };
}
return std::nullopt;
}
ListFoundItem ListSection::findItemDetails(
not_null<BaseLayout*> item) const {
return { item, findItemRect(item), true };
}
auto ListSection::findItemAfterTop(
int top) -> Items::iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
_items,
top,
std::less_equal<>(),
[this](const auto &item) {
const auto itemTop = item->position() / _itemsInRow;
return itemTop + item->height();
});
}
auto ListSection::findItemAfterTop(
int top) const -> Items::const_iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
_items,
top,
std::less_equal<>(),
[this](const auto &item) {
const auto itemTop = item->position() / _itemsInRow;
return itemTop + item->height();
});
}
auto ListSection::findItemAfterBottom(
Items::const_iterator from,
int bottom) const -> Items::const_iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
from,
_items.end(),
bottom,
std::less<>(),
[this](const auto &item) {
const auto itemTop = item->position() / _itemsInRow;
return itemTop;
});
}
const ListSection::Items &ListSection::items() const {
return _items;
}
void ListSection::paint(
Painter &p,
const ListContext &context,
QRect clip,
int outerWidth) const {
const auto header = headerHeight();
if (QRect(0, 0, outerWidth, header).intersects(clip)) {
p.setPen(st::infoMediaHeaderFg);
_header.drawLeftElided(
p,
st::infoMediaHeaderPosition.x(),
st::infoMediaHeaderPosition.y(),
outerWidth - 2 * st::infoMediaHeaderPosition.x(),
outerWidth);
}
auto localContext = context.layoutContext;
if (!_mosaic.empty()) {
const auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
p.translate(point.x(), point.y());
item->paint(
p,
clip.translated(-point),
itemSelection(item, context),
&localContext);
p.translate(-point.x(), -point.y());
};
_mosaic.paint(std::move(paintItem), clip);
return;
}
const auto fromIt = findItemAfterTop(clip.y());
const auto tillIt = findItemAfterBottom(
fromIt,
clip.y() + clip.height());
for (auto it = fromIt; it != tillIt; ++it) {
const auto item = *it;
if (item == context.draggedItem) {
continue;
}
auto rect = findItemRect(item);
rect.translate(item->shift());
localContext.skipBorder = (rect.y() <= header + _itemsTop);
if (rect.intersects(clip)) {
p.translate(rect.topLeft());
item->paint(
p,
clip.translated(-rect.topLeft()),
itemSelection(item, context),
&localContext);
p.translate(-rect.topLeft());
if (_canReorder && isOneColumn()) {
st::stickersReorderIcon.paint(
p,
rect::right(rect) - oneColumnRightPadding(),
(rect.height() - st::stickersReorderIcon.height()) / 2
+ rect.y(),
outerWidth);
}
}
}
}
void ListSection::paintFloatingHeader(
Painter &p,
int visibleTop,
int outerWidth) {
if (!_hasFloatingHeader) {
return;
}
const auto headerTop = st::infoMediaHeaderPosition.y() / 2;
if (visibleTop <= (_top + headerTop)) {
return;
}
const auto header = headerHeight();
const auto headerLeft = st::infoMediaHeaderPosition.x();
const auto floatingTop = std::min(
visibleTop,
bottom() - header + headerTop);
p.save();
p.resetTransform();
p.setOpacity(kFloatingHeaderAlpha);
p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg);
p.setOpacity(1.0);
p.setPen(st::infoMediaHeaderFg);
_header.drawLeftElided(
p,
headerLeft,
floatingTop + headerTop,
outerWidth - 2 * headerLeft,
outerWidth);
p.restore();
}
TextSelection ListSection::itemSelection(
not_null<const BaseLayout*> item,
const ListContext &context) const {
const auto parent = item->getItem();
const auto dragSelectAction = context.dragSelectAction;
if (dragSelectAction != ListDragSelectAction::None) {
const auto i = context.dragSelected->find(parent);
if (i != context.dragSelected->end()) {
return (dragSelectAction == ListDragSelectAction::Selecting)
? FullSelection
: TextSelection();
}
}
const auto i = context.selected->find(parent);
return (i == context.selected->cend())
? TextSelection()
: i->second.text;
}
int ListSection::headerHeight() const {
return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight;
}
int ListSection::oneColumnRightPadding() const {
return !isOneColumn()
? 0
: _canReorder
? st::stickersReorderIcon.width() + st::infoMediaLeft
: 0;
}
void ListSection::resizeToWidth(int newWidth) {
const auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;
if (newWidth < minWidth) {
return;
}
const auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {
const auto rightPadding = oneColumnRightPadding();
_itemsLeft = itemsLeft;
_itemsTop = 0;
_itemsInRow = 1;
_itemWidth = itemWidth - rightPadding;
for (auto &item : _items) {
item->resizeGetHeight(_itemWidth - rightPadding);
}
};
switch (_type) {
case Type::Photo:
case Type::Video:
case Type::PhotoVideo:
case Type::RoundFile: {
const auto skip = st::infoMediaSkip;
_itemsLeft = st::infoMediaLeft;
_itemsTop = st::infoMediaSkip;
_itemsInRow = (newWidth - _itemsLeft * 2 + skip)
/ (st::infoMediaMinGridSize + skip);
_itemWidth = ((newWidth - _itemsLeft * 2 + skip) / _itemsInRow)
- st::infoMediaSkip;
_itemsLeft = (newWidth - (_itemWidth + skip) * _itemsInRow + skip)
/ 2;
for (auto &item : _items) {
_itemHeight = item->resizeGetHeight(_itemWidth);
}
} break;
case Type::GIF: {
_mosaic.setFullWidth(newWidth - st::infoMediaSkip);
} break;
case Type::RoundVoiceFile:
case Type::MusicFile:
resizeOneColumn(0, newWidth);
break;
case Type::File:
case Type::Link: {
const auto itemsLeft = st::infoMediaHeaderPosition.x();
const auto itemWidth = newWidth - 2 * itemsLeft;
resizeOneColumn(itemsLeft, itemWidth);
} break;
}
refreshHeight();
}
int ListSection::recountHeight() {
auto result = headerHeight();
switch (_type) {
case Type::Photo:
case Type::Video:
case Type::PhotoVideo:
case Type::RoundFile: {
const auto itemHeight = _itemHeight + st::infoMediaSkip;
auto index = 0;
result += _itemsTop;
for (auto &item : _items) {
item->setPosition(_itemsInRow * result + index);
if (++index == _itemsInRow) {
result += itemHeight;
index = 0;
}
}
if (_items.size() % _itemsInRow) {
_rowsCount = int(_items.size()) / _itemsInRow + 1;
result += itemHeight;
} else {
_rowsCount = int(_items.size()) / _itemsInRow;
}
} break;
case Type::GIF: {
return _mosaic.countDesiredHeight(0);
} break;
case Type::RoundVoiceFile:
case Type::File:
case Type::MusicFile:
case Type::Link:
for (auto &item : _items) {
item->setPosition(result);
result += item->height();
}
_rowsCount = _items.size();
break;
}
return result;
}
void ListSection::refreshHeight() {
_height = recountHeight();
}
} // namespace Info::Media

View File

@@ -0,0 +1,100 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "info/media/info_media_common.h"
#include "layout/layout_mosaic.h"
#include "ui/text/text.h"
namespace Info::Media {
class ListSection {
public:
ListSection(Type type, not_null<ListSectionDelegate*> delegate);
bool addItem(not_null<BaseLayout*> item);
void finishSection();
[[nodiscard]] bool empty() const;
[[nodiscard]] UniversalMsgId minId() const;
void setTop(int top);
[[nodiscard]] int top() const;
void setCanReorder(bool);
void resizeToWidth(int newWidth);
[[nodiscard]] int height() const;
[[nodiscard]] int bottom() const;
[[nodiscard]] bool isOneColumn() const;
[[nodiscard]] int oneColumnRightPadding() const;
bool removeItem(not_null<const HistoryItem*> item);
void reorderItems(int oldPosition, int newPosition);
[[nodiscard]] std::optional<ListFoundItem> findItemByItem(
not_null<const HistoryItem*> item) const;
[[nodiscard]] ListFoundItem findItemDetails(
not_null<BaseLayout*> item) const;
[[nodiscard]] ListFoundItem findItemByPoint(QPoint point) const;
using Items = std::vector<not_null<BaseLayout*>>;
const Items &items() const;
void paint(
Painter &p,
const ListContext &context,
QRect clip,
int outerWidth) const;
void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth);
private:
[[nodiscard]] int headerHeight() const;
void appendItem(not_null<BaseLayout*> item);
void setHeader(not_null<BaseLayout*> item);
[[nodiscard]] bool belongsHere(not_null<BaseLayout*> item) const;
[[nodiscard]] Items::iterator findItemAfterTop(int top);
[[nodiscard]] Items::const_iterator findItemAfterTop(int top) const;
[[nodiscard]] Items::const_iterator findItemAfterBottom(
Items::const_iterator from,
int bottom) const;
[[nodiscard]] QRect findItemRect(not_null<const BaseLayout*> item) const;
[[nodiscard]] ListFoundItem completeResult(
not_null<BaseLayout*> item,
bool exact) const;
[[nodiscard]] TextSelection itemSelection(
not_null<const BaseLayout*> item,
const ListContext &context) const;
int recountHeight();
void refreshHeight();
Type _type = Type{};
not_null<ListSectionDelegate*> _delegate;
bool _hasFloatingHeader = false;
Ui::Text::String _header;
Items _items;
base::flat_map<
not_null<const HistoryItem*>,
not_null<BaseLayout*>> _byItem;
int _itemsLeft = 0;
int _itemsTop = 0;
int _itemWidth = 0;
int _itemHeight = 0;
int _itemsInRow = 1;
mutable int _rowsCount = 0;
int _top = 0;
int _height = 0;
bool _canReorder = false;
Mosaic::Layout::MosaicLayout<BaseLayout> _mosaic;
};
} // namespace Info::Media

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,382 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/widgets/tooltip.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_common.h"
#include "overview/overview_layout_delegate.h"
class DeleteMessagesBox;
namespace Main {
class Session;
} // namespace Main
namespace HistoryView {
struct TextState;
struct StateRequest;
enum class CursorState : char;
enum class PointState : char;
} // namespace HistoryView
namespace Ui {
class PopupMenu;
class BoxContent;
} // namespace Ui
namespace Overview {
namespace Layout {
class ItemBase;
} // namespace Layout
} // namespace Overview
namespace Window {
class SessionController;
} // namespace Window
namespace Info {
class AbstractController;
namespace Media {
struct ListFoundItem;
struct ListFoundItemWithSection;
struct ListContext;
class ListSection;
class ListProvider;
class ListWidget final
: public Ui::RpWidget
, public Overview::Layout::Delegate
, public Ui::AbstractTooltipShower {
public:
ListWidget(
QWidget *parent,
not_null<AbstractController*> controller);
~ListWidget();
Main::Session &session() const;
void restart();
rpl::producer<int> scrollToRequests() const;
rpl::producer<SelectedItems> selectedListValue() const;
void selectionAction(SelectionAction action);
struct ReorderDescriptor {
Fn<void(int old, int pos, Fn<void()> done, Fn<void()> fail)> save;
Fn<bool(HistoryItem*)> filter;
};
void setReorderDescriptor(ReorderDescriptor descriptor);
QRect getCurrentSongGeometry();
rpl::producer<> checkForHide() const {
return _checkForHide.events();
}
bool preventAutoHide() const;
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
// Overview::Layout::Delegate
void registerHeavyItem(not_null<const BaseLayout*> item) override;
void unregisterHeavyItem(not_null<const BaseLayout*> item) override;
void repaintItem(not_null<const BaseLayout*> item) override;
bool itemVisible(not_null<const BaseLayout*> item) override;
not_null<StickerPremiumMark*> hiddenMark() override;
// AbstractTooltipShower interface
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
void openPhoto(not_null<PhotoData*> photo, FullMsgId id) override;
void openDocument(
not_null<DocumentData*> document,
FullMsgId id,
bool showInMediaView = false) override;
private:
struct DateBadge;
using Section = ListSection;
using FoundItem = ListFoundItem;
using CursorState = HistoryView::CursorState;
using TextState = HistoryView::TextState;
using StateRequest = HistoryView::StateRequest;
using SelectionData = ListItemSelectionData;
using SelectedMap = ListSelectedMap;
using DragSelectAction = ListDragSelectAction;
enum class MouseAction {
None,
PrepareDrag,
Dragging,
PrepareSelect,
Selecting,
PrepareReorder,
Reordering,
};
struct ReorderState {
bool enabled = false;
int index = -1;
int targetIndex = -1;
QPoint startPos;
QPoint dragPoint;
QPoint currentPos;
BaseLayout *item = nullptr;
const Section *section = nullptr;
};
struct ShiftAnimation {
Ui::Animations::Simple xAnimation;
Ui::Animations::Simple yAnimation;
int shift = 0;
int targetShift = 0;
};
struct MouseState {
HistoryItem *item = nullptr;
QSize size;
QPoint cursor;
bool inside = false;
inline bool operator==(const MouseState &other) const {
return (item == other.item)
&& (cursor == other.cursor);
}
inline bool operator!=(const MouseState &other) const {
return !(*this == other);
}
};
enum class ContextMenuSource {
Mouse,
Touch,
Other,
};
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void start();
int recountHeight();
void refreshHeight();
void subscribeToSession(
not_null<Main::Session*> session,
rpl::lifetime &lifetime);
void setupSelectRestriction();
[[nodiscard]] MsgId topicRootId() const;
[[nodiscard]] PeerId monoforumPeerId() const;
QMargins padding() const;
bool isItemLayout(
not_null<const HistoryItem*> item,
BaseLayout *layout) const;
void repaintItem(const HistoryItem *item);
void repaintItem(const BaseLayout *item);
void repaintItem(QRect itemGeometry);
void itemRemoved(not_null<const HistoryItem*> item);
void itemLayoutChanged(not_null<const HistoryItem*> item);
void refreshRows();
void markStoryMsgsSelected();
void trackSession(not_null<Main::Session*> session);
[[nodiscard]] SelectedItems collectSelectedItems() const;
[[nodiscard]] MessageIdsList collectSelectedIds() const;
[[nodiscard]] MessageIdsList collectSelectedIds(
const SelectedItems &items) const;
void pushSelectedItems();
[[nodiscard]] bool hasSelected() const;
[[nodiscard]] bool isSelectedItem(
const SelectedMap::const_iterator &i) const;
void removeItemSelection(
const SelectedMap::const_iterator &i);
[[nodiscard]] bool hasSelectedText() const;
[[nodiscard]] bool hasSelectedItems() const;
void clearSelected();
void forwardSelected();
void forwardItem(GlobalMsgId globalId);
void forwardItems(MessageIdsList &&items);
void deleteSelected();
void toggleStoryPinSelected();
void toggleStoryInProfileSelected(bool toProfile);
void deleteItem(GlobalMsgId globalId);
void deleteItems(SelectedItems &&items, Fn<void()> confirmed = nullptr);
void toggleStoryInProfile(
MessageIdsList &&items,
bool toProfile,
Fn<void()> confirmed = nullptr);
void toggleStoryPin(
MessageIdsList &&items,
bool pin,
Fn<void()> confirmed = nullptr);
void applyItemSelection(
HistoryItem *item,
TextSelection selection);
void toggleItemSelection(not_null<HistoryItem*> item);
[[nodiscard]] SelectedMap::iterator itemUnderPressSelection();
[[nodiscard]] auto itemUnderPressSelection() const
-> SelectedMap::const_iterator;
bool isItemUnderPressSelected() const;
[[nodiscard]] bool requiredToStartDragging(
not_null<BaseLayout*> layout) const;
[[nodiscard]] bool isPressInSelectedText(TextState state) const;
void applyDragSelection();
void applyDragSelection(SelectedMap &applyTo) const;
[[nodiscard]] bool isAfter(
const MouseState &a,
const MouseState &b) const;
[[nodiscard]] static bool SkipSelectFromItem(const MouseState &state);
[[nodiscard]] static bool SkipSelectTillItem(const MouseState &state);
[[nodiscard]] std::vector<Section>::iterator findSectionByItem(
not_null<const HistoryItem*> item);
[[nodiscard]] std::vector<Section>::iterator findSectionAfterTop(
int top);
[[nodiscard]] std::vector<Section>::const_iterator findSectionAfterTop(
int top) const;
[[nodiscard]] auto findSectionAfterBottom(
std::vector<Section>::const_iterator from,
int bottom) const -> std::vector<Section>::const_iterator;
[[nodiscard]] auto findSectionAndItem(QPoint point) const
-> std::pair<std::vector<Section>::const_iterator, FoundItem>;
[[nodiscard]] FoundItem findItemByPoint(QPoint point) const;
[[nodiscard]] ListFoundItemWithSection findItemByPointWithSection(QPoint point) const;
[[nodiscard]] std::optional<FoundItem> findItemByItem(
const HistoryItem *item);
[[nodiscard]] FoundItem findItemDetails(not_null<BaseLayout*> item);
[[nodiscard]] FoundItem foundItemInSection(
const FoundItem &item,
const Section &section) const;
[[nodiscard]] ListScrollTopState countScrollState() const;
void saveScrollState();
void restoreScrollState();
[[nodiscard]] QPoint clampMousePosition(QPoint position) const;
void mouseActionStart(
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionUpdate(const QPoint &globalPosition);
void mouseActionUpdate();
void mouseActionFinish(
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionCancel();
void performDrag();
[[nodiscard]] style::cursor computeMouseCursor() const;
void showContextMenu(
QContextMenuEvent *e,
ContextMenuSource source);
void updateDragSelection();
void clearDragSelection();
void updateDateBadgeFor(int top);
void scrollDateCheck();
void scrollDateHide();
void toggleScrollDateShown();
void trySwitchToWordSelection();
void switchToWordSelection();
void validateTrippleClickStartTime();
void checkMoveToOtherViewer();
void clearHeavyItems();
void setActionBoxWeak(base::weak_qptr<Ui::BoxContent> box);
void setupStoriesTrackIds();
void startReorder(const QPoint &globalPos);
void updateReorder(const QPoint &globalPos);
void finishReorder();
void cancelReorder();
void updateShiftAnimations();
[[nodiscard]] int itemIndexFromPoint(QPoint point) const;
[[nodiscard]] QRect itemGeometryByIndex(int index);
[[nodiscard]] BaseLayout *itemByIndex(int index);
[[nodiscard]] bool canReorder() const;
void reorderItemsInSections(int oldIndex, int newIndex);
void resetAllItemShifts();
void finishShiftAnimations();
const not_null<AbstractController*> _controller;
const std::unique_ptr<ListProvider> _provider;
base::flat_set<not_null<const BaseLayout*>> _heavyLayouts;
bool _heavyLayoutsInvalidated = false;
std::vector<Section> _sections;
int _visibleTop = 0;
int _visibleBottom = 0;
ListScrollTopState _scrollTopState;
rpl::event_stream<int> _scrollToRequests;
MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters;
QPoint _mousePosition;
MouseState _overState;
MouseState _pressState;
BaseLayout *_overLayout = nullptr;
HistoryItem *_contextItem = nullptr;
CursorState _mouseCursorState = CursorState();
uint16 _mouseTextSymbol = 0;
bool _pressWasInactive = false;
SelectedMap _selected;
SelectedMap _dragSelected;
rpl::event_stream<SelectedItems> _selectedListStream;
style::cursor _cursor = style::cur_default;
DragSelectAction _dragSelectAction = DragSelectAction::None;
bool _wasSelectedText = false; // was some text selected in current drag action
const std::unique_ptr<DateBadge> _dateBadge;
int _selectedLimit = 0;
int _storiesAddToAlbumId = 0;
int _storiesAddToAlbumTotal = 0;
base::flat_set<StoryId> _storiesInAlbum;
base::flat_set<MsgId> _storyMsgsToMarkSelected;
std::unique_ptr<StickerPremiumMark> _hiddenMark;
base::unique_qptr<Ui::PopupMenu> _contextMenu;
rpl::event_stream<> _checkForHide;
base::weak_qptr<Ui::BoxContent> _actionBoxWeak;
rpl::lifetime _actionBoxWeakLifetime;
QPoint _trippleClickPoint;
crl::time _trippleClickStartTime = 0;
base::flat_map<not_null<Main::Session*>, rpl::lifetime> _trackedSessions;
ReorderState _reorderState;
base::flat_map<int, ShiftAnimation> _shiftAnimations;
int _activeShiftAnimations = 0;
Ui::Animations::Simple _returnAnimation;
ReorderDescriptor _reorderDescriptor;
bool _inDragArea = false;
};
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,578 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/media/info_media_provider.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_list_section.h"
#include "info/info_controller.h"
#include "layout/layout_selection.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "lang/lang_keys.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "data/data_session.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
#include "data/data_user.h"
#include "data/data_peer_values.h"
#include "data/data_document.h"
#include "data/data_saved_sublist.h"
#include "styles/style_info.h"
#include "styles/style_overview.h"
namespace Info::Media {
namespace {
constexpr auto kPreloadedScreensCount = 4;
constexpr auto kPreloadedScreensCountFull
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
} // namespace
Provider::Provider(not_null<AbstractController*> controller)
: _controller(controller)
, _peer(_controller->key().peer())
, _topicRootId(_controller->key().topic()
? _controller->key().topic()->rootId()
: MsgId())
, _monoforumPeerId(_controller->key().sublist()
? _controller->key().sublist()->sublistPeer()->id
: PeerId())
, _migrated(_controller->migrated())
, _type(_controller->section().mediaType())
, _slice(sliceKey(_universalAroundId)) {
_controller->session().data().itemRemoved(
) | rpl::on_next([this](auto item) {
itemRemoved(item);
}, _lifetime);
style::PaletteChanged(
) | rpl::on_next([=] {
for (auto &layout : _layouts) {
layout.second.item->invalidateCache();
}
}, _lifetime);
_controller->session().appConfig().ignoredRestrictionReasonsChanges(
) | rpl::on_next([=](std::vector<QString> &&changed) {
const auto sensitive = Data::UnavailableReason::Sensitive();
if (ranges::contains(changed, sensitive.reason)) {
for (auto &[id, layout] : _layouts) {
layout.item->maybeClearSensitiveSpoiler();
}
}
}, _lifetime);
}
Type Provider::type() {
return _type;
}
bool Provider::hasSelectRestriction() {
if (_peer->session().frozen()) {
return true;
} else if (_peer->allowsForwarding()) {
return false;
} else if (const auto chat = _peer->asChat()) {
return !chat->canDeleteMessages();
} else if (const auto channel = _peer->asChannel()) {
return !channel->canDeleteMessages();
}
return true;
}
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
if (_peer->isUser()) {
return rpl::never<bool>();
}
const auto chat = _peer->asChat();
const auto channel = _peer->asChannel();
auto noForwards = chat
? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards)
: Data::PeerFlagValue(
channel,
ChannelDataFlag::NoForwards
) | rpl::type_erased;
auto rights = chat
? chat->adminRightsValue()
: channel->adminRightsValue();
auto canDelete = std::move(
rights
) | rpl::map([=] {
return chat
? chat->canDeleteMessages()
: channel->canDeleteMessages();
});
return rpl::combine(
std::move(noForwards),
std::move(canDelete)
) | rpl::map([=] {
return hasSelectRestriction();
}) | rpl::distinct_until_changed() | rpl::skip(1);
}
bool Provider::sectionHasFloatingHeader() {
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::MusicFile:
return false;
case Type::File:
case Type::Link:
return true;
}
Unexpected("Type in HasFloatingHeader()");
}
QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::File:
return langMonthFull(item->dateTime().date());
case Type::Link:
return langDayOfMonthFull(item->dateTime().date());
case Type::MusicFile:
return QString();
}
Unexpected("Type in ListSection::setHeader()");
}
bool Provider::sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) {
const auto date = item->dateTime().date();
const auto sectionDate = previous->dateTime().date();
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::File:
return date.year() == sectionDate.year()
&& date.month() == sectionDate.month();
case Type::Link:
return date == sectionDate;
case Type::MusicFile:
return true;
}
Unexpected("Type in ListSection::belongsHere()");
}
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
return isPossiblyMyPeerId(item->history()->peer->id);
}
bool Provider::isPossiblyMyPeerId(PeerId peerId) const {
return (peerId == _peer->id) || (_migrated && peerId == _migrated->id);
}
std::optional<int> Provider::fullCount() {
return _slice.fullCount();
}
void Provider::restart() {
_layouts.clear();
_universalAroundId = kDefaultAroundId;
_idsLimit = kMinimalIdsLimit;
_slice = SparseIdsMergedSlice(sliceKey(_universalAroundId));
refreshViewer();
}
void Provider::checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) {
const auto visibleWidth = viewport.width();
const auto visibleHeight = viewport.height();
const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
const auto minItemHeight = MinItemHeight(_type, visibleWidth);
const auto preloadedCount = preloadedHeight / minItemHeight;
const auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
const auto preloadIdsLimit = preloadIdsLimitMin
+ (visibleHeight / minItemHeight);
const auto after = _slice.skippedAfter();
const auto topLoaded = after && (*after == 0);
const auto before = _slice.skippedBefore();
const auto bottomLoaded = before && (*before == 0);
const auto minScreenDelta = kPreloadedScreensCount
- kPreloadIfLessThanScreens;
const auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
/ minItemHeight;
const auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {
auto preloadRequired = false;
auto universalId = GetUniversalId(layout);
if (!preloadRequired) {
preloadRequired = (_idsLimit < preloadIdsLimitMin);
}
if (!preloadRequired) {
auto delta = _slice.distance(
sliceKey(_universalAroundId),
sliceKey(universalId));
Assert(delta != std::nullopt);
preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
}
if (preloadRequired) {
_idsLimit = preloadIdsLimit;
_universalAroundId = universalId;
refreshViewer();
}
};
if (preloadTop && !topLoaded) {
preloadAroundItem(topLayout);
} else if (preloadBottom && !bottomLoaded) {
preloadAroundItem(bottomLayout);
}
}
void Provider::refreshViewer() {
_viewerLifetime.destroy();
const auto idForViewer = sliceKey(_universalAroundId).universalId;
_controller->mediaSource(
idForViewer,
_idsLimit,
_idsLimit
) | rpl::on_next([=](SparseIdsMergedSlice &&slice) {
if (!slice.fullCount()) {
// Don't display anything while full count is unknown.
return;
}
_slice = std::move(slice);
if (auto nearest = _slice.nearest(idForViewer)) {
_universalAroundId = GetUniversalId(*nearest);
}
_refreshed.fire({});
}, _viewerLifetime);
}
rpl::producer<> Provider::refreshed() {
return _refreshed.events();
}
std::vector<ListSection> Provider::fillSections(
not_null<Overview::Layout::Delegate*> delegate) {
markLayoutsStale();
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
auto result = std::vector<ListSection>();
auto section = ListSection(_type, sectionDelegate());
auto count = _slice.size();
for (auto i = count; i != 0;) {
auto universalId = GetUniversalId(_slice[--i]);
if (auto layout = getLayout(universalId, delegate)) {
if (!section.addItem(layout)) {
section.finishSection();
result.push_back(std::move(section));
section = ListSection(_type, sectionDelegate());
section.addItem(layout);
}
}
}
if (!section.empty()) {
section.finishSection();
result.push_back(std::move(section));
}
return result;
}
void Provider::markLayoutsStale() {
for (auto &layout : _layouts) {
layout.second.stale = true;
}
}
void Provider::clearStaleLayouts() {
for (auto i = _layouts.begin(); i != _layouts.end();) {
if (i->second.stale) {
_layoutRemoved.fire(i->second.item.get());
i = _layouts.erase(i);
} else {
++i;
}
}
}
rpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {
return _layoutRemoved.events();
}
BaseLayout *Provider::lookupLayout(
const HistoryItem *item) {
const auto i = _layouts.find(GetUniversalId(item));
return (i != _layouts.end()) ? i->second.item.get() : nullptr;
}
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
const auto peer = item->history()->peer;
return (_peer == peer) || (_migrated == peer);
}
bool Provider::isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) {
return (GetUniversalId(a) < GetUniversalId(b));
}
void Provider::setSearchQuery(QString query) {
Unexpected("Media::Provider::setSearchQuery.");
}
SparseIdsMergedSlice::Key Provider::sliceKey(
UniversalMsgId universalId) const {
using Key = SparseIdsMergedSlice::Key;
if (!_topicRootId && _migrated) {
return Key(
_peer->id,
_topicRootId,
_monoforumPeerId,
_migrated->id,
universalId);
}
if (universalId < 0) {
// Convert back to plain id for non-migrated histories.
universalId = universalId + ServerMaxMsgId;
}
return Key(
_peer->id,
_topicRootId,
_monoforumPeerId,
PeerId(),
universalId);
}
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
const auto id = GetUniversalId(item);
if (const auto i = _layouts.find(id); i != end(_layouts)) {
_layoutRemoved.fire(i->second.item.get());
_layouts.erase(i);
}
}
FullMsgId Provider::computeFullId(
UniversalMsgId universalId) const {
Expects(universalId != 0);
return (universalId > 0)
? FullMsgId(_peer->id, universalId)
: FullMsgId(
(_migrated ? _migrated : _peer.get())->id,
ServerMaxMsgId + universalId);
}
BaseLayout *Provider::getLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate) {
auto it = _layouts.find(universalId);
if (it == _layouts.end()) {
if (auto layout = createLayout(universalId, delegate, _type)) {
layout->initDimensions();
it = _layouts.emplace(
universalId,
std::move(layout)).first;
} else {
return nullptr;
}
}
it->second.stale = false;
return it->second.item.get();
}
std::unique_ptr<BaseLayout> Provider::createLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate,
Type type) {
const auto item = _controller->session().data().message(
computeFullId(universalId));
if (!item) {
return nullptr;
}
const auto getPhoto = [&]() -> PhotoData* {
if (const auto media = item->media()) {
return media->photo();
}
return nullptr;
};
const auto getFile = [&]() -> DocumentData* {
if (const auto media = item->media()) {
return media->document();
}
return nullptr;
};
const auto &songSt = st::overviewFileLayout;
using namespace Overview::Layout;
const auto options = [&] {
const auto media = item->media();
return MediaOptions{ .spoiler = media && media->hasSpoiler() };
};
switch (type) {
case Type::Photo:
if (const auto photo = getPhoto()) {
return std::make_unique<Photo>(
delegate,
item,
photo,
options());
}
return nullptr;
case Type::GIF:
if (const auto file = getFile()) {
return std::make_unique<Gif>(delegate, item, file);
}
return nullptr;
case Type::Video:
if (const auto file = getFile()) {
return std::make_unique<Video>(delegate, item, file, options());
}
return nullptr;
case Type::File:
if (const auto file = getFile()) {
return std::make_unique<Document>(
delegate,
item,
DocumentFields{ .document = file },
songSt);
}
return nullptr;
case Type::MusicFile:
if (const auto file = getFile()) {
return std::make_unique<Document>(
delegate,
item,
DocumentFields{ .document = file },
songSt);
}
return nullptr;
case Type::RoundVoiceFile:
if (const auto file = getFile()) {
return std::make_unique<Voice>(delegate, item, file, songSt);
}
return nullptr;
case Type::Link:
return std::make_unique<Link>(delegate, item, item->media());
case Type::RoundFile:
return nullptr;
}
Unexpected("Type in ListWidget::createLayout()");
}
ListItemSelectionData Provider::computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) {
auto result = ListItemSelectionData(selection);
result.canDelete = item->canDelete();
result.canForward = item->allowsForward();
return result;
}
bool Provider::allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return item->allowsForward();
}
QString Provider::showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return document->filepath(true);
}
void Provider::applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) {
const auto fromId = GetUniversalId(fromItem) - (skipFrom ? 1 : 0);
const auto tillId = GetUniversalId(tillItem) - (skipTill ? 0 : 1);
for (auto i = selected.begin(); i != selected.end();) {
const auto itemId = GetUniversalId(i->first);
if (itemId > fromId || itemId <= tillId) {
i = selected.erase(i);
} else {
++i;
}
}
for (auto &layoutItem : _layouts) {
auto &&universalId = layoutItem.first;
if (universalId <= fromId && universalId > tillId) {
const auto item = layoutItem.second.item->getItem();
ChangeItemSelection(
selected,
item,
computeSelectionData(item, FullSelection));
}
}
}
int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
return GetUniversalId(item).bare;
}
HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {
if (state.item && _slice.indexOf(state.item->fullId())) {
return state.item;
} else if (const auto id = _slice.nearest(state.position)) {
if (const auto item = _controller->session().data().message(*id)) {
return item;
}
}
return state.item;
}
void Provider::saveState(
not_null<Memento*> memento,
ListScrollTopState scrollState) {
if (_universalAroundId != kDefaultAroundId && scrollState.item) {
memento->setAroundId(computeFullId(_universalAroundId));
memento->setIdsLimit(_idsLimit);
memento->setScrollTopItem(scrollState.item->globalId());
memento->setScrollTopItemPosition(scrollState.position);
memento->setScrollTopShift(scrollState.shift);
}
}
void Provider::restoreState(
not_null<Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) {
if (const auto limit = memento->idsLimit()) {
auto wasAroundId = memento->aroundId();
if (isPossiblyMyPeerId(wasAroundId.peer)) {
_idsLimit = limit;
_universalAroundId = GetUniversalId(wasAroundId);
restoreScrollState({
.position = memento->scrollTopItemPosition(),
.item = MessageByGlobalId(memento->scrollTopItem()),
.shift = memento->scrollTopShift(),
});
refreshViewer();
}
}
}
} // namespace Info::Media

View File

@@ -0,0 +1,125 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "info/media/info_media_common.h"
#include "data/data_shared_media.h"
namespace Info {
class AbstractController;
} // namespace Info
namespace Info::Media {
class Provider final : public ListProvider, private ListSectionDelegate {
public:
explicit Provider(not_null<AbstractController*> controller);
Type type() override;
bool hasSelectRestriction() override;
rpl::producer<bool> hasSelectRestrictionChanges() override;
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
std::optional<int> fullCount() override;
void restart() override;
void checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) override;
void refreshViewer() override;
rpl::producer<> refreshed() override;
std::vector<ListSection> fillSections(
not_null<Overview::Layout::Delegate*> delegate) override;
rpl::producer<not_null<BaseLayout*>> layoutRemoved() override;
BaseLayout *lookupLayout(const HistoryItem *item) override;
bool isMyItem(not_null<const HistoryItem*> item) override;
bool isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) override;
void setSearchQuery(QString query) override;
ListItemSelectionData computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) override;
void applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) override;
bool allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
QString showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
HistoryItem *scrollTopStateItem(ListScrollTopState state) override;
void saveState(
not_null<Memento*> memento,
ListScrollTopState scrollState) override;
void restoreState(
not_null<Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) override;
private:
static constexpr auto kMinimalIdsLimit = 16;
static constexpr auto kDefaultAroundId = (ServerMaxMsgId - 1);
bool sectionHasFloatingHeader() override;
QString sectionTitle(not_null<const BaseLayout*> item) override;
bool sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) override;
[[nodiscard]] bool isPossiblyMyPeerId(PeerId peerId) const;
[[nodiscard]] FullMsgId computeFullId(UniversalMsgId universalId) const;
[[nodiscard]] BaseLayout *getLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate);
[[nodiscard]] std::unique_ptr<BaseLayout> createLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate,
Type type);
[[nodiscard]] SparseIdsMergedSlice::Key sliceKey(
UniversalMsgId universalId) const;
void itemRemoved(not_null<const HistoryItem*> item);
void markLayoutsStale();
void clearStaleLayouts();
const not_null<AbstractController*> _controller;
const not_null<PeerData*> _peer;
const MsgId _topicRootId = 0;
const PeerId _monoforumPeerId = 0;
PeerData * const _migrated = nullptr;
const Type _type = Type::Photo;
UniversalMsgId _universalAroundId = kDefaultAroundId;
int _idsLimit = kMinimalIdsLimit;
SparseIdsMergedSlice _slice;
std::unordered_map<UniversalMsgId, CachedItem> _layouts;
rpl::event_stream<not_null<BaseLayout*>> _layoutRemoved;
rpl::event_stream<> _refreshed;
rpl::lifetime _lifetime;
rpl::lifetime _viewerLifetime;
};
} // namespace Info::Media

View File

@@ -0,0 +1,201 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/media/info_media_widget.h"
#include "history/history.h"
#include "info/media/info_media_inner_widget.h"
#include "info/info_controller.h"
#include "main/main_session.h"
#include "ui/widgets/scroll_area.h"
#include "ui/search_field_controller.h"
#include "ui/ui_utility.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
namespace Info::Media {
std::optional<int> TypeToTabIndex(Type type) {
switch (type) {
case Type::Photo: return 0;
case Type::Video: return 1;
case Type::File: return 2;
}
return std::nullopt;
}
Type TabIndexToType(int index) {
switch (index) {
case 0: return Type::Photo;
case 1: return Type::Video;
case 2: return Type::File;
}
Unexpected("Index in Info::Media::TabIndexToType()");
}
tr::phrase<> SharedMediaTitle(Type type) {
switch (type) {
case Type::Photo:
return tr::lng_media_type_photos;
case Type::GIF:
return tr::lng_media_type_gifs;
case Type::Video:
return tr::lng_media_type_videos;
case Type::MusicFile:
return tr::lng_media_type_songs;
case Type::File:
return tr::lng_media_type_files;
case Type::RoundVoiceFile:
return tr::lng_media_type_audios;
case Type::Link:
return tr::lng_media_type_links;
case Type::RoundFile:
return tr::lng_media_type_rounds;
}
Unexpected("Bad media type in Info::TitleValue()");
}
Memento::Memento(not_null<Controller*> controller)
: Memento(
(controller->peer()
? controller->peer()
: controller->storiesPeer()
? controller->storiesPeer()
: controller->musicPeer()
? controller->musicPeer()
: controller->parentController()->session().user()),
controller->topic(),
controller->sublist(),
controller->migratedPeerId(),
(controller->section().type() == Section::Type::Downloads
? Type::File
: controller->section().type() == Section::Type::Stories
? Type::PhotoVideo
: controller->section().type() == Section::Type::SavedMusic
? Type::MusicFile
: controller->section().mediaType())) {
}
Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type)
: Memento(peer, nullptr, nullptr, migratedPeerId, type) {
}
Memento::Memento(not_null<Data::ForumTopic*> topic, Type type)
: Memento(topic->peer(), topic, nullptr, PeerId(), type) {
}
Memento::Memento(not_null<Data::SavedSublist*> sublist, Type type)
: Memento(sublist->owningHistory()->peer, nullptr, sublist, PeerId(), type) {
}
Memento::Memento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId,
Type type)
: ContentMemento(peer, topic, sublist, migratedPeerId)
, _type(type) {
_searchState.query.type = type;
_searchState.query.peerId = peer->id;
_searchState.query.topicRootId = topic ? topic->rootId() : MsgId();
_searchState.query.monoforumPeerId = sublist
? sublist->sublistPeer()->id
: PeerId();
_searchState.query.migratedPeerId = migratedPeerId;
if (migratedPeerId) {
_searchState.migratedList = Storage::SparseIdsList();
}
}
Section Memento::section() const {
return Section(_type);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(
parent,
controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(QWidget *parent, not_null<Controller*> controller)
: ContentWidget(parent, controller) {
_inner = setInnerWidget(object_ptr<InnerWidget>(
this,
controller));
_inner->setScrollHeightValue(scrollHeightValue());
_inner->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
scrollTo(request);
}, _inner->lifetime());
}
rpl::producer<SelectedItems> Widget::selectedListValue() const {
return _inner->selectedListValue();
}
void Widget::selectionAction(SelectionAction action) {
_inner->selectionAction(action);
}
rpl::producer<QString> Widget::title() {
if (controller()->key().peer()->sharedMediaInfo() && isStackBottom()) {
return tr::lng_profile_shared_media();
}
return SharedMediaTitle(controller()->section().mediaType())();
}
void Widget::setIsStackBottom(bool isStackBottom) {
ContentWidget::setIsStackBottom(isStackBottom);
_inner->setIsStackBottom(isStackBottom);
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (!controller()->validateMementoPeer(memento)) {
return false;
}
if (const auto mediaMemento = dynamic_cast<Memento*>(memento.get())) {
if (_inner->showInternal(mediaMemento)) {
return true;
}
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
}
} // namespace Info::Media

View File

@@ -0,0 +1,140 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "info/info_content_widget.h"
#include "storage/storage_shared_media.h"
#include "data/data_search_controller.h"
namespace tr {
template <typename ...Tags>
struct phrase;
} // namespace tr
namespace Data {
class ForumTopic;
} // namespace Data
namespace Info::Media {
using Type = Storage::SharedMediaType;
[[nodiscard]] std::optional<int> TypeToTabIndex(Type type);
[[nodiscard]] Type TabIndexToType(int index);
[[nodiscard]] tr::phrase<> SharedMediaTitle(Type type);
class InnerWidget;
class Memento final : public ContentMemento {
public:
explicit Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type);
Memento(not_null<Data::ForumTopic*> topic, Type type);
Memento(not_null<Data::SavedSublist*> sublist, Type type);
using SearchState = Api::DelayedSearchController::SavedState;
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
[[nodiscard]] Section section() const override;
[[nodiscard]] Type type() const {
return _type;
}
// Only for media, not for downloads.
void setAroundId(FullMsgId aroundId) {
_aroundId = aroundId;
}
[[nodiscard]] FullMsgId aroundId() const {
return _aroundId;
}
void setIdsLimit(int limit) {
_idsLimit = limit;
}
[[nodiscard]] int idsLimit() const {
return _idsLimit;
}
void setScrollTopItem(GlobalMsgId item) {
_scrollTopItem = item;
}
[[nodiscard]] GlobalMsgId scrollTopItem() const {
return _scrollTopItem;
}
void setScrollTopItemPosition(int64 position) {
_scrollTopItemPosition = position;
}
[[nodiscard]] int64 scrollTopItemPosition() const {
return _scrollTopItemPosition;
}
void setScrollTopShift(int shift) {
_scrollTopShift = shift;
}
[[nodiscard]] int scrollTopShift() const {
return _scrollTopShift;
}
void setSearchState(SearchState &&state) {
_searchState = std::move(state);
}
[[nodiscard]] SearchState searchState() {
return std::move(_searchState);
}
private:
Memento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId,
Type type);
Type _type = Type::Photo;
FullMsgId _aroundId;
int _idsLimit = 0;
int64 _scrollTopItemPosition = 0;
GlobalMsgId _scrollTopItem;
int _scrollTopShift = 0;
SearchState _searchState;
};
class Widget final : public ContentWidget {
public:
Widget(
QWidget *parent,
not_null<Controller*> controller);
void setIsStackBottom(bool isStackBottom) override;
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<SelectedItems> selectedListValue() const override;
void selectionAction(SelectionAction action) override;
rpl::producer<QString> title() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
InnerWidget *_inner = nullptr;
};
} // namespace Info::Media