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
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:
399
Telegram/SourceFiles/info/media/info_media_buttons.cpp
Normal file
399
Telegram/SourceFiles/info/media/info_media_buttons.cpp
Normal 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
|
||||
80
Telegram/SourceFiles/info/media/info_media_buttons.h
Normal file
80
Telegram/SourceFiles/info/media/info_media_buttons.h
Normal 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
|
||||
94
Telegram/SourceFiles/info/media/info_media_common.cpp
Normal file
94
Telegram/SourceFiles/info/media/info_media_common.cpp
Normal 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
|
||||
185
Telegram/SourceFiles/info/media/info_media_common.h
Normal file
185
Telegram/SourceFiles/info/media/info_media_common.h
Normal 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
|
||||
106
Telegram/SourceFiles/info/media/info_media_empty_widget.cpp
Normal file
106
Telegram/SourceFiles/info/media/info_media_empty_widget.cpp
Normal 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
|
||||
42
Telegram/SourceFiles/info/media/info_media_empty_widget.h
Normal file
42
Telegram/SourceFiles/info/media/info_media_empty_widget.h
Normal 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
|
||||
249
Telegram/SourceFiles/info/media/info_media_inner_widget.cpp
Normal file
249
Telegram/SourceFiles/info/media/info_media_inner_widget.cpp
Normal 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
|
||||
88
Telegram/SourceFiles/info/media/info_media_inner_widget.h
Normal file
88
Telegram/SourceFiles/info/media/info_media_inner_widget.h
Normal 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
|
||||
462
Telegram/SourceFiles/info/media/info_media_list_section.cpp
Normal file
462
Telegram/SourceFiles/info/media/info_media_list_section.cpp
Normal 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
|
||||
100
Telegram/SourceFiles/info/media/info_media_list_section.h
Normal file
100
Telegram/SourceFiles/info/media/info_media_list_section.h
Normal 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
|
||||
2717
Telegram/SourceFiles/info/media/info_media_list_widget.cpp
Normal file
2717
Telegram/SourceFiles/info/media/info_media_list_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
382
Telegram/SourceFiles/info/media/info_media_list_widget.h
Normal file
382
Telegram/SourceFiles/info/media/info_media_list_widget.h
Normal 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 §ion) 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
|
||||
578
Telegram/SourceFiles/info/media/info_media_provider.cpp
Normal file
578
Telegram/SourceFiles/info/media/info_media_provider.cpp
Normal 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
|
||||
125
Telegram/SourceFiles/info/media/info_media_provider.h
Normal file
125
Telegram/SourceFiles/info/media/info_media_provider.h
Normal 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
|
||||
201
Telegram/SourceFiles/info/media/info_media_widget.cpp
Normal file
201
Telegram/SourceFiles/info/media/info_media_widget.cpp
Normal 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
|
||||
140
Telegram/SourceFiles/info/media/info_media_widget.h
Normal file
140
Telegram/SourceFiles/info/media/info_media_widget.h
Normal 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
|
||||
Reference in New Issue
Block a user