Allow enabling direct messages in channels.

This commit is contained in:
John Preston 2025-05-06 17:48:18 +04:00
parent 23eedb468f
commit d3f9a84a0a
39 changed files with 685 additions and 242 deletions

View file

@ -1886,6 +1886,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_linked_channel_posted" = "All new posts from this channel are forwarded to the group.";
"lng_manage_discussion_group_warning" = "\"Chat history for new members\" will be switched to **Visible**.";
"lng_manage_monoforum" = "Direct Messages";
"lng_manage_monoforum_off" = "Off";
"lng_manage_monoforum_free" = "Free";
"lng_manage_monoforum_allow" = "Allow Direct Messages";
"lng_manage_monoforum_about" = "Allow users to write direct private messages to your channel, with the option to charge a fee for every message.";
"lng_manage_monoforum_price_about" = "Charge users for the ability to write a direct message to your channel. Your channel will receive {percent} of the selected fee ({amount}) for each incoming message.";
"lng_manage_history_visibility_title" = "Chat history for new members";
"lng_manage_history_visibility_shown" = "Visible";
"lng_manage_history_visibility_shown_about" = "New members will see messages that were sent before they joined.";
@ -2243,6 +2250,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_message_price_free" = "Messages are now free in this group.";
"lng_action_message_price_paid#one" = "Messages now cost {count} Star each in this group.";
"lng_action_message_price_paid#other" = "Messages now cost {count} Stars each in this group.";
"lng_action_direct_messages_enabled" = "Channel enabled Direct Messages.";
"lng_action_direct_messages_paid#one" = "Channel allows Direct Messages for {count} Star each.";
"lng_action_direct_messages_paid#other" = "Channel allows Direct Messages for {count} Stars each";
"lng_action_direct_messages_disabled" = "Channel disabled Direct Messages.";
"lng_you_paid_stars#one" = "You paid {count} Star.";
"lng_you_paid_stars#other" = "You paid {count} Stars.";

View file

@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_folder.h"
#include "data/data_forum_topic.h"
#include "data/data_forum.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_search_controller.h"
#include "data/data_session.h"
@ -381,6 +382,9 @@ void ApiWrap::savePinnedOrder(not_null<Data::Forum*> forum) {
}
void ApiWrap::savePinnedOrder(not_null<Data::SavedMessages*> saved) {
if (saved->parentChat()) {
return;
}
const auto &order = _session->data().pinnedChatsOrder(saved);
const auto input = [](Dialogs::Key key) {
if (const auto sublist = key.sublist()) {

View file

@ -1168,12 +1168,13 @@ rpl::producer<int> SetupChargeSlider(
struct State {
rpl::variable<int> stars;
};
const auto group = !peer->isUser();
const auto broadcast = peer->isBroadcast();
const auto group = !broadcast && !peer->isUser();
const auto state = container->lifetime().make_state<State>();
const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars;
state->stars = chargeStars;
Ui::AddSubsectionTitle(container, group
Ui::AddSubsectionTitle(container, (group || broadcast)
? tr::lng_rights_charge_price()
: tr::lng_messages_privacy_price());
@ -1225,7 +1226,9 @@ rpl::producer<int> SetupChargeSlider(
const auto percent = peer->session().appConfig().paidMessageCommission();
Ui::AddDividerText(
container,
(group
(broadcast
? tr::lng_manage_monoforum_price_about
: group
? tr::lng_rights_charge_price_about
: tr::lng_messages_privacy_price_about)(
lt_percent,
@ -1235,3 +1238,54 @@ rpl::producer<int> SetupChargeSlider(
return state->stars.value();
}
void EditDirectMessagesPriceBox(
not_null<Ui::GenericBox*> box,
not_null<ChannelData*> channel,
std::optional<int> savedValue,
Fn<void(std::optional<int>)> callback) {
box->setTitle(tr::lng_manage_monoforum());
const auto toggle = box->addRow(object_ptr<Ui::SettingsButton>(
box,
tr::lng_manage_monoforum_allow(),
st::settingsButtonNoIcon
), {})->toggleOn(rpl::single(savedValue.has_value()));
Ui::AddSkip(box->verticalLayout());
Ui::AddDividerText(
box->verticalLayout(),
tr::lng_manage_monoforum_about());
const auto wrap = box->addRow(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)),
{});
wrap->toggle(savedValue.has_value(), anim::type::instant);
wrap->toggleOn(toggle->toggledChanges());
const auto result = box->lifetime().make_state<int>(
savedValue.value_or(0));
const auto inner = wrap->entity();
Ui::AddSkip(inner);
SetupChargeSlider(
inner,
channel,
savedValue.value_or(0)
) | rpl::start_with_next([=](int stars) {
*result = stars;
}, box->lifetime());
box->addButton(tr::lng_settings_save(), [=] {
const auto weak = Ui::MakeWeak(box);
callback(toggle->toggled() ? *result : std::optional<int>());
if (const auto strong = weak.data()) {
strong->closeBox();
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}

View file

@ -174,3 +174,9 @@ void EditMessagesPrivacyBox(
not_null<Ui::VerticalLayout*> container,
not_null<PeerData*> peer,
int savedValue);
void EditDirectMessagesPriceBox(
not_null<Ui::GenericBox*> box,
not_null<ChannelData*> channel,
std::optional<int> savedValue,
Fn<void(std::optional<int>)> callback);

View file

@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/replace_boost_box.h"
#include "boxes/peers/verify_peers_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/edit_privacy_box.h" // EditDirectMessagesPriceBox
#include "boxes/stickers_box.h"
#include "boxes/username_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
@ -220,28 +221,41 @@ void SaveSlowmodeSeconds(
}
void SaveStarsPerMessage(
std::shared_ptr<Ui::Show> show,
not_null<ChannelData*> channel,
int starsPerMessage,
Fn<void()> done) {
Fn<void(bool)> done) {
const auto api = &channel->session().api();
const auto key = Api::RequestKey("stars_per_message", channel->id);
const auto broadcast = channel->isBroadcast();
using Flag = MTPchannels_UpdatePaidMessagesPrice::Flag;
const auto broadcastAllowed = broadcast && (starsPerMessage >= 0);
const auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice(
MTP_flags(0), // #TODO Support broadcast_messages_allowed flag in UI
MTP_flags(broadcastAllowed
? Flag::f_broadcast_messages_allowed
: Flag(0)),
channel->inputChannel,
MTP_long(starsPerMessage)
)).done([=](const MTPUpdates &result) {
api->clearModifyRequest(key);
api->applyUpdates(result);
channel->setStarsPerMessage(starsPerMessage);
done();
if (!broadcast) {
channel->setStarsPerMessage(starsPerMessage);
}
done(true);
}).fail([=](const MTP::Error &error) {
api->clearModifyRequest(key);
if (error.type() != u"CHAT_NOT_MODIFIED"_q) {
return;
show->showToast(error.type());
done(false);
} else {
if (!broadcast) {
channel->setStarsPerMessage(starsPerMessage);
}
done(true);
}
channel->setStarsPerMessage(starsPerMessage);
done();
}).send();
api->registerModifyRequest(key, requestId);
@ -281,6 +295,7 @@ void SaveBoostsUnrestrict(
void ShowEditPermissions(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer) {
const auto show = navigation->uiShow();
auto createBox = [=](not_null<Ui::GenericBox*> box) {
const auto saving = box->lifetime().make_state<int>(0);
const auto save = [=](
@ -299,7 +314,10 @@ void ShowEditPermissions(
channel,
result.boostsUnrestrict,
close);
SaveStarsPerMessage(channel, result.starsPerMessage, close);
const auto price = result.starsPerMessage;
SaveStarsPerMessage(show, channel, price, [=](bool ok) {
close();
});
}
};
auto done = [=](EditPeerPermissionsBoxResult result) {
@ -366,6 +384,7 @@ private:
std::optional<bool> joinToWrite;
std::optional<bool> requestToJoin;
std::optional<ChannelData*> discussionLink;
std::optional<int> starsPerDirectMessage;
};
[[nodiscard]] object_ptr<Ui::RpWidget> createPhotoAndTitleEdit();
@ -382,8 +401,10 @@ private:
void showEditPeerTypeBox(
std::optional<rpl::producer<QString>> error = {});
void showEditDiscussionLinkBox();
void showEditDirectMessagesBox();
void fillPrivacyTypeButton();
void fillDiscussionLinkButton();
void fillDirectMessagesButton();
//void fillInviteLinkButton();
void fillForumButton();
void fillColorIndexButton();
@ -412,6 +433,7 @@ private:
[[nodiscard]] bool validateUsernamesOrder(Saving &to) const;
[[nodiscard]] bool validateUsername(Saving &to) const;
[[nodiscard]] bool validateDiscussionLink(Saving &to) const;
[[nodiscard]] bool validateDirectMessagesPrice(Saving &to) const;
[[nodiscard]] bool validateTitle(Saving &to) const;
[[nodiscard]] bool validateDescription(Saving &to) const;
[[nodiscard]] bool validateHistoryVisibility(Saving &to) const;
@ -426,6 +448,7 @@ private:
void saveUsernamesOrder();
void saveUsername();
void saveDiscussionLink();
void saveDirectMessagesPrice();
void saveTitle();
void saveDescription();
void saveHistoryVisibility();
@ -454,6 +477,7 @@ private:
std::optional<ChannelData*> _discussionLinkSavedValue;
ChannelData *_discussionLinkOriginalValue = nullptr;
bool _channelHasLocationOriginalValue = false;
std::optional<rpl::variable<int>> _starsPerDirectMessageSavedValue;
std::optional<HistoryVisibility> _historyVisibilitySavedValue;
std::optional<EditPeerTypeData> _typeDataSavedValue;
std::optional<bool> _forumSavedValue;
@ -918,6 +942,20 @@ void Controller::showEditDiscussionLinkBox() {
}).send();
}
void Controller::showEditDirectMessagesBox() {
Expects(_peer->isBroadcast());
Expects(_starsPerDirectMessageSavedValue.has_value());
const auto stars = _starsPerDirectMessageSavedValue->current();
_navigation->parentController()->show(Box(
EditDirectMessagesPriceBox,
_peer->asChannel(),
(stars >= 0) ? stars : std::optional<int>(),
[=](std::optional<int> value) {
*_starsPerDirectMessageSavedValue = value.value_or(-1);
}));
}
void Controller::fillPrivacyTypeButton() {
Expects(_controls.buttonsLayout != nullptr);
@ -983,9 +1021,11 @@ void Controller::fillPrivacyTypeButton() {
void Controller::fillDiscussionLinkButton() {
Expects(_controls.buttonsLayout != nullptr);
_discussionLinkSavedValue = _discussionLinkOriginalValue = _peer->isChannel()
? _peer->asChannel()->discussionLink()
: nullptr;
_discussionLinkSavedValue
= _discussionLinkOriginalValue
= (_peer->isChannel()
? _peer->asChannel()->discussionLink()
: nullptr);
const auto isGroup = (_peer->isChat() || _peer->isMegagroup());
auto text = !isGroup
@ -1019,6 +1059,33 @@ void Controller::fillDiscussionLinkButton() {
{ isGroup ? &st::menuIconChannel : &st::menuIconGroups });
_discussionLinkUpdates.fire_copy(*_discussionLinkSavedValue);
}
void Controller::fillDirectMessagesButton() {
Expects(_controls.buttonsLayout != nullptr);
if (!_peer->isBroadcast() || !_peer->asChannel()->canEditInformation()) {
return;
}
const auto monoforumLink = _peer->asChannel()->monoforumLink();
_starsPerDirectMessageSavedValue = rpl::variable<int>(
monoforumLink ? monoforumLink->starsPerMessage() : -1);
auto label = _starsPerDirectMessageSavedValue->value(
) | rpl::map([](int starsPerMessage) {
return (starsPerMessage < 0)
? tr::lng_manage_monoforum_off()
: !starsPerMessage
? tr::lng_manage_monoforum_free()
: rpl::single(Lang::FormatCountDecimal(starsPerMessage));
}) | rpl::flatten_latest();
AddButtonWithText(
_controls.buttonsLayout,
tr::lng_manage_monoforum(),
std::move(label),
[=] { showEditDirectMessagesBox(); },
{ &st::menuIconChatBubble });
}
//
//void Controller::fillInviteLinkButton() {
// Expects(_controls.buttonsLayout != nullptr);
@ -1359,6 +1426,8 @@ void Controller::fillManageSection() {
const auto canViewOrEditDiscussionLink = isChannel
&& (channel->discussionLink()
|| (channel->isBroadcast() && channel->canEditInformation()));
const auto canEditDirectMessages = isChannel
&& (channel->isBroadcast() && channel->canEditInformation());
::AddSkip(_controls.buttonsLayout, 0);
@ -1370,6 +1439,9 @@ void Controller::fillManageSection() {
if (canViewOrEditDiscussionLink) {
fillDiscussionLinkButton();
}
if (canEditDirectMessages) {
fillDirectMessagesButton();
}
if (canEditPreHistoryHidden) {
fillHistoryVisibilityButton();
}
@ -1973,6 +2045,7 @@ std::optional<Controller::Saving> Controller::validate() const {
if (validateUsernamesOrder(result)
&& validateUsername(result)
&& validateDiscussionLink(result)
&& validateDirectMessagesPrice(result)
&& validateTitle(result)
&& validateDescription(result)
&& validateHistoryVisibility(result)
@ -2022,6 +2095,14 @@ bool Controller::validateDiscussionLink(Saving &to) const {
return true;
}
bool Controller::validateDirectMessagesPrice(Saving &to) const {
if (!_starsPerDirectMessageSavedValue) {
return true;
}
to.starsPerDirectMessage = _starsPerDirectMessageSavedValue->current();
return true;
}
bool Controller::validateTitle(Saving &to) const {
if (!_controls.title) {
return true;
@ -2120,6 +2201,7 @@ void Controller::save() {
pushSaveStage([=] { saveUsernamesOrder(); });
pushSaveStage([=] { saveUsername(); });
pushSaveStage([=] { saveDiscussionLink(); });
pushSaveStage([=] { saveDirectMessagesPrice(); });
pushSaveStage([=] { saveTitle(); });
pushSaveStage([=] { saveDescription(); });
pushSaveStage([=] { saveHistoryVisibility(); });
@ -2277,6 +2359,30 @@ void Controller::saveDiscussionLink() {
}).send();
}
void Controller::saveDirectMessagesPrice() {
const auto channel = _peer->asChannel();
if (!channel) {
return continueSave();
}
const auto monoforumLink = channel->monoforumLink();
const auto current = monoforumLink ? monoforumLink->starsPerMessage() : -1;
const auto desired = _savingData.starsPerDirectMessage
? *_savingData.starsPerDirectMessage
: current;
if (desired == current) {
return continueSave();
}
const auto show = _navigation->uiShow();
const auto done = [=](bool ok) {
if (ok) {
continueSave();
} else {
cancelSave();
}
};
SaveStarsPerMessage(show, channel, desired, crl::guard(this, done));
}
void Controller::saveTitle() {
if (!_savingData.title || *_savingData.title == _peer->name()) {
return continueSave();

View file

@ -907,6 +907,7 @@ void PinsLimitBox(
limits.dialogsPinnedPremium(),
PinsCount(session->data().chatsList()));
}
void SublistsPinsLimitBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session) {

View file

@ -871,6 +871,10 @@ historyGiftToChannel: IconButton(defaultIconButton) {
rippleAreaSize: 40px;
ripple: universalRippleAnimation;
}
historyDirectMessage: IconButton(historyGiftToChannel) {
icon: icon{{ "menu/chat_bubble", windowActiveTextFg }};
iconOver: icon{{ "menu/chat_bubble", windowActiveTextFg }};
}
historyUnblock: FlatButton(historyComposeButton) {
color: attentionButtonFg;
overColor: attentionButtonFgOver;

View file

@ -112,12 +112,13 @@ struct PeerUpdate {
StickersSet = (1ULL << 46),
EmojiSet = (1ULL << 47),
DiscussionLink = (1ULL << 48),
ChannelLocation = (1ULL << 49),
Slowmode = (1ULL << 50),
GroupCall = (1ULL << 51),
MonoforumLink = (1ULL << 49),
ChannelLocation = (1ULL << 50),
Slowmode = (1ULL << 51),
GroupCall = (1ULL << 52),
// For iteration
LastUsedBit = (1ULL << 51),
LastUsedBit = (1ULL << 52),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_histories.h"
#include "data/data_group_call.h"
#include "data/data_message_reactions.h"
#include "data/data_saved_messages.h"
#include "data/data_wall_paper.h"
#include "data/notify/data_notify_settings.h"
#include "main/main_session.h"
@ -89,6 +90,29 @@ std::unique_ptr<Data::Forum> MegagroupInfo::takeForumData() {
return nullptr;
}
void MegagroupInfo::ensureMonoforum(not_null<ChannelData*> that) {
if (!_monoforum) {
const auto history = that->owner().history(that);
_monoforum = std::make_unique<Data::SavedMessages>(
&that->owner(),
that);
history->monoforumChanged(nullptr);
}
}
Data::SavedMessages *MegagroupInfo::monoforum() const {
return _monoforum.get();
}
std::unique_ptr<Data::SavedMessages> MegagroupInfo::takeMonoforumData() {
if (auto result = base::take(_monoforum)) {
const auto history = result->owner().history(result->parentChat());
history->monoforumChanged(result.get());
return result;
}
return nullptr;
}
ChannelData::ChannelData(not_null<Data::Session*> owner, PeerId id)
: PeerData(owner, id)
, inputChannel(
@ -161,6 +185,12 @@ void ChannelData::setAccessHash(uint64 accessHash) {
}
void ChannelData::setFlags(ChannelDataFlags which) {
if (which & (Flag::Forum | Flag::Monoforum)) {
which |= Flag::Megagroup;
}
if (which & Flag::Monoforum) {
which &= ~Flag::Forum;
}
const auto diff = flags() ^ which;
if ((which & Flag::Megagroup) && !mgInfo) {
mgInfo = std::make_unique<MegagroupInfo>();
@ -276,8 +306,9 @@ const ChannelLocation *ChannelData::getLocation() const {
}
void ChannelData::setDiscussionLink(ChannelData *linked) {
if (_discussionLink != linked) {
if (_discussionLink != linked || !_discussionLinkKnown) {
_discussionLink = linked;
_discussionLinkKnown = true;
if (const auto history = owner().historyLoaded(this)) {
history->forceFullResize();
}
@ -286,11 +317,22 @@ void ChannelData::setDiscussionLink(ChannelData *linked) {
}
ChannelData *ChannelData::discussionLink() const {
return _discussionLink.value_or(nullptr);
return _discussionLink;
}
bool ChannelData::discussionLinkKnown() const {
return _discussionLink.has_value();
return _discussionLinkKnown;
}
void ChannelData::setMonoforumLink(ChannelData *link) {
if (_monoforumLink != link) {
_monoforumLink = link;
session().changes().peerUpdated(this, UpdateFlag::MonoforumLink);
}
}
ChannelData *ChannelData::monoforumLink() const {
return _monoforumLink;
}
void ChannelData::setMembersCount(int newMembersCount) {
@ -1240,6 +1282,11 @@ void ApplyChannelUpdate(
} else {
channel->setDiscussionLink(nullptr);
}
if (const auto chat = update.vlinked_monoforum_id()) {
channel->setMonoforumLink(channel->owner().channelLoaded(chat->v));
} else {
channel->setMonoforumLink(nullptr);
}
if (const auto history = channel->owner().historyLoaded(channel)) {
if (const auto available = update.vavailable_min_id()) {
history->clearUpTill(available->v);

View file

@ -16,6 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class ChannelData;
namespace Data {
class Forum;
class SavedMessages;
} // namespace Data
struct ChannelLocation {
QString address;
Data::LocationPoint point;
@ -74,6 +79,7 @@ enum class ChannelDataFlag : uint64 {
StargiftsAvailable = (1ULL << 36),
PaidMessagesAvailable = (1ULL << 37),
AutoTranslation = (1ULL << 38),
Monoforum = (1ULL << 39),
};
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
using ChannelDataFlags = base::flags<ChannelDataFlag>;
@ -118,6 +124,10 @@ public:
[[nodiscard]] Data::Forum *forum() const;
[[nodiscard]] std::unique_ptr<Data::Forum> takeForumData();
void ensureMonoforum(not_null<ChannelData*> that);
[[nodiscard]] Data::SavedMessages *monoforum() const;
[[nodiscard]] std::unique_ptr<Data::SavedMessages> takeMonoforumData();
std::deque<not_null<UserData*>> lastParticipants;
base::flat_map<not_null<UserData*>, Admin> lastAdmins;
base::flat_map<not_null<UserData*>, Restricted> lastRestricted;
@ -154,6 +164,7 @@ private:
ChannelLocation _location;
Data::ChatBotCommands _botCommands;
std::unique_ptr<Data::Forum> _forum;
std::unique_ptr<Data::SavedMessages> _monoforum;
int _starsPerMessage = 0;
friend class ChannelData;
@ -301,6 +312,9 @@ public:
[[nodiscard]] bool isForum() const {
return flags() & Flag::Forum;
}
[[nodiscard]] bool isMonoforum() const {
return flags() & Flag::Monoforum;
}
[[nodiscard]] bool hasUsername() const {
return flags() & Flag::Username;
}
@ -413,6 +427,9 @@ public:
[[nodiscard]] ChannelData *discussionLink() const;
[[nodiscard]] bool discussionLinkKnown() const;
void setMonoforumLink(ChannelData *link);
[[nodiscard]] ChannelData *monoforumLink() const;
void ptsInit(int32 pts) {
_ptsWaiter.init(pts);
}
@ -510,6 +527,9 @@ public:
[[nodiscard]] Data::Forum *forum() const {
return mgInfo ? mgInfo->forum() : nullptr;
}
[[nodiscard]] Data::SavedMessages *monoforum() const {
return mgInfo ? mgInfo->monoforum() : nullptr;
}
void processTopics(const MTPVector<MTPForumTopic> &topics);
@ -546,18 +566,11 @@ private:
std::vector<Data::UnavailableReason> &&reasons) override;
Flags _flags = ChannelDataFlags(Flag::Forbidden);
int _peerGiftsCount = 0;
PtsWaiter _ptsWaiter;
Data::UsernamesInfo _username;
int _membersCount = -1;
int _adminsCount = 1;
int _restrictedCount = 0;
int _kickedCount = 0;
int _pendingRequestsCount = 0;
int _levelHint = 0;
std::vector<UserId> _recentRequesters;
MsgId _availableMinId = 0;
@ -570,7 +583,18 @@ private:
std::vector<Data::UnavailableReason> _unavailableReasons;
std::unique_ptr<InvitePeek> _invitePeek;
QString _inviteLink;
std::optional<ChannelData*> _discussionLink;
ChannelData *_discussionLink = nullptr;
ChannelData *_monoforumLink = nullptr;
bool _discussionLinkKnown = false;
int _peerGiftsCount = 0;
int _membersCount = -1;
int _adminsCount = 1;
int _restrictedCount = 0;
int _kickedCount = 0;
int _pendingRequestsCount = 0;
int _levelHint = 0;
Data::AllowedReactions _allowedReactions;

View file

@ -159,7 +159,8 @@ bool CanSendAnyOf(
using Flag = ChannelDataFlag;
const auto allowed = channel->amIn()
|| ((channel->flags() & Flag::HasLink)
&& !(channel->flags() & Flag::JoinToWrite));
&& !(channel->flags() & Flag::JoinToWrite))
|| channel->isMonoforum();
if (!allowed || (forbidInForums && channel->isForum())) {
return false;
} else if (channel->canPostMessages()) {

View file

@ -1333,6 +1333,13 @@ bool PeerData::isForum() const {
return false;
}
bool PeerData::isMonoforum() const {
if (const auto channel = asChannel()) {
return channel->isMonoforum();
}
return false;
}
bool PeerData::isGigagroup() const {
if (const auto channel = asChannel()) {
return channel->isGigagroup();
@ -1416,6 +1423,13 @@ Data::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const {
return nullptr;
}
Data::SavedMessages *PeerData::monoforum() const {
if (const auto channel = asChannel()) {
return channel->monoforum();
}
return nullptr;
}
bool PeerData::allowsForwarding() const {
if (isUser()) {
return true;

View file

@ -37,6 +37,7 @@ class Forum;
class ForumTopic;
class Session;
class GroupCall;
class SavedMessages;
struct ReactionId;
class WallPaper;
@ -232,6 +233,7 @@ public:
[[nodiscard]] bool isMegagroup() const;
[[nodiscard]] bool isBroadcast() const;
[[nodiscard]] bool isForum() const;
[[nodiscard]] bool isMonoforum() const;
[[nodiscard]] bool isGigagroup() const;
[[nodiscard]] bool isRepliesChat() const;
[[nodiscard]] bool isVerifyCodes() const;
@ -257,6 +259,8 @@ public:
[[nodiscard]] Data::Forum *forum() const;
[[nodiscard]] Data::ForumTopic *forumTopicFor(MsgId rootId) const;
[[nodiscard]] Data::SavedMessages *monoforum() const;
[[nodiscard]] Data::PeerNotifySettings &notify() {
return _notify;
}

View file

@ -269,6 +269,7 @@ inline auto DefaultRestrictionValue(
| Flag::Left
| Flag::Forum
| Flag::JoinToWrite
| Flag::Monoforum
| Flag::HasLink
| Flag::Forbidden
| Flag::Creator
@ -292,7 +293,8 @@ inline auto DefaultRestrictionValue(
&& (flags & Flag::Forum);
const auto allowed = !(flags & notAmInFlags)
|| ((flags & Flag::HasLink)
&& !(flags & Flag::JoinToWrite));
&& !(flags & Flag::JoinToWrite))
|| (flags & Flag::Monoforum);
const auto restricted = sendRestriction
| (defaultSendRestriction && !unrestrictedByBoosts);
return allowed

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_saved_messages.h"
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
@ -25,12 +26,15 @@ constexpr auto kListFirstPerPage = 20;
} // namespace
SavedMessages::SavedMessages(not_null<Session*> owner)
SavedMessages::SavedMessages(
not_null<Session*> owner,
ChannelData *parentChat)
: _owner(owner)
, _parentChat(parentChat)
, _chatsList(
&owner->session(),
&_owner->session(),
FilterId(),
owner->maxPinnedChatsLimitValue(this))
_owner->maxPinnedChatsLimitValue(this))
, _loadMore([=] { sendLoadMoreRequests(); }) {
}
@ -40,6 +44,10 @@ bool SavedMessages::supported() const {
return !_unsupported;
}
ChannelData *SavedMessages::parentChat() const {
return _parentChat;
}
Session &SavedMessages::owner() const {
return *_owner;
}
@ -59,7 +67,11 @@ not_null<SavedSublist*> SavedMessages::sublist(not_null<PeerData*> peer) {
}
return _sublists.emplace(
peer,
std::make_unique<SavedSublist>(peer)).first->second.get();
std::make_unique<SavedSublist>(this, peer)).first->second.get();
}
rpl::producer<> SavedMessages::chatsListChanges() const {
return _chatsListChanges.events();
}
void SavedMessages::loadMore() {
@ -78,10 +90,12 @@ void SavedMessages::sendLoadMore() {
} else if (!_pinnedLoaded) {
loadPinned();
}
using Flag = MTPmessages_GetSavedDialogs::Flag;
_loadMoreRequestId = _owner->session().api().request(
MTPmessages_GetSavedDialogs(
MTP_flags(MTPmessages_GetSavedDialogs::Flag::f_exclude_pinned),
MTPInputPeer(), // parent_peer
MTP_flags(Flag::f_exclude_pinned
| (_parentChat ? Flag::f_parent_peer : Flag(0))),
_parentChat ? _parentChat->input : MTPInputPeer(),
MTP_int(_offsetDate),
MTP_int(_offsetId),
_offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(),
@ -89,6 +103,7 @@ void SavedMessages::sendLoadMore() {
MTP_long(0)) // hash
).done([=](const MTPmessages_SavedDialogs &result) {
apply(result, false);
_chatsListChanges.fire({});
}).fail([=](const MTP::Error &error) {
if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) {
_unsupported = true;
@ -99,13 +114,14 @@ void SavedMessages::sendLoadMore() {
}
void SavedMessages::loadPinned() {
if (_pinnedRequestId) {
if (_pinnedRequestId || parentChat()) {
return;
}
_pinnedRequestId = _owner->session().api().request(
MTPmessages_GetPinnedSavedDialogs()
).done([=](const MTPmessages_SavedDialogs &result) {
apply(result, true);
_chatsListChanges.fire({});
}).fail([=](const MTP::Error &error) {
if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) {
_unsupported = true;
@ -124,10 +140,11 @@ void SavedMessages::sendLoadMore(not_null<SavedSublist*> sublist) {
const auto offsetId = list.empty() ? MsgId(0) : list.back()->id;
const auto offsetDate = list.empty() ? MsgId(0) : list.back()->date();
const auto limit = offsetId ? kPerPage : kFirstPerPage;
using Flag = MTPmessages_GetSavedHistory::Flag;
const auto requestId = _owner->session().api().request(
MTPmessages_GetSavedHistory(
MTP_flags(0),
MTPInputPeer(), // parent_peer
MTP_flags(_parentChat ? Flag::f_parent_peer : Flag(0)),
_parentChat ? _parentChat->input : MTPInputPeer(),
sublist->peer()->input,
MTP_int(offsetId),
MTP_int(offsetDate),
@ -261,6 +278,8 @@ void SavedMessages::sendLoadMoreRequests() {
}
void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) {
Expects(!parentChat());
const auto list = update.vorder();
if (!list) {
loadPinned();
@ -286,6 +305,8 @@ void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) {
}
void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) {
Expects(!parentChat());
update.vpeer().match([&](const MTPDdialogPeer &data) {
const auto peer = _owner->peer(peerFromMTP(data.vpeer()));
const auto i = _sublists.find(peer);
@ -300,4 +321,8 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) {
});
}
rpl::lifetime &SavedMessages::lifetime() {
return _lifetime;
}
} // namespace Data

View file

@ -20,10 +20,13 @@ class SavedSublist;
class SavedMessages final {
public:
explicit SavedMessages(not_null<Session*> owner);
explicit SavedMessages(
not_null<Session*> owner,
ChannelData *parentChat = nullptr);
~SavedMessages();
[[nodiscard]] bool supported() const;
[[nodiscard]] ChannelData *parentChat() const;
[[nodiscard]] Session &owner() const;
[[nodiscard]] Main::Session &session() const;
@ -31,12 +34,16 @@ public:
[[nodiscard]] not_null<Dialogs::MainList*> chatsList();
[[nodiscard]] not_null<SavedSublist*> sublist(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<> chatsListChanges() const;
void loadMore();
void loadMore(not_null<SavedSublist*> sublist);
void apply(const MTPDupdatePinnedSavedDialogs &update);
void apply(const MTPDupdateSavedDialogPinned &update);
[[nodiscard]] rpl::lifetime &lifetime();
private:
void loadPinned();
void apply(const MTPmessages_SavedDialogs &result, bool pinned);
@ -46,6 +53,7 @@ private:
void sendLoadMoreRequests();
const not_null<Session*> _owner;
ChannelData *_parentChat = nullptr;
Dialogs::MainList _chatsList;
base::flat_map<
@ -64,9 +72,13 @@ private:
base::flat_set<not_null<SavedSublist*>> _loadMoreSublistsScheduled;
bool _loadMoreScheduled = false;
rpl::event_stream<> _chatsListChanges;
bool _pinnedLoaded = false;
bool _unsupported = false;
rpl::lifetime _lifetime;
};
} // namespace Data

View file

@ -17,13 +17,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
SavedSublist::SavedSublist(not_null<PeerData*> peer)
SavedSublist::SavedSublist(
not_null<SavedMessages*> parent,
not_null<PeerData*> peer)
: Entry(&peer->owner(), Dialogs::Entry::Type::SavedSublist)
, _parent(parent)
, _history(peer->owner().history(peer)) {
}
SavedSublist::~SavedSublist() = default;
not_null<SavedMessages*> SavedSublist::parent() const {
return _parent;
}
ChannelData *SavedSublist::parentChat() const {
return _parent->parentChat();
}
not_null<History*> SavedSublist::history() const {
return _history;
}
@ -101,9 +112,7 @@ void SavedSublist::removeOne(not_null<HistoryItem*> item) {
updateChatListExistence();
} else {
updateChatListEntry();
crl::on_main(this, [=] {
owner().savedMessages().loadMore(this);
});
crl::on_main(this, [=] { _parent->loadMore(this); });
}
} else {
setChatListTimeId(_items.front()->date());

View file

@ -16,12 +16,15 @@ class History;
namespace Data {
class Session;
class SavedMessages;
class SavedSublist final : public Dialogs::Entry {
public:
explicit SavedSublist(not_null<PeerData*> peer);
SavedSublist(not_null<SavedMessages*> parent,not_null<PeerData*> peer);
~SavedSublist();
[[nodiscard]] not_null<SavedMessages*> parent() const;
[[nodiscard]] ChannelData *parentChat() const;
[[nodiscard]] not_null<History*> history() const;
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] bool isHiddenAuthor() const;
@ -72,6 +75,7 @@ private:
void allowChatListMessageResolve();
void resolveChatListMessageGroup();
const not_null<SavedMessages*> _parent;
const not_null<History*> _history;
std::vector<not_null<HistoryItem*>> _items;

View file

@ -967,7 +967,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| ((!minimal && !data.is_stories_hidden_min())
? Flag::StoriesHidden
: Flag())
| Flag::AutoTranslation;
| Flag::AutoTranslation
| Flag::Monoforum;
const auto storiesState = minimal
? std::optional<Data::Stories::PeerSourceState>()
: data.is_stories_unavailable()
@ -1007,7 +1008,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
&& data.is_stories_hidden())
? Flag::StoriesHidden
: Flag())
| (data.is_autotranslation() ? Flag::AutoTranslation : Flag());
| (data.is_autotranslation() ? Flag::AutoTranslation : Flag())
| (data.is_monoforum() ? Flag::Monoforum : Flag());
channel->setFlags((channel->flags() & ~flagsMask) | flagsSet);
channel->setBotVerifyDetailsIcon(
data.vbot_verification_icon().value_or_empty());
@ -2310,6 +2312,9 @@ void Session::applyDialog(
bool Session::pinnedCanPin(not_null<Dialogs::Entry*> entry) const {
if ([[maybe_unused]] const auto sublist = entry->asSublist()) {
if (sublist->parentChat()) {
return false;
}
const auto saved = &savedMessages();
return pinnedChatsOrder(saved).size() < pinnedChatsLimit(saved);
} else if (const auto topic = entry->asTopic()) {
@ -2351,6 +2356,9 @@ int Session::pinnedChatsLimit(not_null<Data::Forum*> forum) const {
}
int Session::pinnedChatsLimit(not_null<Data::SavedMessages*> saved) const {
if (saved->parentChat()) {
return 0;
}
const auto limits = Data::PremiumLimits(_session);
return limits.savedSublistsPinnedCurrent();
}
@ -2391,6 +2399,9 @@ rpl::producer<int> Session::maxPinnedChatsLimitValue(
rpl::producer<int> Session::maxPinnedChatsLimitValue(
not_null<SavedMessages*> saved) const {
if (saved->parentChat()) {
return rpl::single(0);
}
// Premium limit from appconfig.
// We always use premium limit in the MainList limit producer,
// because it slices the list to that limit. We don't want to slice
@ -4563,12 +4574,12 @@ not_null<Folder*> Session::processFolder(const MTPDfolder &data) {
not_null<Dialogs::MainList*> Session::chatsListFor(
not_null<Dialogs::Entry*> entry) {
const auto topic = entry->asTopic();
return topic
? topic->forum()->topicsList()
: entry->asSublist()
? _savedMessages->chatsList()
: chatsList(entry->folder());
if (const auto topic = entry->asTopic()) {
return topic->forum()->topicsList();
} else if (const auto sublist = entry->asSublist()) {
return sublist->parent()->chatsList();
}
return chatsList(entry->folder());
}
not_null<Dialogs::MainList*> Session::chatsList(Data::Folder *folder) {

View file

@ -781,11 +781,14 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) {
}
}
void InnerWidget::showSavedSublists() {
void InnerWidget::showSavedSublists(ChannelData *parentChat) {
Expects(!parentChat || parentChat->monoforum());
Expects(!_geometryInited);
Expects(!_savedSublists);
_savedSublists = true;
_savedSublists = parentChat
? parentChat->monoforum()
: &session().data().savedMessages();
stopReorderPinned();
clearSelection();
@ -2115,7 +2118,7 @@ bool InnerWidget::addQuickActionRipple(
const std::vector<Key> &InnerWidget::pinnedChatsOrder() const {
const auto owner = &session().data();
return _savedSublists
? owner->pinnedChatsOrder(&owner->savedMessages())
? owner->pinnedChatsOrder(_savedSublists)
: _openedForum
? owner->pinnedChatsOrder(_openedForum)
: _filterId
@ -2179,6 +2182,9 @@ int InnerWidget::countPinnedIndex(Row *ofRow) {
}
void InnerWidget::savePinnedOrder() {
if (_savedSublists && _savedSublists->parentChat()) {
return;
}
const auto &newOrder = pinnedChatsOrder();
if (newOrder.size() != _pinnedOnDragStart.size()) {
return; // Something has changed in the set of pinned chats.
@ -2316,8 +2322,11 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) {
const auto delta = [&] {
if (localPosition.y() < _visibleTop) {
return localPosition.y() - _visibleTop;
} else if ((_savedSublists || _openedFolder || _openedForum || _filterId)
&& localPosition.y() > _visibleBottom) {
} else if ((localPosition.y() > _visibleBottom)
&& (_savedSublists
|| _openedFolder
|| _openedForum
|| _filterId)) {
return localPosition.y() - _visibleBottom;
}
return 0;
@ -2685,8 +2694,8 @@ void InnerWidget::handleChatListEntryRefreshes() {
return false;
} else if (const auto topic = event.key.topic()) {
return (topic->forum() == _openedForum);
} else if (event.key.sublist()) {
return _savedSublists;
} else if (const auto sublist = event.key.sublist()) {
return sublist->parent() == _savedSublists;
} else {
return !_openedForum;
}
@ -2704,7 +2713,7 @@ void InnerWidget::handleChatListEntryRefreshes() {
&& (key.topic()
? (key.topic()->forum() == _openedForum)
: key.sublist()
? _savedSublists
? (key.sublist()->parent() == _savedSublists)
: (entry->folder() == _openedFolder))) {
_dialogMoved.fire({ from, to });
}
@ -2909,7 +2918,8 @@ void InnerWidget::enterEventHook(QEnterEvent *e) {
Row *InnerWidget::shownRowByKey(Key key) {
const auto entry = key.entry();
if (_savedSublists) {
if (!entry->asSublist()) {
const auto sublist = entry->asSublist();
if (!sublist || sublist->parent() != _savedSublists) {
return nullptr;
}
} else if (_openedForum) {
@ -2978,7 +2988,7 @@ void InnerWidget::updateSelectedRow(Key key) {
void InnerWidget::refreshShownList() {
const auto list = _savedSublists
? session().data().savedMessages().chatsList()->indexed()
? _savedSublists->chatsList()->indexed()
: _openedForum
? _openedForum->topicsList()->indexed()
: _filterId
@ -3440,8 +3450,7 @@ void InnerWidget::applySearchState(SearchState state) {
};
if (_searchState.filterChatsList() && !words.isEmpty()) {
if (_savedSublists) {
const auto owner = &session().data();
append(owner->savedMessages().chatsList()->indexed());
append(_savedSublists->chatsList()->indexed());
} else if (_openedForum) {
append(_openedForum->topicsList()->indexed());
} else {
@ -4012,7 +4021,7 @@ void InnerWidget::refreshEmpty() {
const auto state = !_shownList->empty()
? EmptyState::None
: _savedSublists
? (data->savedMessages().chatsList()->loaded()
? (_savedSublists->chatsList()->loaded()
? EmptyState::EmptySavedSublists
: EmptyState::Loading)
: _openedForum

View file

@ -58,6 +58,7 @@ class ChatFilter;
class Thread;
class Folder;
class Forum;
class SavedMessages;
struct ReactionId;
} // namespace Data
@ -140,7 +141,7 @@ public:
void changeOpenedFolder(Data::Folder *folder);
void changeOpenedForum(Data::Forum *forum);
void showSavedSublists();
void showSavedSublists(ChannelData *parentChat);
void selectSkip(int32 direction);
void selectSkipPage(int32 pixels, int32 direction);
@ -668,7 +669,8 @@ private:
float64 _narrowRatio = 0.;
bool _geometryInited = false;
bool _savedSublists = false;
Data::SavedMessages *_savedSublists = nullptr;
bool _searchLoading = false;
bool _searchWaiting = false;

View file

@ -1712,6 +1712,7 @@ ServiceAction ParseServiceAction(
}, [&](const MTPDmessageActionPaidMessagesPrice &data) {
result.content = ActionPaidMessagesPrice{
.stars = int(data.vstars().v),
.broadcastAllowed = data.is_broadcast_messages_allowed(),
};
}, [&](const MTPDmessageActionConferenceCall &data) {
auto content = ActionPhoneCall();

View file

@ -673,6 +673,7 @@ struct ActionPaidMessagesRefunded {
struct ActionPaidMessagesPrice {
int stars = 0;
bool broadcastAllowed = false;
};
struct ServiceAction {

View file

@ -1383,7 +1383,15 @@ auto HtmlWriter::Wrap::pushMessage(
+ " messages to you");
return result;
}, [&](const ActionPaidMessagesPrice &data) {
auto result = "Price per messages changed to "
if (isChannel) {
auto result = !data.broadcastAllowed
? "Direct messages were disabled."
: ("Price per direct message changed to "
+ QString::number(data.stars).toUtf8()
+ " Telegram Stars.");
return result;
}
auto result = "Price per message changed to "
+ QString::number(data.stars).toUtf8()
+ " Telegram Stars.";
return result;

View file

@ -679,6 +679,7 @@ QByteArray SerializeMessage(
pushActor();
pushAction("paid_messages_price_change");
push("price_stars", data.stars);
push("is_broadcast_messages_allowed", data.broadcastAllowed);
}, [](v::null_t) {});
if (v::is_null(message.action.content)) {

View file

@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/notify/data_notify_settings.h"
#include "data/stickers/data_stickers.h"
#include "data/data_drafts.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_media_types.h"
@ -3131,6 +3132,42 @@ bool History::isForum() const {
return (_flags & Flag::IsForum);
}
void History::monoforumChanged(Data::SavedMessages *old) {
if (inChatList()) {
notifyUnreadStateChange(old
? AdjustedForumUnreadState(old->chatsList()->unreadState())
: computeUnreadState());
}
if (const auto monoforum = peer->monoforum()) {
_flags |= Flag::IsMonoforum;
monoforum->chatsList()->unreadStateChanges(
) | rpl::filter([=] {
return (_flags & Flag::IsMonoforum) && inChatList();
}) | rpl::map(
AdjustedForumUnreadState
) | rpl::start_with_next([=](const Dialogs::UnreadState &old) {
notifyUnreadStateChange(old);
}, monoforum->lifetime());
monoforum->chatsListChanges(
) | rpl::start_with_next([=] {
updateChatListEntry();
}, monoforum->lifetime());
} else {
_flags &= ~Flag::IsMonoforum;
}
if (cloudDraft(MsgId(0))) {
updateChatListSortPosition();
}
_flags |= Flag::PendingAllItemsResize;
}
bool History::isMonoforum() const {
return (_flags & Flag::IsMonoforum);
}
not_null<History*> History::migrateToOrMe() const {
if (const auto to = peer->migrateTo()) {
return owner().history(to);

View file

@ -27,12 +27,14 @@ struct LanguageId;
namespace Data {
struct Draft;
class Forum;
class Session;
class Folder;
class ChatFilter;
struct SponsoredFrom;
class SponsoredMessages;
class HistoryMessages;
class SavedMessages;
} // namespace Data
namespace Dialogs {
@ -71,6 +73,9 @@ public:
void forumChanged(Data::Forum *old);
[[nodiscard]] bool isForum() const;
void monoforumChanged(Data::SavedMessages *old);
[[nodiscard]] bool isMonoforum() const;
[[nodiscard]] not_null<History*> migrateToOrMe() const;
[[nodiscard]] History *migrateFrom() const;
[[nodiscard]] MsgRange rangeForDifferenceRequest() const;
@ -430,9 +435,10 @@ private:
PendingAllItemsResize = (1 << 1),
IsTopPromoted = (1 << 2),
IsForum = (1 << 3),
FakeUnreadWhileOpened = (1 << 4),
HasPinnedMessages = (1 << 5),
ResolveChatListMessage = (1 << 6),
IsMonoforum = (1 << 4),
FakeUnreadWhileOpened = (1 << 5),
HasPinnedMessages = (1 << 6),
ResolveChatListMessage = (1 << 7),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) {

View file

@ -3563,6 +3563,12 @@ Data::SavedSublist *HistoryItem::savedSublist() const {
that->AddComponents(HistoryMessageSaved::Bit());
that->Get<HistoryMessageSaved>()->sublist = sublist;
return sublist;
} else if (const auto monoforum = _history->peer->monoforum()) {
const auto sublist = monoforum->sublist(_history->peer);
const auto that = const_cast<HistoryItem*>(this);
that->AddComponents(HistoryMessageSaved::Bit());
that->Get<HistoryMessageSaved>()->sublist = sublist;
return sublist;
}
return nullptr;
}
@ -3785,7 +3791,9 @@ void HistoryItem::createComponents(CreateConfig &&config) {
}
}
const auto peer = _history->owner().peer(config.savedSublistPeer);
saved->sublist = _history->owner().savedMessages().sublist(peer);
saved->sublist = _history->peer->isSelf()
? _history->owner().savedMessages().sublist(peer)
: _history->peer->monoforum()->sublist(peer);
}
if (const auto reply = Get<HistoryMessageReply>()) {
@ -5744,8 +5752,23 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
auto preparePaidMessagesPrice = [&](const MTPDmessageActionPaidMessagesPrice &action) {
const auto stars = action.vstars().v;
const auto broadcastAllowed = action.is_broadcast_messages_allowed();
auto result = PreparedServiceText();
result.text = stars
result.text = _history->peer->isBroadcast()
? (stars > 0
? tr::lng_action_direct_messages_paid(
tr::now,
lt_count,
stars,
Ui::Text::WithEntities)
: broadcastAllowed
? tr::lng_action_direct_messages_enabled(
tr::now,
Ui::Text::WithEntities)
: tr::lng_action_direct_messages_disabled(
tr::now,
Ui::Text::WithEntities))
: stars
? tr::lng_action_message_price_paid(
tr::now,
lt_count,

View file

@ -383,6 +383,7 @@ HistoryWidget::HistoryWidget(
_joinChannel->addClickHandler([=] { joinChannel(); });
_muteUnmute->addClickHandler([=] { toggleMuteUnmute(); });
setupGiftToChannelButton();
setupDirectMessageButton();
_reportMessages->addClickHandler([=] { reportSelectedMessages(); });
_field->submits(
) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) {
@ -1050,15 +1051,23 @@ void HistoryWidget::refreshJoinChannelText() {
}
void HistoryWidget::refreshGiftToChannelShown() {
if (!_giftToChannelIn || !_giftToChannelOut) {
if (!_giftToChannel || !_peer) {
return;
}
const auto channel = _peer->asChannel();
const auto shown = channel
_giftToChannel->setVisible(channel
&& channel->isBroadcast()
&& channel->stargiftsAvailable();
_giftToChannelIn->setVisible(shown);
_giftToChannelOut->setVisible(shown);
&& channel->stargiftsAvailable());
}
void HistoryWidget::refreshDirectMessageShown() {
if (!_directMessage || !_peer) {
return;
}
const auto channel = _peer->asChannel();
_directMessage->setVisible(channel
&& channel->isBroadcast()
&& channel->monoforumLink());
}
void HistoryWidget::refreshTopBarActiveChat() {
@ -2074,22 +2083,63 @@ void HistoryWidget::setupShortcuts() {
}
void HistoryWidget::setupGiftToChannelButton() {
const auto setupButton = [=](not_null<Ui::RpWidget*> parent) {
auto *button = Ui::CreateChild<Ui::IconButton>(
parent.get(),
st::historyGiftToChannel);
parent->widthValue() | rpl::start_with_next([=](int width) {
button->moveToRight(0, 0);
}, button->lifetime());
button->setClickedCallback([=] {
if (_peer) {
Ui::ShowStarGiftBox(controller(), _peer);
_giftToChannel = Ui::CreateChild<Ui::IconButton>(
_muteUnmute.data(),
st::historyGiftToChannel);
widthValue() | rpl::start_with_next([=](int width) {
_giftToChannel->moveToRight(0, 0, width);
}, _giftToChannel->lifetime());
_giftToChannel->setClickedCallback([=] {
Ui::ShowStarGiftBox(controller(), _peer);
});
rpl::combine(
_muteUnmute->shownValue(),
_joinChannel->shownValue()
) | rpl::start_with_next([=](bool muteUnmute, bool joinChannel) {
const auto newParent = (muteUnmute && !joinChannel)
? _muteUnmute.data()
: (joinChannel && !muteUnmute)
? _joinChannel.data()
: nullptr;
if (newParent) {
_giftToChannel->setParent(newParent);
_giftToChannel->moveToRight(0, 0);
refreshGiftToChannelShown();
}
}, _giftToChannel->lifetime());
}
void HistoryWidget::setupDirectMessageButton() {
_directMessage = Ui::CreateChild<Ui::IconButton>(
_muteUnmute.data(),
st::historyDirectMessage);
widthValue() | rpl::start_with_next([=](int width) {
_directMessage->moveToRight(0, 0, width);
}, _directMessage->lifetime());
_directMessage->setClickedCallback([=] {
if (const auto channel = _peer ? _peer->asChannel() : nullptr) {
if (const auto monoforum = channel->monoforumLink()) {
controller()->showPeerHistory(
monoforum,
Window::SectionShow::Way::Forward);
}
});
return button;
};
_giftToChannelIn = setupButton(_muteUnmute);
_giftToChannelOut = setupButton(_joinChannel);
}
});
rpl::combine(
_muteUnmute->shownValue(),
_joinChannel->shownValue()
) | rpl::start_with_next([=](bool muteUnmute, bool joinChannel) {
const auto newParent = (muteUnmute && !joinChannel)
? _muteUnmute.data()
: (joinChannel && !muteUnmute)
? _joinChannel.data()
: nullptr;
if (newParent) {
_directMessage->setParent(newParent);
_directMessage->moveToLeft(0, 0);
refreshDirectMessageShown();
}
}, _directMessage->lifetime());
}
void HistoryWidget::pushReplyReturn(not_null<HistoryItem*> item) {
@ -2456,6 +2506,7 @@ void HistoryWidget::showHistory(
}, _contactStatus->bar().lifetime());
refreshGiftToChannelShown();
refreshDirectMessageShown();
if (const auto user = _peer->asUser()) {
_paysStatus = std::make_unique<PaysStatus>(
controller(),
@ -5220,7 +5271,10 @@ bool HistoryWidget::isBlocked() const {
}
bool HistoryWidget::isJoinChannel() const {
return _peer && _peer->isChannel() && !_peer->asChannel()->amIn();
if (const auto channel = _peer ? _peer->asChannel() : nullptr) {
return !channel->amIn() && !channel->isMonoforum();
}
return false;
}
bool HistoryWidget::isChoosingTheme() const {
@ -8639,6 +8693,7 @@ void HistoryWidget::fullInfoUpdated() {
sendBotStartCommand();
}
refreshGiftToChannelShown();
refreshDirectMessageShown();
}
if (updateCmdStartShown()) {
refresh = true;

View file

@ -406,6 +406,7 @@ private:
void refreshJoinChannelText();
void refreshGiftToChannelShown();
void refreshDirectMessageShown();
void requestMessageData(MsgId msgId);
void messageDataReceived(not_null<PeerData*> peer, MsgId msgId);
@ -535,6 +536,7 @@ private:
void setupShortcuts();
void setupGiftToChannelButton();
void setupDirectMessageButton();
void handlePeerMigration();
@ -797,8 +799,8 @@ private:
object_ptr<Ui::FlatButton> _botStart;
object_ptr<Ui::FlatButton> _joinChannel;
object_ptr<Ui::FlatButton> _muteUnmute;
QPointer<Ui::IconButton> _giftToChannelIn;
QPointer<Ui::IconButton> _giftToChannelOut;
QPointer<Ui::IconButton> _giftToChannel;
QPointer<Ui::IconButton> _directMessage;
object_ptr<Ui::FlatButton> _reportMessages;
struct {
object_ptr<Ui::RoundButton> button = { nullptr };

View file

@ -606,7 +606,7 @@ rpl::producer<Data::MessagesSlice> SublistWidget::listSource(
? (*result.fullCount - after - useBefore)
: std::optional<int>();
if (!result.fullCount || useBefore < limitBefore) {
_sublist->owner().savedMessages().loadMore(_sublist);
_sublist->parent()->loadMore(_sublist);
}
consumer.put_next(std::move(result));
};

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_stories_ids.h"
#include "data/data_user.h"
#include "history/view/history_view_sublist_section.h"
#include "history/history.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "info/profile/info_profile_values.h"
@ -32,39 +33,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Info::Media {
namespace {
[[nodiscard]] Window::SeparateSharedMediaType ToSeparateType(
Storage::SharedMediaType type) {
[[nodiscard]] bool SeparateSupported(Storage::SharedMediaType type) {
using Type = Storage::SharedMediaType;
using SeparatedType = Window::SeparateSharedMediaType;
return (type == Type::Photo)
? SeparatedType::Photos
: (type == Type::Video)
? SeparatedType::Videos
: (type == Type::File)
? SeparatedType::Files
: (type == Type::MusicFile)
? SeparatedType::Audio
: (type == Type::Link)
? SeparatedType::Links
: (type == Type::RoundVoiceFile)
? SeparatedType::Voices
: (type == Type::GIF)
? SeparatedType::GIF
: SeparatedType::None;
|| (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()) {
if (peer->isSelf() || !SeparateSupported(type)) {
return { nullptr };
}
const auto separateType = ToSeparateType(type);
if (separateType == Window::SeparateSharedMediaType::None) {
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
if (topicRootId && !topic) {
return { nullptr };
}
return { Window::SeparateSharedMedia{ separateType, peer, topicRootId } };
const auto thread = topic
? (Data::Thread*)topic
: peer->owner().history(peer);
return { thread, type };
}
void AddContextMenuToButton(

View file

@ -40,6 +40,28 @@ Type TabIndexToType(int index) {
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()
@ -119,25 +141,7 @@ rpl::producer<QString> Widget::title() {
if (controller()->key().peer()->sharedMediaInfo() && isStackBottom()) {
return tr::lng_profile_shared_media();
}
switch (controller()->section().mediaType()) {
case Section::MediaType::Photo:
return tr::lng_media_type_photos();
case Section::MediaType::GIF:
return tr::lng_media_type_gifs();
case Section::MediaType::Video:
return tr::lng_media_type_videos();
case Section::MediaType::MusicFile:
return tr::lng_media_type_songs();
case Section::MediaType::File:
return tr::lng_media_type_files();
case Section::MediaType::RoundVoiceFile:
return tr::lng_media_type_audios();
case Section::MediaType::Link:
return tr::lng_media_type_links();
case Section::MediaType::RoundFile:
return tr::lng_media_type_rounds();
}
Unexpected("Bad media type in Info::TitleValue()");
return SharedMediaTitle(controller()->section().mediaType())();
}
void Widget::setIsStackBottom(bool isStackBottom) {

View file

@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#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
@ -19,8 +24,9 @@ namespace Info::Media {
using Type = Storage::SharedMediaType;
std::optional<int> TypeToTabIndex(Type type);
Type TabIndexToType(int index);
[[nodiscard]] std::optional<int> TypeToTabIndex(Type type);
[[nodiscard]] Type TabIndexToType(int index);
[[nodiscard]] tr::phrase<> SharedMediaTitle(Type type);
class InnerWidget;

View file

@ -57,7 +57,7 @@ SublistsWidget::SublistsWidget(
this,
controller->parentController(),
rpl::single(Dialogs::InnerWidget::ChildListShown())));
_list->showSavedSublists();
_list->showSavedSublists(nullptr);
_list->setNarrowRatio(0.);
_list->chosenRow() | rpl::start_with_next([=](Dialogs::ChosenRow row) {

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/platform/ui_platform_window.h"
#include "platform/platform_window_title.h"
#include "history/history.h"
#include "info/media/info_media_widget.h" // SharedMediaTitle.
#include "window/window_separate_id.h"
#include "window/window_session_controller.h"
#include "window/window_lock_widgets.h"
@ -87,42 +88,24 @@ base::options::toggle OptionDisableTouchbar({
.restartRequired = true,
});
[[nodiscard]] QString TitleFromSeparateId(
[[nodiscard]] QString TitleFromSeparateSharedMedia(
const Core::WindowTitleContent &settings,
const SeparateId &id) {
if (id.sharedMedia == SeparateSharedMediaType::None
|| !id.sharedMediaPeer()) {
if (id.type != SeparateType::SharedMedia) {
return QString();
}
const auto result = (id.sharedMedia == SeparateSharedMediaType::Photos)
? tr::lng_media_type_photos(tr::now)
: (id.sharedMedia == SeparateSharedMediaType::Videos)
? tr::lng_media_type_videos(tr::now)
: (id.sharedMedia == SeparateSharedMediaType::Files)
? tr::lng_media_type_files(tr::now)
: (id.sharedMedia == SeparateSharedMediaType::Audio)
? tr::lng_media_type_songs(tr::now)
: (id.sharedMedia == SeparateSharedMediaType::Links)
? tr::lng_media_type_links(tr::now)
: (id.sharedMedia == SeparateSharedMediaType::GIF)
? tr::lng_media_type_gifs(tr::now)
: (id.sharedMedia == SeparateSharedMediaType::Voices)
? tr::lng_media_type_audios(tr::now)
: QString();
const auto type = id.sharedMediaType;
const auto result = Info::Media::SharedMediaTitle(type)(tr::now);
if (settings.hideChatName) {
return result;
}
const auto peer = id.sharedMediaPeer();
const auto topicRootId = id.sharedMediaTopicRootId();
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
const auto thread = id.thread;
const auto topic = thread->asTopic();
const auto name = topic
? topic->title()
: peer->isSelf()
: thread->peer()->isSelf()
? tr::lng_saved_messages(tr::now)
: peer->name();
: thread->peer()->name();
const auto wrapped = st::wrap_rtl(name);
return name + u" @ "_q + result;
}
@ -902,11 +885,11 @@ void MainWindow::updateTitle() {
&& Core::App().domain().accountsAuthedCount() > 1)
? st::wrap_rtl(session->authedName())
: QString();
const auto separateIdTitle = session
? TitleFromSeparateId(settings, session->windowId())
const auto separateSharedMediaTitle = session
? TitleFromSeparateSharedMedia(settings, session->windowId())
: QString();
if (!separateIdTitle.isEmpty()) {
setTitle(separateIdTitle);
if (!separateSharedMediaTitle.isEmpty()) {
setTitle(separateSharedMediaTitle);
return;
}
const auto key = (session && !settings.hideChatName)

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "window/window_separate_id.h"
#include "data/data_channel.h"
#include "data/data_folder.h"
#include "data/data_peer.h"
#include "data/data_saved_messages.h"
@ -30,10 +31,14 @@ SeparateId::SeparateId(SeparateType type, not_null<Main::Session*> session)
, account(&session->account()) {
}
SeparateId::SeparateId(SeparateType type, not_null<Data::Thread*> thread)
SeparateId::SeparateId(
SeparateType type,
not_null<Data::Thread*> thread,
ChannelData *parentChat)
: type(type)
, account(&thread->session().account())
, thread(thread) {
, thread(thread)
, parentChat((type == SeparateType::SavedSublist) ? parentChat : nullptr) {
}
SeparateId::SeparateId(not_null<Data::Thread*> thread)
@ -44,12 +49,13 @@ SeparateId::SeparateId(not_null<PeerData*> peer)
: SeparateId(SeparateType::Chat, peer->owner().history(peer)) {
}
SeparateId::SeparateId(SeparateSharedMedia data)
SeparateId::SeparateId(
not_null<Data::Thread*> thread,
Storage::SharedMediaType sharedMediaType)
: type(SeparateType::SharedMedia)
, sharedMedia(data.type)
, account(&data.peer->session().account())
, sharedMediaDataPeer(data.peer)
, sharedMediaDataTopicRootId(data.topicRootId) {
, sharedMediaType(sharedMediaType)
, account(&thread->session().account())
, thread(thread) {
}
bool SeparateId::primary() const {
@ -71,9 +77,12 @@ Data::Folder *SeparateId::folder() const {
}
Data::SavedSublist *SeparateId::sublist() const {
return (type == SeparateType::SavedSublist)
? thread->owner().savedMessages().sublist(thread->peer()).get()
: nullptr;
const auto monoforum = parentChat ? parentChat->monoforum() : nullptr;
return (type != SeparateType::SavedSublist)
? nullptr
: monoforum
? monoforum->sublist(thread->peer()).get()
: thread->owner().savedMessages().sublist(thread->peer()).get();
}
bool SeparateId::hasChatsList() const {
@ -82,16 +91,4 @@ bool SeparateId::hasChatsList() const {
|| (type == SeparateType::Forum);
}
PeerData *SeparateId::sharedMediaPeer() const {
return (type == SeparateType::SharedMedia)
? sharedMediaDataPeer
: nullptr;
}
MsgId SeparateId::sharedMediaTopicRootId() const {
return (type == SeparateType::SharedMedia)
? sharedMediaDataTopicRootId
: MsgId();
}
} // namespace Window

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class ChannelData;
class PeerData;
namespace Data {
@ -21,6 +22,10 @@ class Account;
class Session;
} // namespace Main
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Window {
enum class SeparateType {
@ -32,39 +37,30 @@ enum class SeparateType {
SharedMedia,
};
enum class SeparateSharedMediaType {
None,
Photos,
Videos,
Files,
Audio,
Links,
Voices,
GIF,
};
struct SeparateSharedMedia {
SeparateSharedMediaType type = SeparateSharedMediaType::None;
not_null<PeerData*> peer;
MsgId topicRootId = MsgId();
not_null<Data::Thread*> thread;
Storage::SharedMediaType type = {};
};
struct SeparateId {
SeparateId(std::nullptr_t);
SeparateId(not_null<Main::Account*> account);
SeparateId(SeparateType type, not_null<Main::Session*> session);
SeparateId(SeparateType type, not_null<Data::Thread*> thread);
SeparateId(
SeparateType type,
not_null<Data::Thread*> thread,
ChannelData *parentChat = nullptr);
SeparateId(not_null<Data::Thread*> thread);
SeparateId(not_null<PeerData*> peer);
SeparateId(SeparateSharedMedia data);
SeparateId(
not_null<Data::Thread*> thread,
Storage::SharedMediaType sharedMediaType);
SeparateType type = SeparateType::Primary;
SeparateSharedMediaType sharedMedia = SeparateSharedMediaType::None;
Storage::SharedMediaType sharedMediaType = {};
Main::Account *account = nullptr;
Data::Thread *thread = nullptr; // For types except Main and Archive.
PeerData *sharedMediaDataPeer = nullptr;
MsgId sharedMediaDataTopicRootId = MsgId();
ChannelData *parentChat = nullptr;
[[nodiscard]] bool valid() const {
return account != nullptr;
}
@ -77,8 +73,6 @@ struct SeparateId {
[[nodiscard]] Data::Forum *forum() const;
[[nodiscard]] Data::Folder *folder() const;
[[nodiscard]] Data::SavedSublist *sublist() const;
[[nodiscard]] PeerData *sharedMediaPeer() const;
[[nodiscard]] MsgId sharedMediaTopicRootId() const;
[[nodiscard]] bool hasChatsList() const;

View file

@ -1321,35 +1321,13 @@ void SessionNavigation::showByInitialId(
showThread(id.thread, msgId, instant);
break;
case SeparateType::SharedMedia: {
Assert(id.sharedMedia != SeparateSharedMediaType::None);
clearSectionStack(instant);
const auto type = (id.sharedMedia == SeparateSharedMediaType::Photos)
? Storage::SharedMediaType::Photo
: (id.sharedMedia == SeparateSharedMediaType::Videos)
? Storage::SharedMediaType::Video
: (id.sharedMedia == SeparateSharedMediaType::Files)
? Storage::SharedMediaType::File
: (id.sharedMedia == SeparateSharedMediaType::Audio)
? Storage::SharedMediaType::MusicFile
: (id.sharedMedia == SeparateSharedMediaType::Links)
? Storage::SharedMediaType::Link
: (id.sharedMedia == SeparateSharedMediaType::Voices)
? Storage::SharedMediaType::RoundVoiceFile
: (id.sharedMedia == SeparateSharedMediaType::GIF)
? Storage::SharedMediaType::GIF
: Storage::SharedMediaType::Photo;
const auto topicRootId = id.sharedMediaTopicRootId();
const auto peer = id.sharedMediaPeer();
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
if (topicRootId && !topic) {
break;
}
const auto type = id.sharedMediaType;
const auto topic = id.thread->asTopic();
showSection(
topicRootId
(topic
? std::make_shared<Info::Memento>(topic, type)
: std::make_shared<Info::Memento>(peer, type),
: std::make_shared<Info::Memento>(id.thread->peer(), type)),
instant);
parent->widget()->setMaximumWidth(st::maxWidthSharedMediaWindow);
break;