Allow sending photo/video captions above media.

This commit is contained in:
John Preston 2024-05-29 21:27:07 +04:00
parent 924d80ecba
commit 67f7816088
15 changed files with 182 additions and 46 deletions

View file

@ -251,6 +251,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_caption_limit2#other" = "Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** characters."; "lng_caption_limit2#other" = "Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** characters.";
"lng_caption_limit_reached#one" = "You've reached the media caption limit. Please make the caption shorter by {count} character."; "lng_caption_limit_reached#one" = "You've reached the media caption limit. Please make the caption shorter by {count} character.";
"lng_caption_limit_reached#other" = "You've reached the media caption limit. Please make the caption shorter by {count} characters."; "lng_caption_limit_reached#other" = "You've reached the media caption limit. Please make the caption shorter by {count} characters.";
"lng_caption_move_up" = "Move Caption Up";
"lng_caption_move_down" = "Move Caption Down";
"lng_file_size_limit_title" = "File Too Large"; "lng_file_size_limit_title" = "File Too Large";
"lng_file_size_limit#one" = "{count} Gb"; "lng_file_size_limit#one" = "{count} Gb";

View file

@ -26,6 +26,7 @@ struct SendOptions {
EffectId effectId = 0; EffectId effectId = 0;
bool silent = false; bool silent = false;
bool handleSupportSwitch = false; bool handleSupportSwitch = false;
bool invertCaption = false;
bool hideViaBot = false; bool hideViaBot = false;
crl::time ttlSeconds = 0; crl::time ttlSeconds = 0;
}; };

View file

@ -81,7 +81,8 @@ mtpRequestId EditMessage(
| ((!webpage.removed && !webpage.url.isEmpty()) | ((!webpage.removed && !webpage.url.isEmpty())
? MTPmessages_EditMessage::Flag::f_media ? MTPmessages_EditMessage::Flag::f_media
: emptyFlag) : emptyFlag)
| ((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
|| options.invertCaption)
? MTPmessages_EditMessage::Flag::f_invert_media ? MTPmessages_EditMessage::Flag::f_invert_media
: emptyFlag) : emptyFlag)
| (!sentEntities.v.isEmpty() | (!sentEntities.v.isEmpty()

View file

@ -136,6 +136,10 @@ void SendExistingMedia(
if (action.options.effectId) { if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect; sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
} }
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
session->data().registerMessageRandomId(randomId, newId); session->data().registerMessageRandomId(randomId, newId);
@ -314,6 +318,10 @@ bool SendDice(MessageToSend &message) {
if (action.options.effectId) { if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect; sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
} }
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
session->data().registerMessageRandomId(randomId, newId); session->data().registerMessageRandomId(randomId, newId);
@ -440,6 +448,9 @@ void SendConfirmedFile(
flags |= MessageFlag::MediaIsUnread; flags |= MessageFlag::MediaIsUnread;
} }
} }
if (file->to.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
}
const auto messageFromId = file->to.options.sendAs const auto messageFromId = file->to.options.sendAs
? file->to.options.sendAs->id ? file->to.options.sendAs->id

View file

@ -3772,7 +3772,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
const auto anonymousPost = peer->amAnonymous(); const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options); const auto silentPost = ShouldSendSilent(peer, action.options);
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);
if (exactWebPage && !ignoreWebPage && message.webPage.invert) { if ((exactWebPage && !ignoreWebPage && message.webPage.invert)
|| action.options.invertCaption) {
flags |= MessageFlag::InvertMedia; flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMessage::Flag::f_invert_media; sendFlags |= MTPmessages_SendMessage::Flag::f_invert_media;
mediaFlags |= MTPmessages_SendMedia::Flag::f_invert_media; mediaFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
@ -4170,7 +4171,8 @@ void ApiWrap::sendMediaWithRandomId(
| (options.scheduled ? Flag::f_schedule_date : Flag(0)) | (options.scheduled ? Flag::f_schedule_date : Flag(0))
| (options.sendAs ? Flag::f_send_as : Flag(0)) | (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0)); | (options.effectId ? Flag::f_effect : Flag(0))
| (options.invertCaption ? Flag::f_invert_media : Flag(0));
auto &histories = history->owner().histories(); auto &histories = history->owner().histories();
const auto peer = history->peer; const auto peer = history->peer;
@ -4280,7 +4282,8 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
| (album->options.shortcutId | (album->options.shortcutId
? Flag::f_quick_reply_shortcut ? Flag::f_quick_reply_shortcut
: Flag(0)) : Flag(0))
| (album->options.effectId ? Flag::f_effect : Flag(0)); | (album->options.effectId ? Flag::f_effect : Flag(0))
| (album->options.invertCaption ? Flag::f_invert_media : Flag(0));
auto &histories = history->owner().histories(); auto &histories = history->owner().histories();
const auto peer = history->peer; const auto peer = history->peer;
histories.sendPreparedMessage( histories.sendPreparedMessage(

View file

@ -350,9 +350,8 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)
, _titleHeight(st::boxTitleHeight) , _titleHeight(st::boxTitleHeight)
, _list(std::move(descriptor.list)) , _list(std::move(descriptor.list))
, _limits(descriptor.limits) , _limits(descriptor.limits)
, _sendMenuDetails(descriptor.sendMenuDetails , _sendMenuDetails(prepareSendMenuDetails(descriptor))
? descriptor.sendMenuDetails , _sendMenuCallback(prepareSendMenuCallback())
: [] { return SendMenu::Details(); })
, _captionToPeer(descriptor.captionToPeer) , _captionToPeer(descriptor.captionToPeer)
, _check(std::move(descriptor.check)) , _check(std::move(descriptor.check))
, _confirmedCallback(std::move(descriptor.confirmed)) , _confirmedCallback(std::move(descriptor.confirmed))
@ -366,6 +365,50 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)
enqueueNextPrepare(); enqueueNextPrepare();
} }
Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor) {
auto initial = descriptor.sendMenuDetails;
return crl::guard(this, [=] {
auto result = initial ? initial() : SendMenu::Details();
result.spoiler = !hasSpoilerMenu()
? SendMenu::SpoilerState::None
: allWithSpoilers()
? SendMenu::SpoilerState::Enabled
: SendMenu::SpoilerState::Possible;
const auto way = _sendWay.current();
const auto canMoveCaption = _list.canMoveCaption(
way.groupFiles() && way.sendImagesAsPhotos(),
way.sendImagesAsPhotos()
) && _caption && !_caption->getLastText().isEmpty();
result.caption = !canMoveCaption
? SendMenu::CaptionState::None
: _invertCaption
? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below;
return result;
});
}
auto SendFilesBox::prepareSendMenuCallback()
-> Fn<void(MenuAction, MenuDetails)> {
return crl::guard(this, [=](MenuAction action, MenuDetails details) {
using Type = SendMenu::ActionType;
switch (action.type) {
case Type::CaptionDown: _invertCaption = false; break;
case Type::CaptionUp: _invertCaption = true; break;
case Type::SpoilerOn: toggleSpoilers(true); break;
case Type::SpoilerOff: toggleSpoilers(false); break;
default:
SendMenu::DefaultCallback(
_show,
sendCallback())(
action,
details);
break;
}
});
}
void SendFilesBox::initPreview() { void SendFilesBox::initPreview() {
using namespace rpl::mappers; using namespace rpl::mappers;
@ -533,7 +576,7 @@ void SendFilesBox::refreshButtons() {
_send, _send,
_show, _show,
_sendMenuDetails, _sendMenuDetails,
SendMenu::DefaultCallback(_show, sendCallback())); _sendMenuCallback);
} }
addButton(tr::lng_cancel(), [=] { closeBox(); }); addButton(tr::lng_cancel(), [=] { closeBox(); });
_addFile = addLeftButton( _addFile = addLeftButton(
@ -545,8 +588,10 @@ void SendFilesBox::refreshButtons() {
addMenuButton(); addMenuButton();
} }
bool SendFilesBox::hasSendMenu() const { bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const {
return (_sendMenuDetails().type != SendMenu::Type::Disabled); return (details.type != SendMenu::Type::Disabled)
|| (details.spoiler != SendMenu::SpoilerState::None)
|| (details.caption != SendMenu::CaptionState::None);
} }
bool SendFilesBox::hasSpoilerMenu() const { bool SendFilesBox::hasSpoilerMenu() const {
@ -583,7 +628,8 @@ void SendFilesBox::toggleSpoilers(bool enabled) {
} }
void SendFilesBox::addMenuButton() { void SendFilesBox::addMenuButton() {
if (!hasSendMenu() && !hasSpoilerMenu()) { const auto details = _sendMenuDetails();
if (!hasSendMenu(details)) {
return; return;
} }
@ -592,31 +638,16 @@ void SendFilesBox::addMenuButton() {
const auto &tabbed = _st.tabbed; const auto &tabbed = _st.tabbed;
const auto &icons = tabbed.icons; const auto &icons = tabbed.icons;
_menu = base::make_unique_q<Ui::PopupMenu>(top, tabbed.menu); _menu = base::make_unique_q<Ui::PopupMenu>(top, tabbed.menu);
if (hasSpoilerMenu()) { SendMenu::FillSendMenu(
const auto spoilered = allWithSpoilers(); _menu.get(),
_menu->addAction( _show,
(spoilered _sendMenuDetails(),
? tr::lng_context_disable_spoiler(tr::now) _sendMenuCallback,
: tr::lng_context_spoiler_effect(tr::now)), &_st.tabbed.icons,
[=] { toggleSpoilers(!spoilered); }, QCursor::pos());
spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler);
if (hasSendMenu()) {
_menu->addSeparator(&tabbed.expandedSeparator);
}
}
if (hasSendMenu()) {
SendMenu::FillSendMenu(
_menu.get(),
_show,
_sendMenuDetails(),
SendMenu::DefaultCallback(_show, sendCallback()),
&_st.tabbed.icons,
QCursor::pos());
}
_menu->popup(QCursor::pos()); _menu->popup(QCursor::pos());
return true; return true;
}); });
} }
void SendFilesBox::initSendWay() { void SendFilesBox::initSendWay() {
@ -658,9 +689,7 @@ void SendFilesBox::initSendWay() {
for (auto &block : _blocks) { for (auto &block : _blocks) {
block.setSendWay(value); block.setSendWay(value);
} }
if (!hasSendMenu()) { refreshButtons();
refreshButtons();
}
if (was != hidden()) { if (was != hidden()) {
updateBoxSize(); updateBoxSize();
updateControlsGeometry(); updateControlsGeometry();
@ -872,9 +901,7 @@ void SendFilesBox::pushBlock(int from, int till) {
} }
void SendFilesBox::refreshControls(bool initial) { void SendFilesBox::refreshControls(bool initial) {
if (initial || !hasSendMenu()) { refreshButtons();
refreshButtons();
}
refreshTitleText(); refreshTitleText();
updateSendWayControls(); updateSendWayControls();
updateCaptionPlaceholder(); updateCaptionPlaceholder();
@ -1426,9 +1453,12 @@ void SendFilesBox::send(
if ((_sendType == Api::SendType::Scheduled if ((_sendType == Api::SendType::Scheduled
|| _sendType == Api::SendType::ScheduledToUser) || _sendType == Api::SendType::ScheduledToUser)
&& !options.scheduled) { && !options.scheduled) {
auto child = _sendMenuDetails();
child.spoiler = SendMenu::SpoilerState::None;
child.caption = SendMenu::CaptionState::None;
return SendMenu::DefaultCallback(_show, sendCallback())( return SendMenu::DefaultCallback(_show, sendCallback())(
{ .type = SendMenu::ActionType::Schedule }, { .type = SendMenu::ActionType::Schedule },
_sendMenuDetails()); child);
} }
if (_preparing) { if (_preparing) {
_whenReadySend = [=] { _whenReadySend = [=] {
@ -1453,6 +1483,7 @@ void SendFilesBox::send(
auto caption = (_caption && !_caption->isHidden()) auto caption = (_caption && !_caption->isHidden())
? _caption->getTextWithAppliedMarkdown() ? _caption->getTextWithAppliedMarkdown()
: TextWithTags(); : TextWithTags();
options.invertCaption = _invertCaption;
if (!validateLength(caption.text)) { if (!validateLength(caption.text)) {
return; return;
} }

View file

@ -48,6 +48,7 @@ class SessionController;
namespace SendMenu { namespace SendMenu {
struct Details; struct Details;
struct Action;
} // namespace SendMenu } // namespace SendMenu
namespace HistoryView::Controls { namespace HistoryView::Controls {
@ -136,6 +137,9 @@ protected:
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;
private: private:
using MenuAction = SendMenu::Action;
using MenuDetails = SendMenu::Details;
class Block final { class Block final {
public: public:
Block( Block(
@ -173,7 +177,7 @@ private:
void initSendWay(); void initSendWay();
void initPreview(); void initPreview();
[[nodiscard]] bool hasSendMenu() const; [[nodiscard]] bool hasSendMenu(const MenuDetails &details) const;
[[nodiscard]] bool hasSpoilerMenu() const; [[nodiscard]] bool hasSpoilerMenu() const;
[[nodiscard]] bool allWithSpoilers(); [[nodiscard]] bool allWithSpoilers();
[[nodiscard]] bool checkWithWay( [[nodiscard]] bool checkWithWay(
@ -225,6 +229,11 @@ private:
void checkCharsLimitation(); void checkCharsLimitation();
[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor);
[[nodiscard]] auto prepareSendMenuCallback()
-> Fn<void(MenuAction, MenuDetails)>;
const std::shared_ptr<ChatHelpers::Show> _show; const std::shared_ptr<ChatHelpers::Show> _show;
const style::ComposeControls &_st; const style::ComposeControls &_st;
const Api::SendType _sendType = Api::SendType(); const Api::SendType _sendType = Api::SendType();
@ -236,12 +245,14 @@ private:
std::optional<int> _removingIndex; std::optional<int> _removingIndex;
SendFilesLimits _limits = {}; SendFilesLimits _limits = {};
Fn<SendMenu::Details()> _sendMenuDetails = nullptr; Fn<MenuDetails()> _sendMenuDetails;
Fn<void(MenuAction, MenuDetails)> _sendMenuCallback;
PeerData *_captionToPeer = nullptr; PeerData *_captionToPeer = nullptr;
SendFilesCheck _check; SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback; SendFilesConfirmed _confirmedCallback;
Fn<void()> _cancelledCallback; Fn<void()> _cancelledCallback;
bool _confirmed = false; bool _confirmed = false;
bool _invertCaption = false;
object_ptr<Ui::InputField> _caption = { nullptr }; object_ptr<Ui::InputField> _caption = { nullptr };
TextWithTags _prefilledCaptionText; TextWithTags _prefilledCaptionText;

View file

@ -69,6 +69,8 @@ ComposeIcons {
menuWhenOnline: icon; menuWhenOnline: icon;
menuSpoiler: icon; menuSpoiler: icon;
menuSpoilerOff: icon; menuSpoilerOff: icon;
menuBelow: icon;
menuAbove: icon;
stripBubble: icon; stripBubble: icon;
stripExpandPanel: icon; stripExpandPanel: icon;
@ -606,6 +608,8 @@ defaultComposeIcons: ComposeIcons {
menuWhenOnline: menuIconWhenOnline; menuWhenOnline: menuIconWhenOnline;
menuSpoiler: menuIconSpoiler; menuSpoiler: menuIconSpoiler;
menuSpoilerOff: menuIconSpoilerOff; menuSpoilerOff: menuIconSpoilerOff;
menuBelow: menuIconBelow;
menuAbove: menuIconAbove;
stripBubble: icon{ stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg }, { "chat/reactions_bubble_shadow", windowShadowFg },

View file

@ -1104,6 +1104,7 @@ void Reactions::defaultUpdated() {
} }
refreshMyTags(); refreshMyTags();
refreshTags(); refreshTags();
refreshEffects();
_defaultUpdated.fire({}); _defaultUpdated.fire({});
} }

View file

@ -141,6 +141,9 @@ namespace Media::Stories {
if (options.effectId) { if (options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect; sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
} }
if (options.invertCaption) {
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
const auto done = [=] { const auto done = [=] {
if (!--state->requests) { if (!--state->requests) {
if (show->valid()) { if (show->valid()) {

View file

@ -636,6 +636,8 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) {
menuWhenOnline: icon {{ "menu/send_when_online", storiesComposeWhiteText }}; menuWhenOnline: icon {{ "menu/send_when_online", storiesComposeWhiteText }};
menuSpoiler: icon {{ "menu/spoiler_on", storiesComposeWhiteText }}; menuSpoiler: icon {{ "menu/spoiler_on", storiesComposeWhiteText }};
menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }}; menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }};
menuBelow: icon {{ "menu/link_below", storiesComposeWhiteText }};
menuAbove: icon {{ "menu/link_above", storiesComposeWhiteText }};
stripBubble: icon{ stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg }, { "chat/reactions_bubble_shadow", windowShadowFg },

View file

@ -615,13 +615,47 @@ FillMenuResult FillSendMenu(
const style::ComposeIcons *iconsOverride, const style::ComposeIcons *iconsOverride,
std::optional<QPoint> desiredPositionOverride) { std::optional<QPoint> desiredPositionOverride) {
const auto type = details.type; const auto type = details.type;
if (type == Type::Disabled || !action) { const auto empty = (type == Type::Disabled)
&& (details.spoiler == SpoilerState::None)
&& (details.caption == CaptionState::None);
if (empty || !action) {
return FillMenuResult::Skipped; return FillMenuResult::Skipped;
} }
const auto &icons = iconsOverride const auto &icons = iconsOverride
? *iconsOverride ? *iconsOverride
: st::defaultComposeIcons; : st::defaultComposeIcons;
auto toggles = false;
if (details.spoiler != SpoilerState::None) {
const auto spoilered = (details.spoiler == SpoilerState::Enabled);
menu->addAction(
(spoilered
? tr::lng_context_disable_spoiler(tr::now)
: tr::lng_context_spoiler_effect(tr::now)),
[=] { action({ .type = spoilered
? ActionType::SpoilerOff
: ActionType::SpoilerOn
}, details); },
spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler);
toggles = true;
}
if (details.caption != CaptionState::None) {
const auto above = (details.caption == CaptionState::Above);
menu->addAction(
(above
? tr::lng_caption_move_down(tr::now)
: tr::lng_caption_move_up(tr::now)),
[=] { action({ .type = above
? ActionType::CaptionDown
: ActionType::CaptionUp
}, details); },
above ? &icons.menuBelow : &icons.menuAbove);
toggles = true;
}
if (toggles && type != Type::Disabled) {
menu->addSeparator();
}
if (type != Type::Reminder) { if (type != Type::Reminder) {
menu->addAction( menu->addAction(
tr::lng_send_silent_message(tr::now), tr::lng_send_silent_message(tr::now),

View file

@ -29,7 +29,7 @@ class Thread;
namespace SendMenu { namespace SendMenu {
enum class Type { enum class Type : uchar {
Disabled, Disabled,
SilentOnly, SilentOnly,
Scheduled, Scheduled,
@ -37,20 +37,38 @@ enum class Type {
Reminder, Reminder,
}; };
enum class SpoilerState : uchar {
None,
Enabled,
Possible,
};
enum class CaptionState : uchar {
None,
Below,
Above,
};
struct Details { struct Details {
Type type = Type::Disabled; Type type = Type::Disabled;
SpoilerState spoiler = SpoilerState::None;
CaptionState caption = CaptionState::None;
bool effectAllowed = false; bool effectAllowed = false;
}; };
enum class FillMenuResult { enum class FillMenuResult : uchar {
Prepared, Prepared,
Skipped, Skipped,
Failed, Failed,
}; };
enum class ActionType { enum class ActionType : uchar {
Send, Send,
Schedule, Schedule,
SpoilerOn,
SpoilerOff,
CaptionUp,
CaptionDown,
}; };
struct Action { struct Action {
using Type = ActionType; using Type = ActionType;

View file

@ -184,6 +184,17 @@ bool PreparedList::canAddCaption(bool sendingAlbum, bool compress) const {
return !hasFiles && !hasMusic && !hasNotGrouped; return !hasFiles && !hasMusic && !hasNotGrouped;
} }
bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const {
if (!canAddCaption(sendingAlbum, compress)) {
return false;
} else if (files.size() != 1) {
return true;
}
const auto &file = files.front();
return (file.type == PreparedFile::Type::Video)
|| (file.type == PreparedFile::Type::Photo && compress);
}
bool PreparedList::hasGroupOption(bool slowmode) const { bool PreparedList::hasGroupOption(bool slowmode) const {
if (slowmode || files.size() < 2) { if (slowmode || files.size() < 2) {
return false; return false;

View file

@ -112,6 +112,9 @@ struct PreparedList {
void mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false); void mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false);
[[nodiscard]] bool canAddCaption(bool sendingAlbum, bool compress) const; [[nodiscard]] bool canAddCaption(bool sendingAlbum, bool compress) const;
[[nodiscard]] bool canMoveCaption(
bool sendingAlbum,
bool compress) const;
[[nodiscard]] bool canBeSentInSlowmode() const; [[nodiscard]] bool canBeSentInSlowmode() const;
[[nodiscard]] bool canBeSentInSlowmodeWith( [[nodiscard]] bool canBeSentInSlowmodeWith(
const PreparedList &other) const; const PreparedList &other) const;