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

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

View File

@@ -0,0 +1,107 @@
/*
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 "api/api_common.h"
namespace Api {
enum class SendProgressType;
struct SendAction;
} // namespace Api
namespace Data {
class GroupCall;
} // namespace Data
class History;
namespace HistoryView::Controls {
struct MessageToEdit {
FullMsgId fullId;
Api::SendOptions options;
TextWithTags textWithTags;
bool spoilered = false;
};
struct VoiceToSend {
QByteArray bytes;
VoiceWaveform waveform;
crl::time duration = 0;
Api::SendOptions options;
bool video = false;
};
struct SendActionUpdate {
Api::SendProgressType type = Api::SendProgressType();
int progress = 0;
bool cancel = false;
};
enum class WriteRestrictionType {
None,
Rights,
PremiumRequired,
Frozen,
Hidden,
};
struct WriteRestriction {
using Type = WriteRestrictionType;
QString text;
QString button;
Type type = Type::None;
int boostsToLift = false;
[[nodiscard]] bool empty() const {
return (type == Type::None);
}
explicit operator bool() const {
return !empty();
}
friend inline bool operator==(
const WriteRestriction &a,
const WriteRestriction &b) = default;
};
struct SetHistoryArgs {
required<History*> history;
std::shared_ptr<Data::GroupCall> videoStream;
MsgId topicRootId = 0;
PeerId monoforumPeerId = 0;
Fn<bool()> showSlowmodeError;
Fn<Api::SendAction()> sendActionFactory;
rpl::producer<int> slowmodeSecondsLeft;
rpl::producer<bool> sendDisabledBySlowmode;
rpl::producer<bool> liked;
rpl::producer<int> minStarsCount;
rpl::producer<WriteRestriction> writeRestriction;
};
struct ReplyNextRequest {
enum class Direction {
Next,
Previous,
};
const FullMsgId replyId;
const Direction direction;
};
enum class ToggleCommentsState {
Empty,
Shown,
Hidden,
WithNew,
};
struct SendStarButtonEffect {
not_null<PeerData*> from;
int stars = 0;
};
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,71 @@
/*
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 "history/view/controls/history_view_characters_limit.h"
#include "ui/rect.h"
#include "styles/style_chat_helpers.h"
namespace {
constexpr auto kMinus = QChar(0x2212);
constexpr auto kLimit = int(999);
[[nodiscard]] int CountDigits(int n) {
return n == 0 ? 1 : static_cast<int>(std::log10(std::abs(n))) + 1;
}
} // namespace
namespace HistoryView::Controls {
CharactersLimitLabel::CharactersLimitLabel(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> widgetToAlign,
style::align align,
QMargins margins)
: Ui::FlatLabel(parent, st::historyCharsLimitationLabel)
, _widgetToAlign(widgetToAlign)
, _position((align == style::al_top)
? Fn<void(int, const QRect &)>([=](int height, const QRect &g) {
const auto w = textMaxWidth();
move(
g.x() + (g.width() - w) / 2 + margins.left(),
rect::bottom(g) + margins.top());
})
: Fn<void(int, const QRect &)>([=](int height, const QRect &g) {
const auto w = textMaxWidth();
move(
g.x() + (g.width() - w) / 2 + margins.left(),
g.y() - height - margins.bottom());
})) {
Expects((align == style::al_top) || align == style::al_bottom);
rpl::combine(
Ui::RpWidget::heightValue(),
widgetToAlign->geometryValue()
) | rpl::on_next(_position, lifetime());
}
void CharactersLimitLabel::setLeft(int value) {
const auto orderChanged = (CountDigits(value) != CountDigits(_lastValue));
_lastValue = value;
if (value > 0) {
setTextColorOverride(st::historyCharsLimitationLabel.textFg->c);
Ui::FlatLabel::setText(kMinus
+ QString::number(std::min(value, kLimit)));
} else {
setTextColorOverride(st::windowSubTextFg->c);
Ui::FlatLabel::setText(QString::number(-value));
}
if (orderChanged) {
_position(height(), _widgetToAlign->geometry());
}
}
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,31 @@
/*
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/widgets/labels.h"
namespace HistoryView::Controls {
class CharactersLimitLabel final : public Ui::FlatLabel {
public:
CharactersLimitLabel(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> widgetToAlign,
style::align align,
QMargins margins = {});
void setLeft(int value);
private:
int _lastValue = 0;
not_null<Ui::RpWidget*> _widgetToAlign;
Fn<void(int, const QRect &)> _position;
};
} // namespace HistoryView::Controls

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,534 @@
/*
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 "api/api_common.h"
#include "base/required.h"
#include "base/unique_qptr.h"
#include "base/timer.h"
#include "chat_helpers/compose/compose_features.h"
#include "dialogs/dialogs_key.h"
#include "history/view/controls/compose_controls_common.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "ui/widgets/fields/input_field.h"
class History;
class DocumentData;
class Image;
namespace style {
struct ComposeControls;
} // namespace style
namespace SendMenu {
struct Details;
} // namespace SendMenu
namespace ChatHelpers {
class TabbedPanel;
class TabbedSelector;
struct FileChosen;
struct PhotoChosen;
class Show;
enum class PauseReason;
class FieldAutocomplete;
} // namespace ChatHelpers
namespace Data {
struct MessagePosition;
struct Draft;
class DraftKey;
class PhotoMedia;
class GroupCall;
struct WebPageDraft;
struct MessageReactionsTopPaid;
} // namespace Data
namespace InlineBots {
namespace Layout {
class ItemBase;
class Widget;
} // namespace Layout
class Result;
struct ResultSelected;
} // namespace InlineBots
namespace Ui {
class AbstractButton;
class SendButton;
class IconButton;
class EmojiButton;
class SendAsButton;
class SilentToggle;
class DropdownMenu;
struct PreparedList;
struct SendStarButtonState;
class ReactionFlyAnimation;
} // namespace Ui
namespace Ui::Emoji {
class SuggestionsController;
} // namespace Ui::Emoji
namespace Main {
class Session;
struct SendAsKey;
} // namespace Main
namespace Webrtc {
enum class RecordAvailability : uchar;
} // namespace Webrtc
namespace Window {
struct SectionShow;
class SessionController;
} // namespace Window
namespace Api {
enum class SendProgressType;
} // namespace Api
namespace HistoryView::Controls {
class VoiceRecordBar;
class TTLButton;
class WebpageProcessor;
class CharactersLimitLabel;
} // namespace HistoryView::Controls
namespace HistoryView {
class FieldHeader;
enum class ComposeControlsMode {
Normal,
Scheduled,
};
extern const ChatHelpers::PauseReason kDefaultPanelsLevel;
struct ComposeControlsDescriptor {
const style::ComposeControls *stOverride = nullptr;
std::shared_ptr<ChatHelpers::Show> show;
Fn<void(not_null<DocumentData*>)> unavailableEmojiPasted;
ComposeControlsMode mode = ComposeControlsMode::Normal;
Fn<SendMenu::Details()> sendMenuDetails = nullptr;
Window::SessionController *regularWindow = nullptr;
rpl::producer<ChatHelpers::FileChosen> stickerOrEmojiChosen;
rpl::producer<QString> customPlaceholder;
QWidget *panelsParent = nullptr;
ChatHelpers::PauseReason panelsLevel = kDefaultPanelsLevel;
QString voiceCustomCancelText;
bool voiceLockFromBottom = false;
ChatHelpers::ComposeFeatures features;
rpl::producer<bool> scheduledToggleValue;
};
class ComposeControls final {
public:
using FileChosen = ChatHelpers::FileChosen;
using PhotoChosen = ChatHelpers::PhotoChosen;
using InlineChosen = InlineBots::ResultSelected;
using MessageToEdit = Controls::MessageToEdit;
using VoiceToSend = Controls::VoiceToSend;
using SendActionUpdate = Controls::SendActionUpdate;
using SetHistoryArgs = Controls::SetHistoryArgs;
using ReplyNextRequest = Controls::ReplyNextRequest;
using FieldHistoryAction = Ui::InputField::HistoryAction;
using Mode = ComposeControlsMode;
using ToggleCommentsState = Controls::ToggleCommentsState;
using SendStarButtonEffect = Controls::SendStarButtonEffect;
ComposeControls(
not_null<Ui::RpWidget*> parent,
ComposeControlsDescriptor descriptor);
~ComposeControls();
[[nodiscard]] Main::Session &session() const;
void setHistory(SetHistoryArgs &&args);
void updateFeatures(ChatHelpers::ComposeFeatures features);
void updateTopicRootId(MsgId topicRootId);
void updateShortcutId(BusinessShortcutId shortcutId);
void setCurrentDialogsEntryState(Dialogs::EntryState state);
[[nodiscard]] PeerData *sendAsPeer() const;
void finishAnimating();
void move(int x, int y);
void resizeToWidth(int width);
void setAutocompleteBoundingRect(QRect rect);
[[nodiscard]] rpl::producer<int> height() const;
[[nodiscard]] int heightCurrent() const;
void setupCommentsShownNewDot();
void setToggleCommentsButton(rpl::producer<ToggleCommentsState> state);
[[nodiscard]] rpl::producer<> commentsShownToggles() const;
void setStarsReactionCounter(
rpl::producer<Ui::SendStarButtonState> count,
rpl::producer<SendStarButtonEffect> effects);
using StarReactionTop = Data::MessageReactionsTopPaid;
void setStarsReactionTop(
rpl::producer<std::vector<StarReactionTop>> top);
struct StarReactionIncrement {
int count = 0;
bool fromBox = false;
};
[[nodiscard]] auto starsReactionIncrements() const
-> rpl::producer<StarReactionIncrement>;
bool focus();
[[nodiscard]] bool focused() const;
[[nodiscard]] rpl::producer<bool> focusedValue() const;
[[nodiscard]] rpl::producer<bool> tabbedPanelShownValue() const;
[[nodiscard]] rpl::producer<> cancelRequests() const;
[[nodiscard]] rpl::producer<Api::SendOptions> sendRequests() const;
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
[[nodiscard]] rpl::producer<QString> sendCommandRequests() const;
[[nodiscard]] rpl::producer<MessageToEdit> editRequests() const;
[[nodiscard]] rpl::producer<std::optional<bool>> attachRequests() const;
[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;
[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;
[[nodiscard]] rpl::producer<FullReplyTo> jumpToItemRequests() const;
[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
[[nodiscard]] rpl::producer<not_null<QEvent*>> viewportEvents() const;
[[nodiscard]] rpl::producer<> likeToggled() const;
[[nodiscard]] auto scrollKeyEvents() const
-> rpl::producer<not_null<QKeyEvent*>>;
[[nodiscard]] auto editLastMessageRequests() const
-> rpl::producer<not_null<QKeyEvent*>>;
[[nodiscard]] auto replyNextRequests() const
-> rpl::producer<ReplyNextRequest>;
[[nodiscard]] rpl::producer<> focusRequests() const;
[[nodiscard]] rpl::producer<> showScheduledRequests() const;
[[nodiscard]] rpl::producer<> scrollToMaxRequests() const;
using MimeDataHook = Fn<bool(
not_null<const QMimeData*> data,
Ui::InputField::MimeAction action)>;
void setMimeDataHook(MimeDataHook hook);
bool confirmMediaEdit(Ui::PreparedList &list);
bool pushTabbedSelectorToThirdSection(
not_null<Data::Thread*> thread,
const Window::SectionShow &params);
bool returnTabbedSelector();
[[nodiscard]] bool isEditingMessage() const;
[[nodiscard]] bool readyToForward() const;
[[nodiscard]] const HistoryItemsList &forwardItems() const;
[[nodiscard]] FullReplyTo replyingToMessage() const;
[[nodiscard]] bool preventsClose(Fn<void()> &&continueCallback) const;
void showForGrab();
void showStarted();
void showFinished();
void raisePanels();
void editMessage(FullMsgId id, const TextSelection &selection);
void cancelEditMessage();
void maybeCancelEditMessage(); // Confirm if changed and cancel.
void replyToMessage(FullReplyTo id);
void cancelReplyMessage();
void updateForwarding();
void cancelForward();
bool handleCancelRequest();
void tryProcessKeyInput(not_null<QKeyEvent*> e);
[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;
[[nodiscard]] Data::WebPageDraft webPageDraft() const;
void setText(const TextWithTags &text);
void clear();
void hidePanelsAnimated();
void clearListenState();
void clearChosenStarsForMessage();
[[nodiscard]] int chosenStarsForMessage() const;
void hide();
void show();
[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
[[nodiscard]] bool isLockPresent() const;
[[nodiscard]] bool isTTLButtonShown() const;
[[nodiscard]] bool isRecording() const;
[[nodiscard]] bool isRecordingPressed() const;
[[nodiscard]] rpl::producer<bool> recordingActiveValue() const;
[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
[[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const;
[[nodiscard]] Ui::RpWidget *likeAnimationTarget() const;
[[nodiscard]] int fieldCharacterCount() const;
[[nodiscard]] TextWithEntities prepareTextForEditMsg() const;
void applyCloudDraft();
void applyDraft(
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
Fn<void()> restoreTextCallback(const QString &insertTextOnCancel) const;
[[nodiscard]] Ui::InputField *fieldForMention() const;
private:
struct StarEffect;
enum class TextUpdateEvent {
SaveDraft = (1 << 0),
SendTyping = (1 << 1),
};
enum class DraftType {
Normal,
Edit,
};
enum class SendRequestType {
Text,
Voice,
};
using TextUpdateEvents = base::flags<TextUpdateEvent>;
friend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; };
void init();
void initField();
void initFieldAutocomplete();
void initTabbedSelector();
void initSendButton();
void initSendAsButton(
not_null<PeerData*> peer,
std::shared_ptr<Data::GroupCall> videoStream);
void initWebpageProcess();
void initForwardProcess();
void initWriteRestriction();
void initVoiceRecordBar();
void initKeyHandler();
void initLikeButton();
void initEditStarsButton();
void updateControlsParents();
void updateSubmitSettings();
void updateSendButtonType();
void updateMessagesTTLShown();
bool updateSendAsButton(std::shared_ptr<Data::GroupCall> videoStream);
void updateAttachBotsMenu();
void updateHeight();
void updateWrappingVisibility();
void updateControlsVisibility();
void updateControlsGeometry(QSize size);
bool updateReplaceMediaButton();
void updateOuterGeometry(QRect rect);
void paintBackground(QPainter &p, QRect full, QRect clip);
[[nodiscard]] auto computeSendButtonType() const;
[[nodiscard]] SendMenu::Details sendMenuDetails() const;
[[nodiscard]] SendMenu::Details saveMenuDetails() const;
[[nodiscard]] SendMenu::Details sendButtonMenuDetails() const;
[[nodiscard]] auto sendContentRequests(
SendRequestType requestType = SendRequestType::Text) const;
void editStarsFrom(int selected = 0);
void orderControls();
void updateFieldPlaceholder();
void updateSilentBroadcast();
void editMessage(not_null<HistoryItem*> item);
void escape();
void fieldChanged();
void toggleTabbedSelectorMode();
void createTabbedPanel();
void setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel);
[[nodiscard]] bool showRecordButton() const;
[[nodiscard]] bool showEditStarsButton() const;
[[nodiscard]] int shownStarsPerMessage() const;
bool updateBotCommandShown();
bool updateLikeShown();
void cancelInlineBot();
void clearInlineBot();
void inlineBotChanged();
[[nodiscard]] bool hasSilentBroadcastToggle() const;
[[nodiscard]] bool editStarsButtonShown() const;
void startStarsSendEffect();
void setupStarsSendEffectsCanvas();
void startStarsEffect(SendStarButtonEffect event);
void setupStarsEffectsCanvas();
// Look in the _field for the inline bot and query string.
void updateInlineBotQuery();
// Request to show results in the emoji panel.
void applyInlineBotQuery(UserData *bot, const QString &query);
[[nodiscard]] Data::DraftKey draftKey(
DraftType type = DraftType::Normal) const;
[[nodiscard]] Data::DraftKey draftKeyCurrent() const;
void saveDraft(bool delayed = false);
void saveDraftDelayed();
void saveDraftWithTextNow();
void saveCloudDraft();
void writeDrafts();
void writeDraftTexts();
void writeDraftCursors();
void setFieldText(
const TextWithTags &textWithTags,
TextUpdateEvents events = 0,
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
void clearFieldText(
TextUpdateEvents events = 0,
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
void saveFieldToHistoryLocalDraft();
void unregisterDraftSources();
void registerDraftSource();
void changeFocusedControl();
void checkCharsLimitation();
const style::ComposeControls &_st;
ChatHelpers::ComposeFeatures _features;
const not_null<Ui::RpWidget*> _parent;
const not_null<QWidget*> _panelsParent;
const std::shared_ptr<ChatHelpers::Show> _show;
const not_null<Main::Session*> _session;
Window::SessionController * const _regularWindow = nullptr;
std::unique_ptr<ChatHelpers::TabbedSelector> _ownedSelector;
const not_null<ChatHelpers::TabbedSelector*> _selector;
rpl::event_stream<ChatHelpers::FileChosen> _stickerOrEmojiChosen;
History *_history = nullptr;
MsgId _topicRootId = 0;
PeerId _monoforumPeerId = 0;
BusinessShortcutId _shortcutId = 0;
Fn<bool()> _showSlowmodeError;
Fn<Api::SendAction()> _sendActionFactory;
rpl::variable<int> _slowmodeSecondsLeft;
rpl::variable<bool> _sendDisabledBySlowmode;
rpl::variable<bool> _liked;
rpl::variable<Controls::WriteRestriction> _writeRestriction;
rpl::variable<bool> _hidden;
Mode _mode = Mode::Normal;
const std::unique_ptr<Ui::RpWidget> _wrap;
std::unique_ptr<Ui::RpWidget> _writeRestricted;
rpl::event_stream<FullReplyTo> _jumpToItemRequests;
std::optional<Ui::RoundRect> _backgroundRect;
const std::shared_ptr<Ui::SendButton> _send;
Ui::IconButton *_editStars = nullptr;
Ui::IconButton *_like = nullptr;
rpl::variable<int> _minStarsCount;
std::optional<int> _chosenStarsCount;
Ui::IconButton *_commentsShown = nullptr;
rpl::variable<bool> _commentsShownHidden;
Ui::RpWidget *_commentsShownNewDot = nullptr;
Ui::IconButton *_attachToggle = nullptr;
Ui::AbstractButton *_starsReaction = nullptr;
std::vector<std::unique_ptr<Ui::ReactionFlyAnimation>> _starSendEffects;
std::unique_ptr<Ui::RpWidget> _starSendEffectsCanvas;
std::vector<std::unique_ptr<StarEffect>> _starEffects;
std::unique_ptr<Ui::RpWidget> _starEffectsCanvas;
std::unique_ptr<Ui::IconButton> _replaceMedia;
const not_null<Ui::EmojiButton*> _tabbedSelectorToggle;
rpl::producer<QString> _fieldCustomPlaceholder;
const not_null<Ui::InputField*> _field;
Ui::IconButton * const _botCommandStart = nullptr;
std::unique_ptr<Ui::SendAsButton> _sendAs;
rpl::variable<bool> _videoStreamAdmin;
std::unique_ptr<Ui::SilentToggle> _silent;
std::unique_ptr<Controls::TTLButton> _ttlInfo;
base::unique_qptr<Controls::CharactersLimitLabel> _charsLimitation;
base::unique_qptr<Ui::IconButton> _scheduled;
std::unique_ptr<InlineBots::Layout::Widget> _inlineResults;
std::unique_ptr<ChatHelpers::TabbedPanel> _tabbedPanel;
std::unique_ptr<Ui::DropdownMenu> _attachBotsMenu;
std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
std::unique_ptr<Ui::Emoji::SuggestionsController> _emojiSuggestions;
friend class FieldHeader;
const std::unique_ptr<FieldHeader> _header;
const std::unique_ptr<Controls::VoiceRecordBar> _voiceRecordBar;
const Fn<SendMenu::Details()> _sendMenuDetails;
const Fn<void(not_null<DocumentData*>)> _unavailableEmojiPasted;
rpl::event_stream<Api::SendOptions> _sendCustomRequests;
rpl::event_stream<> _cancelRequests;
rpl::event_stream<FileChosen> _fileChosen;
rpl::event_stream<PhotoChosen> _photoChosen;
rpl::event_stream<InlineChosen> _inlineResultChosen;
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
rpl::event_stream<QString> _sendCommandRequests;
rpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents;
rpl::event_stream<not_null<QKeyEvent*>> _editLastMessageRequests;
rpl::event_stream<std::optional<bool>> _attachRequests;
rpl::event_stream<> _likeToggled;
rpl::event_stream<ReplyNextRequest> _replyNextRequests;
rpl::event_stream<> _focusRequests;
rpl::event_stream<> _showScheduledRequests;
rpl::event_stream<> _commentsShownToggles;
rpl::event_stream<StarReactionIncrement> _starsReactionIncrements;
rpl::variable<std::vector<StarReactionTop>> _starsReactionTop;
rpl::variable<bool> _recording;
rpl::variable<bool> _hasSendText;
TextUpdateEvents _textUpdateEvents = TextUpdateEvents()
| TextUpdateEvent::SaveDraft
| TextUpdateEvent::SendTyping;
Dialogs::EntryState _currentDialogsEntryState;
crl::time _saveDraftStart = 0;
bool _saveDraftText = false;
base::Timer _saveDraftTimer;
base::Timer _saveCloudDraftTimer;
UserData *_inlineBot = nullptr;
QString _inlineBotUsername;
bool _inlineLookingUpBot = false;
mtpRequestId _inlineBotResolveRequestId = 0;
bool _isInlineBot = false;
bool _botCommandShown = false;
bool _likeShown = false;
Webrtc::RecordAvailability _recordAvailability = {};
FullMsgId _editingId;
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
bool _canReplaceMedia = false;
bool _canAddMedia = false;
std::unique_ptr<Controls::WebpageProcessor> _preview;
rpl::lifetime _historyLifetime;
rpl::lifetime _uploaderSubscriptions;
};
[[nodiscard]] rpl::producer<int> SlowmodeSecondsLeft(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<bool> SendDisabledBySlowmode(
not_null<PeerData*> peer);
void ShowPhotoEditSpoilerMenu(
not_null<Ui::RpWidget*> parent,
not_null<HistoryItem*> item,
const std::optional<bool> &override,
Fn<void(bool)> callback);
[[nodiscard]] Image *MediaPreviewWithOverriddenSpoiler(
not_null<HistoryItem*> item,
bool spoiler);
} // namespace HistoryView

View File

@@ -0,0 +1,164 @@
/*
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 "history/view/controls/history_view_compose_media_edit_manager.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "menu/menu_send.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
namespace HistoryView {
MediaEditManager::MediaEditManager() = default;
void MediaEditManager::start(
not_null<HistoryItem*> item,
std::optional<bool> spoilered,
std::optional<bool> invertCaption) {
const auto media = item->media();
if (!media) {
cancel();
return;
}
_item = item;
_spoilered = spoilered.value_or(media->hasSpoiler());
_invertCaption = invertCaption.value_or(item->invertMedia());
_lifetime = item->history()->owner().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> removed) {
if (removed == _item) {
cancel();
}
});
}
void MediaEditManager::apply(SendMenu::Action action) {
using Type = SendMenu::Action::Type;
if (action.type == Type::CaptionUp) {
_invertCaption = true;
} else if (action.type == Type::CaptionDown) {
_invertCaption = false;
} else if (action.type == Type::SpoilerOn) {
_spoilered = true;
} else if (action.type == Type::SpoilerOff) {
_spoilered = false;
}
_updateRequests.fire({});
}
void MediaEditManager::cancel() {
_menu = nullptr;
_item = nullptr;
_lifetime.destroy();
}
void MediaEditManager::showMenu(
not_null<Ui::RpWidget*> parent,
Fn<void()> finished,
bool hasCaptionText) {
if (!_item) {
return;
}
const auto media = _item->media();
const auto hasPreview = media && media->hasReplyPreview();
const auto preview = hasPreview ? media->replyPreview() : nullptr;
if (!preview || (media && media->webpage())) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto callback = [=](SendMenu::Action action, const auto &) {
apply(action);
};
const auto position = QCursor::pos();
SendMenu::FillSendMenu(
_menu.get(),
nullptr,
sendMenuDetails(hasCaptionText),
callback,
&st::defaultComposeIcons,
position);
_menu->popup(position);
}
Image *MediaEditManager::mediaPreview() {
if (const auto media = _item ? _item->media() : nullptr) {
if (const auto photo = media->photo()) {
return photo->getReplyPreview(
_item->fullId(),
_item->history()->peer,
_spoilered);
} else if (const auto document = media->document()) {
return document->getReplyPreview(
_item->fullId(),
_item->history()->peer,
_spoilered);
}
}
return nullptr;
}
bool MediaEditManager::spoilered() const {
return _spoilered;
}
bool MediaEditManager::invertCaption() const {
return _invertCaption;
}
SendMenu::Details MediaEditManager::sendMenuDetails(
bool hasCaptionText) const {
const auto media = _item ? _item->media() : nullptr;
if (!media) {
return {};
}
const auto editingMedia = media->allowsEditMedia();
const auto editPhoto = editingMedia ? media->photo() : nullptr;
const auto editDocument = editingMedia ? media->document() : nullptr;
const auto canSaveSpoiler = CanBeSpoilered(_item);
const auto canMoveCaption = media->allowsEditCaption()
&& hasCaptionText
&& (editPhoto
|| (editDocument
&& (editDocument->isVideoFile() || editDocument->isGifv())));
return {
.spoiler = (!canSaveSpoiler
? SendMenu::SpoilerState::None
: _spoilered
? SendMenu::SpoilerState::Enabled
: SendMenu::SpoilerState::Possible),
.caption = (!canMoveCaption
? SendMenu::CaptionState::None
: _invertCaption
? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below),
};
}
rpl::producer<> MediaEditManager::updateRequests() const {
return _updateRequests.events();
}
bool MediaEditManager::CanBeSpoilered(not_null<HistoryItem*> item) {
const auto media = item ? item->media() : nullptr;
const auto editingMedia = media && media->allowsEditMedia();
const auto editPhoto = editingMedia ? media->photo() : nullptr;
const auto editDocument = editingMedia ? media->document() : nullptr;
return (editPhoto && !editPhoto->isNull())
|| (editDocument
&& (editDocument->isVideoFile() || editDocument->isGifv()));
}
} // namespace HistoryView

View File

@@ -0,0 +1,71 @@
/*
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 "base/unique_qptr.h"
namespace SendMenu {
struct Details;
struct Action;
} // namespace SendMenu
namespace Ui {
class RpWidget;
class PopupMenu;
} // namespace Ui
class Image;
class HistoryItem;
namespace HistoryView {
class MediaEditManager final {
public:
MediaEditManager();
void start(
not_null<HistoryItem*> item,
std::optional<bool> spoilered = {},
std::optional<bool> invertCaption = {});
void apply(SendMenu::Action action);
void cancel();
void showMenu(
not_null<Ui::RpWidget*> parent,
Fn<void()> finished,
bool hasCaptionText);
[[nodiscard]] Image *mediaPreview();
[[nodiscard]] bool spoilered() const;
[[nodiscard]] bool invertCaption() const;
[[nodiscard]] SendMenu::Details sendMenuDetails(
bool hasCaptionText) const;
[[nodiscard]] rpl::producer<> updateRequests() const;
[[nodiscard]] explicit operator bool() const {
return _item != nullptr;
}
[[nodiscard]] static bool CanBeSpoilered(not_null<HistoryItem*> item);
private:
base::unique_qptr<Ui::PopupMenu> _menu;
HistoryItem *_item = nullptr;
bool _spoilered = false;
bool _invertCaption = false;
rpl::event_stream<> _updateRequests;
rpl::lifetime _lifetime;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
/*
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
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class RpWidget;
} // namespace Ui
class History;
namespace HistoryView {
class ComposeSearch final {
public:
ComposeSearch(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> window,
not_null<History*> history,
PeerData *from = nullptr,
const QString &query = QString());
~ComposeSearch();
void hideAnimated();
void setInnerFocus();
void setQuery(const QString &query);
void setTopMsgId(MsgId topMsgId);
struct Activation {
not_null<HistoryItem*> item;
QString query;
};
[[nodiscard]] rpl::producer<Activation> activations() const;
[[nodiscard]] rpl::producer<> destroyRequests() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
class Inner;
const std::unique_ptr<Inner> _inner;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
/*
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 "data/data_drafts.h"
class History;
struct MessageLinkRange;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Window {
class SessionController;
} // namespace Window
namespace HistoryView::Controls {
class WebpageResolver;
struct EditDraftOptionsArgs {
std::shared_ptr<ChatHelpers::Show> show;
not_null<History*> history;
Data::Draft draft;
QString usedLink;
Data::ResolvedForwardDraft forward;
std::vector<MessageLinkRange> links;
std::shared_ptr<WebpageResolver> resolver;
Fn<void(FullReplyTo, Data::WebPageDraft, Data::ForwardDraft)> done;
Fn<void(FullReplyTo)> highlight;
Fn<void()> clearOldDraft;
};
void EditDraftOptions(EditDraftOptionsArgs &&args);
void ShowReplyToChatBox(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> clearOldDraft = nullptr);
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,482 @@
/*
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 "history/view/controls/history_view_forward_panel.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/history_item_components.h"
#include "history/view/history_view_item_preview.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_media_types.h"
#include "data/data_forum_topic.h"
#include "main/main_session.h"
#include "ui/chat/forward_options_box.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "core/ui_integration.h"
#include "lang/lang_keys.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "apiwrap.h"
#include "boxes/peer_list_controllers.h"
#include "data/data_changes.h"
#include "settings/settings_common.h"
#include "ui/widgets/buttons.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
namespace HistoryView::Controls {
namespace {
constexpr auto kUnknownVersion = -1;
constexpr auto kNameWithCaptionsVersion = -2;
constexpr auto kNameNoCaptionsVersion = -3;
} // namespace
ForwardPanel::ForwardPanel(Fn<void()> repaint)
: _repaint(std::move(repaint)) {
}
void ForwardPanel::update(
Data::Thread *to,
Data::ResolvedForwardDraft draft) {
if (_to == to
&& _data.items == draft.items
&& _data.options == draft.options) {
return;
}
_dataLifetime.destroy();
_data = std::move(draft);
_to = to;
if (!empty()) {
Assert(to != nullptr);
_data.items.front()->history()->owner().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
itemRemoved(item);
}, _dataLifetime);
if (const auto topic = _to->asTopic()) {
topic->destroyed(
) | rpl::on_next([=] {
update(nullptr, {});
}, _dataLifetime);
} else if (const auto sublist = _to->asSublist()) {
sublist->destroyed(
) | rpl::on_next([=] {
update(nullptr, {});
}, _dataLifetime);
}
updateTexts();
}
_itemsUpdated.fire({});
}
rpl::producer<> ForwardPanel::itemsUpdated() const {
return _itemsUpdated.events();
}
void ForwardPanel::checkTexts() {
if (empty()) {
return;
}
const auto keepNames = (_data.options
== Data::ForwardOptions::PreserveInfo);
const auto keepCaptions = (_data.options
!= Data::ForwardOptions::NoNamesAndCaptions);
auto version = keepNames
? 0
: keepCaptions
? kNameWithCaptionsVersion
: kNameNoCaptionsVersion;
if (keepNames) {
for (const auto item : _data.items) {
if (const auto from = item->originalSender()) {
version += from->nameVersion();
} else if (item->originalHiddenSenderInfo()) {
++version;
} else {
Unexpected("Corrupt forwarded information in message.");
}
}
}
if (_nameVersion != version) {
_nameVersion = version;
updateTexts();
}
}
void ForwardPanel::updateTexts() {
const auto repainter = gsl::finally([&] {
_repaint();
});
if (empty()) {
_from.clear();
_text.clear();
return;
}
QString from;
TextWithEntities text;
const auto keepNames = (_data.options
== Data::ForwardOptions::PreserveInfo);
const auto keepCaptions = (_data.options
!= Data::ForwardOptions::NoNamesAndCaptions);
if (const auto count = int(_data.items.size())) {
auto insertedPeers = base::flat_set<not_null<PeerData*>>();
auto insertedNames = base::flat_set<QString>();
auto fullname = QString();
auto names = std::vector<QString>();
names.reserve(_data.items.size());
for (const auto item : _data.items) {
if (const auto from = item->originalSender()) {
if (!insertedPeers.contains(from)) {
insertedPeers.emplace(from);
names.push_back(from->shortName());
fullname = from->name();
}
} else if (const auto info = item->originalHiddenSenderInfo()) {
if (!insertedNames.contains(info->name)) {
insertedNames.emplace(info->name);
names.push_back(info->firstName);
fullname = info->name;
}
} else {
Unexpected("Corrupt forwarded information in message.");
}
}
if (!keepNames || HasOnlyDroppedForwardedInfo(_data.items)) {
from = tr::lng_forward_sender_names_removed(tr::now);
} else if (names.size() > 2) {
from = tr::lng_forwarding_from(
tr::now,
lt_count,
names.size() - 1,
lt_user,
names[0]);
} else if (names.size() < 2) {
from = fullname;
} else {
from = tr::lng_forwarding_from_two(
tr::now,
lt_user,
names[0],
lt_second_user,
names[1]);
}
if (count < 2) {
const auto item = _data.items.front();
text = item->toPreview({
.hideSender = true,
.hideCaption = !keepCaptions,
.generateImages = false,
.ignoreGroup = true,
}).text;
if (item->computeDropForwardedInfo() || !keepNames) {
text = DropDisallowedCustomEmoji(_to->peer(), std::move(text));
}
} else {
text = Ui::Text::Colorized(
tr::lng_forward_messages(tr::now, lt_count, count));
}
}
_from.setText(st::msgNameStyle, from, Ui::NameTextOptions());
const auto context = Core::TextContext({
.session = &_to->session(),
.repaint = _repaint,
});
_text.setMarkedText(
st::defaultTextStyle,
text,
Ui::DialogTextOptions(),
context);
}
void ForwardPanel::refreshTexts() {
_nameVersion = kUnknownVersion;
checkTexts();
}
void ForwardPanel::itemRemoved(not_null<const HistoryItem*> item) {
const auto i = ranges::find(_data.items, item);
if (i != end(_data.items)) {
_data.items.erase(i);
refreshTexts();
_itemsUpdated.fire({});
}
}
const Data::ResolvedForwardDraft &ForwardPanel::draft() const {
return _data;
}
const HistoryItemsList &ForwardPanel::items() const {
return _data.items;
}
bool ForwardPanel::empty() const {
return _data.items.empty();
}
void ForwardPanel::applyOptions(Data::ForwardOptions options) {
if (_data.items.empty()) {
return;
} else if (_data.options != options) {
const auto topicRootId = _to->topicRootId();
const auto monoforumPeerId = _to->monoforumPeerId();
_data.options = options;
_to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, {
.ids = _to->owner().itemsToIds(_data.items),
.options = options,
});
_repaint();
}
}
void ForwardPanel::editToNextOption() {
using Options = Data::ForwardOptions;
const auto captionsCount = ItemsForwardCaptionsCount(_data.items);
const auto hasOnlyForcedForwardedInfo = !captionsCount
&& HasOnlyForcedForwardedInfo(_data.items);
if (hasOnlyForcedForwardedInfo) {
return;
}
const auto now = _data.options;
const auto next = (now == Options::PreserveInfo)
? Options::NoSenderNames
: ((now == Options::NoSenderNames) && captionsCount)
? Options::NoNamesAndCaptions
: Options::PreserveInfo;
const auto topicRootId = _to->topicRootId();
const auto monoforumPeerId = _to->monoforumPeerId();
_to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, {
.ids = _to->owner().itemsToIds(_data.items),
.options = next,
});
_repaint();
}
void ForwardPanel::paint(
Painter &p,
int x,
int y,
int available,
int outerWidth) const {
if (empty()) {
return;
}
const_cast<ForwardPanel*>(this)->checkTexts();
const auto now = crl::now();
const auto paused = p.inactive();
const auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler);
const auto firstItem = _data.items.front();
const auto firstMedia = firstItem->media();
const auto hasPreview = (_data.items.size() < 2)
&& firstMedia
&& firstMedia->hasReplyPreview();
const auto preview = hasPreview ? firstMedia->replyPreview() : nullptr;
const auto spoiler = preview && firstMedia->hasSpoiler();
if (!spoiler) {
_spoiler = nullptr;
} else if (!_spoiler) {
_spoiler = std::make_unique<Ui::SpoilerAnimation>(_repaint);
}
if (preview) {
auto to = QRect(
x,
y + (st::historyReplyHeight - st::historyReplyPreview) / 2,
st::historyReplyPreview,
st::historyReplyPreview);
p.drawPixmap(to.x(), to.y(), preview->pixSingle(
preview->size() / style::DevicePixelRatio(),
{
.options = Images::Option::RoundSmall,
.outer = to.size(),
}));
if (_spoiler) {
Ui::FillSpoilerRect(p, to, Ui::DefaultImageSpoiler().frame(
_spoiler->index(now, pausedSpoiler)));
}
const auto skip = st::historyReplyPreview + st::msgReplyBarSkip;
x += skip;
available -= skip;
}
p.setPen(st::historyReplyNameFg);
_from.drawElided(
p,
x,
y + st::msgReplyPadding.top(),
available);
p.setPen(st::historyComposeAreaFg);
_text.draw(p, {
.position = QPoint(
x,
y + st::msgReplyPadding.top() + st::msgServiceNameFont->height),
.availableWidth = available,
.palette = &st::historyComposeAreaPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = now,
.pausedEmoji = paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = pausedSpoiler,
.elisionLines = 1,
});
}
void ClearDraftReplyTo(
not_null<History*> history,
MsgId topicRootId,
PeerId monoforumPeerId,
FullMsgId equalTo) {
const auto local = history->localDraft(topicRootId, monoforumPeerId);
if (!local || (equalTo && local->reply.messageId != equalTo)) {
return;
}
auto draft = *local;
draft.reply = {
.topicRootId = topicRootId,
.monoforumPeerId = monoforumPeerId,
};
draft.suggest = SuggestOptions();
if (Data::DraftIsNull(&draft)) {
history->clearLocalDraft(topicRootId, monoforumPeerId);
} else {
history->setLocalDraft(
std::make_unique<Data::Draft>(std::move(draft)));
}
const auto thread = history->threadFor(topicRootId, monoforumPeerId);
if (thread) {
history->session().api().saveDraftToCloudDelayed(thread);
}
}
void EditWebPageOptions(
std::shared_ptr<ChatHelpers::Show> show,
not_null<WebPageData*> webpage,
Data::WebPageDraft draft,
Fn<void(Data::WebPageDraft)> done) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(rpl::single(u"Link Preview"_q));
struct State {
rpl::variable<Data::WebPageDraft> result;
Ui::SettingsButton *large = nullptr;
Ui::SettingsButton *small = nullptr;
};
const auto state = box->lifetime().make_state<State>(State{
.result = draft,
});
state->large = Settings::AddButtonWithIcon(
box->verticalLayout(),
rpl::single(u"Force large media"_q),
st::settingsButton,
{ &st::menuIconMakeBig });
state->large->setClickedCallback([=] {
auto copy = state->result.current();
copy.forceLargeMedia = true;
copy.forceSmallMedia = false;
state->result = copy;
});
state->small = Settings::AddButtonWithIcon(
box->verticalLayout(),
rpl::single(u"Force small media"_q),
st::settingsButton,
{ &st::menuIconMakeSmall });
state->small->setClickedCallback([=] {
auto copy = state->result.current();
copy.forceSmallMedia = true;
copy.forceLargeMedia = false;
state->result = copy;
});
state->result.value(
) | rpl::on_next([=](const Data::WebPageDraft &draft) {
state->large->setColorOverride(draft.forceLargeMedia
? st::windowActiveTextFg->c
: std::optional<QColor>());
state->small->setColorOverride(draft.forceSmallMedia
? st::windowActiveTextFg->c
: std::optional<QColor>());
}, box->lifetime());
Settings::AddButtonWithIcon(
box->verticalLayout(),
state->result.value(
) | rpl::map([=](const Data::WebPageDraft &draft) {
return draft.invert
? u"Above message"_q
: u"Below message"_q;
}),
st::settingsButton,
{ &st::menuIconChangeOrder }
)->setClickedCallback([=] {
auto copy = state->result.current();
copy.invert = !copy.invert;
state->result = copy;
});
box->addButton(tr::lng_settings_save(), [=] {
const auto weak = base::make_weak(box.get());
auto result = state->result.current();
result.manual = true;
done(result);
if (const auto strong = weak.get()) {
strong->closeBox();
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}));
}
bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) {
for (const auto &item : list) {
if (const auto media = item->media()) {
if (!media->forceForwardedInfo()) {
return false;
}
} else {
return false;
}
}
return true;
}
bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list) {
for (const auto &item : list) {
if (item->isSavedMusicItem() || !item->computeDropForwardedInfo()) {
return false;
}
}
return true;
}
bool HasDropForwardedInfoSetting(const HistoryItemsList &list) {
for (const auto &item : list) {
if (!item->computeDropForwardedInfo()) {
return true;
}
}
return false;
}
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,92 @@
/*
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 "history/history.h"
#include "ui/text/text.h"
#include "base/weak_ptr.h"
class Painter;
class HistoryItem;
namespace Ui {
class SpoilerAnimation;
} // namespace Ui
namespace Data {
class Thread;
struct WebPageDraft;
} // namespace Data
namespace Window {
class SessionController;
} // namespace Window
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace HistoryView::Controls {
class ForwardPanel final : public base::has_weak_ptr {
public:
explicit ForwardPanel(Fn<void()> repaint);
void update(Data::Thread *to, Data::ResolvedForwardDraft draft);
void paint(
Painter &p,
int x,
int y,
int available,
int outerWidth) const;
[[nodiscard]] rpl::producer<> itemsUpdated() const;
void applyOptions(Data::ForwardOptions options);
void editToNextOption();
[[nodiscard]] const Data::ResolvedForwardDraft &draft() const;
[[nodiscard]] const HistoryItemsList &items() const;
[[nodiscard]] bool empty() const;
private:
void checkTexts();
void updateTexts();
void refreshTexts();
void itemRemoved(not_null<const HistoryItem*> item);
Fn<void()> _repaint;
Data::Thread *_to = nullptr;
Data::ResolvedForwardDraft _data;
rpl::lifetime _dataLifetime;
rpl::event_stream<> _itemsUpdated;
Ui::Text::String _from, _text;
mutable std::unique_ptr<Ui::SpoilerAnimation> _spoiler;
int _nameVersion = 0;
};
void ClearDraftReplyTo(
not_null<History*> history,
MsgId topicRootId,
PeerId monoforumPeerId,
FullMsgId equalTo);
void EditWebPageOptions(
std::shared_ptr<ChatHelpers::Show> show,
not_null<WebPageData*> webpage,
Data::WebPageDraft draft,
Fn<void(Data::WebPageDraft)> done);
[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list);
[[nodiscard]] bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list);
[[nodiscard]] bool HasDropForwardedInfoSetting(const HistoryItemsList &list);
} // namespace HistoryView::Controls

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,164 @@
/*
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 "api/api_common.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Ui {
class GenericBox;
class VerticalLayout;
class NumberInput;
class InputField;
} // namespace Ui
namespace Ui::Text {
class CustomEmojiHelper;
} // namespace Ui::Text
namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace HistoryView {
enum class SuggestMode {
New,
Change,
Publish,
Gift,
};
struct SuggestTimeBoxArgs {
not_null<Main::Session*> session;
Fn<void(TimeId)> done;
TimeId value = 0;
SuggestMode mode = SuggestMode::New;
};
void ChooseSuggestTimeBox(
not_null<Ui::GenericBox*> box,
SuggestTimeBoxArgs &&args);
struct StarsInputFieldArgs {
std::optional<int64> value;
int64 max = 0;
};
[[nodiscard]] not_null<Ui::NumberInput*> AddStarsInputField(
not_null<Ui::VerticalLayout*> container,
StarsInputFieldArgs &&args);
struct TonInputFieldArgs {
int64 value = 0;
};
[[nodiscard]] not_null<Ui::InputField*> AddTonInputField(
not_null<Ui::VerticalLayout*> container,
TonInputFieldArgs &&args);
struct StarsTonPriceInput {
Fn<void()> focusCallback;
Fn<std::optional<CreditsAmount>()> computeResult;
rpl::producer<> submits;
rpl::producer<> updates;
rpl::producer<CreditsAmount> result;
};
struct StarsTonPriceArgs {
not_null<Main::Session*> session;
rpl::producer<bool> showTon;
CreditsAmount price;
int starsMin = 0;
int starsMax = 0;
int64 nanoTonMin = 0;
int64 nanoTonMax = 0;
bool allowEmpty = false;
Fn<void(CreditsAmount)> errorHook;
rpl::producer<TextWithEntities> starsAbout;
rpl::producer<TextWithEntities> tonAbout;
};
[[nodiscard]] StarsTonPriceInput AddStarsTonPriceInput(
not_null<Ui::VerticalLayout*> container,
StarsTonPriceArgs &&args);
struct SuggestPriceBoxArgs {
not_null<PeerData*> peer;
bool updating = false;
Fn<void(SuggestOptions)> done;
SuggestOptions value;
SuggestMode mode = SuggestMode::New;
QString giftName;
};
void ChooseSuggestPriceBox(
not_null<Ui::GenericBox*> box,
SuggestPriceBoxArgs &&args);
[[nodiscard]] bool CanEditSuggestedMessage(not_null<HistoryItem*> item);
[[nodiscard]] bool CanAddOfferToMessage(not_null<HistoryItem*> item);
[[nodiscard]] CreditsAmount PriceAfterCommission(
not_null<Main::Session*> session,
CreditsAmount price);
[[nodiscard]] QString FormatAfterCommissionPercent(
not_null<Main::Session*> session,
CreditsAmount price);
void InsufficientTonBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
CreditsAmount required);
class SuggestOptionsBar final {
public:
SuggestOptionsBar(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
SuggestOptions values,
SuggestMode mode);
~SuggestOptionsBar();
void paintBar(QPainter &p, int x, int y, int outerWidth);
void edit();
void paintIcon(QPainter &p, int x, int y, int outerWidth);
void paintLines(QPainter &p, int x, int y, int outerWidth);
[[nodiscard]] SuggestOptions values() const;
[[nodiscard]] rpl::producer<> updates() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
void updateTexts();
[[nodiscard]] TextWithEntities composeText(
Ui::Text::CustomEmojiHelper &helper) const;
const std::shared_ptr<ChatHelpers::Show> _show;
const not_null<PeerData*> _peer;
const SuggestMode _mode = SuggestMode::New;
Ui::Text::String _title;
Ui::Text::String _text;
SuggestOptions _values;
rpl::event_stream<> _updates;
rpl::lifetime _lifetime;
};
} // namespace HistoryView

View File

@@ -0,0 +1,69 @@
/*
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 "history/view/controls/history_view_ttl_button.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "main/main_session.h"
#include "menu/menu_ttl_validator.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_chat.h"
namespace HistoryView::Controls {
TTLButton::TTLButton(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer)
: _peer(peer)
, _button(parent, st::historyMessagesTTL) {
const auto validator = TTLMenu::TTLValidator(std::move(show), peer);
_button.setClickedCallback([=] {
if (!validator.can()) {
validator.showToast();
return;
}
validator.showBox();
});
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::MessagesTTL
) | rpl::on_next([=] {
_button.setText(Ui::FormatTTLTiny(peer->messagesTTL()));
}, _button.lifetime());
}
void TTLButton::show() {
_button.show();
}
void TTLButton::hide() {
_button.hide();
}
void TTLButton::setVisible(bool visible) {
_button.setVisible(visible);
}
bool TTLButton::isVisible() const {
return _button.isVisible();
}
void TTLButton::move(int x, int y) {
_button.move(x, y);
}
int TTLButton::width() const {
return _button.width();
}
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,43 @@
/*
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/widgets/icon_button_with_text.h"
namespace Ui {
class Show;
} // namespace Ui
namespace HistoryView::Controls {
class TTLButton final {
public:
TTLButton(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer);
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
void show();
void hide();
void setVisible(bool visible);
[[nodiscard]] bool isVisible() const;
void move(int x, int y);
[[nodiscard]] int width() const;
private:
const not_null<PeerData*> _peer;
Ui::IconButtonWithText _button;
};
} // namespace HistoryView::Controls

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,235 @@
/*
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 "api/api_common.h"
#include "base/timer.h"
#include "history/view/controls/compose_controls_common.h"
#include "media/audio/media_audio_capture_common.h"
#include "ui/controls/round_video_recorder.h"
#include "ui/effects/animations.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
struct VoiceData;
namespace style {
struct RecordBar;
} // namespace style
namespace Media::Capture {
enum class Error : uchar;
} // namespace Media::Capture
namespace Ui {
class AbstractButton;
class SendButton;
class RoundVideoRecorder;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace HistoryView::Controls {
class VoiceRecordButton;
class ListenWrap;
class RecordLock;
class CancelButton;
struct VoiceRecordBarDescriptor {
not_null<Ui::RpWidget*> outerContainer;
std::shared_ptr<ChatHelpers::Show> show;
std::shared_ptr<Ui::SendButton> send;
QString customCancelText;
const style::RecordBar *stOverride = nullptr;
int recorderHeight = 0;
bool lockFromBottom = false;
};
class VoiceRecordBar final : public Ui::RpWidget {
public:
using SendActionUpdate = Controls::SendActionUpdate;
using VoiceToSend = Controls::VoiceToSend;
using FilterCallback = Fn<bool()>;
using Error = ::Media::Capture::Error;
VoiceRecordBar(
not_null<Ui::RpWidget*> parent,
VoiceRecordBarDescriptor &&descriptor);
VoiceRecordBar(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Ui::SendButton> send,
int recorderHeight);
~VoiceRecordBar();
void showDiscardBox(
Fn<void()> &&callback,
anim::type animated = anim::type::instant);
void startRecordingAndLock(bool round);
void finishAnimating();
void hideAnimated();
void hideFast();
void clearListenState();
void orderControls();
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
[[nodiscard]] rpl::producer<> cancelRequests() const;
[[nodiscard]] rpl::producer<bool> recordingStateChanges() const;
[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
[[nodiscard]] rpl::producer<not_null<QEvent*>> lockViewportEvents() const;
[[nodiscard]] rpl::producer<> updateSendButtonTypeRequests() const;
[[nodiscard]] rpl::producer<> recordingTipRequests() const;
[[nodiscard]] rpl::producer<Error> errors() const;
void requestToSendWithOptions(Api::SendOptions options);
void setStartRecordingFilter(FilterCallback &&callback);
void setTTLFilter(FilterCallback &&callback);
void setPauseInsteadSend(bool pauseInsteadSend);
[[nodiscard]] bool isRecording() const;
[[nodiscard]] bool isRecordingLocked() const;
[[nodiscard]] bool isLockPresent() const;
[[nodiscard]] bool isListenState() const;
[[nodiscard]] bool isActive() const;
[[nodiscard]] bool isRecordingByAnotherBar() const;
[[nodiscard]] bool isTTLButtonShown() const;
private:
enum class StopType {
Cancel,
Send,
Listen,
};
enum class TTLAnimationType {
RightLeft,
TopBottom,
RightTopStatic,
};
void init();
void initLockGeometry();
void initLevelGeometry();
void updateMessageGeometry();
void updateLockGeometry();
void updateTTLGeometry(TTLAnimationType type, float64 progress);
void recordUpdated(quint16 level, int samples);
void checkTipRequired();
void stop(bool send);
void stopRecording(StopType type, bool ttlBeforeHide = false);
void visibilityAnimate(bool show, Fn<void()> &&callback);
void drawDuration(QPainter &p);
void drawRedCircle(QPainter &p);
void drawMessage(QPainter &p, float64 recordActive);
void startRedCircleAnimation();
void installListenStateFilter();
void startRecording();
void prepareOnSendPress();
[[nodiscard]] bool isTypeRecord() const;
[[nodiscard]] bool hasDuration() const;
void finish();
void activeAnimate(bool active);
[[nodiscard]] float64 showAnimationRatio() const;
[[nodiscard]] float64 showListenAnimationRatio() const;
[[nodiscard]] float64 activeAnimationRatio() const;
void computeAndSetLockProgress(QPoint globalPos);
[[nodiscard]] float64 calcLockProgress(QPoint globalPos);
[[nodiscard]] bool peekTTLState() const;
[[nodiscard]] bool takeTTLState() const;
[[nodiscard]] bool createVideoRecorder();
const style::RecordBar &_st;
const not_null<Ui::RpWidget*> _outerContainer;
const std::shared_ptr<ChatHelpers::Show> _show;
const std::shared_ptr<Ui::SendButton> _send;
const std::unique_ptr<RecordLock> _lock;
const std::unique_ptr<VoiceRecordButton> _level;
const std::unique_ptr<CancelButton> _cancel;
std::unique_ptr<Ui::AbstractButton> _ttlButton;
std::unique_ptr<ListenWrap> _listen;
Ui::RoundVideoResult _data;
rpl::variable<bool> _paused;
base::Timer _startTimer;
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
rpl::event_stream<> _cancelRequests;
rpl::event_stream<> _listenChanges;
rpl::event_stream<Error> _errors;
int _centerY = 0;
QRect _redCircleRect;
QRect _durationRect;
QRect _messageRect;
Ui::Text::String _message;
FilterCallback _startRecordingFilter;
FilterCallback _hasTTLFilter;
base::unique_qptr<QObject> _keyFilterInRecordingState;
bool _warningShown = false;
bool _pauseInsteadSend = false;
rpl::variable<bool> _recording = false;
rpl::variable<bool> _inField = false;
rpl::variable<bool> _lockShowing = false;
int _recordingSamples = 0;
float64 _redCircleProgress = 0.;
rpl::event_stream<> _recordingTipRequests;
crl::time _recordingTipRequire = 0;
bool _lockFromBottom = false;
std::unique_ptr<Ui::RoundVideoRecorder> _videoRecorder;
std::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding;
rpl::lifetime _videoCapturerLifetime;
bool _recordingVideo = false;
bool _fullRecord = false;
const style::font &_cancelFont;
rpl::lifetime _recordingLifetime;
std::optional<Ui::RoundRect> _backgroundRect;
Ui::Animations::Simple _showLockAnimation;
Ui::Animations::Simple _lockToStopAnimation;
Ui::Animations::Simple _showListenAnimation;
Ui::Animations::Simple _activeAnimation;
Ui::Animations::Simple _showAnimation;
};
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,264 @@
/*
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 "history/view/controls/history_view_voice_record_button.h"
#include "ui/paint/blobs.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include <QtMath>
namespace HistoryView::Controls {
namespace {
constexpr auto kMaxLevel = 1800.;
constexpr auto kBlobAlpha = 76. / 255.;
constexpr auto kBlobMaxSpeed = 5.0;
constexpr auto kLevelDuration = 100. + 500. * 0.33;
constexpr auto kBlobsScaleEnterDuration = crl::time(250);
auto Blobs() {
return std::vector<Ui::Paint::Blobs::BlobData>{
{
.segmentsCount = 9,
.minScale = 0.605229,
.minRadius = (float)st::historyRecordMinorBlobMinRadius,
.maxRadius = (float)st::historyRecordMinorBlobMaxRadius,
.speedScale = 1.,
.alpha = kBlobAlpha,
.maxSpeed = kBlobMaxSpeed,
},
{
.segmentsCount = 12,
.minScale = 0.553943,
.minRadius = (float)st::historyRecordMajorBlobMinRadius,
.maxRadius = (float)st::historyRecordMajorBlobMaxRadius,
.speedScale = 1.,
.alpha = kBlobAlpha,
.maxSpeed = kBlobMaxSpeed,
},
};
}
} // namespace
VoiceRecordButton::VoiceRecordButton(
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st)
: AbstractButton(parent)
, _blobs(std::make_unique<Ui::Paint::Blobs>(
Blobs(),
kLevelDuration,
kMaxLevel))
, _center(_blobs->maxRadius()) {
resize(_center * 2, _center * 2);
init();
}
VoiceRecordButton::~VoiceRecordButton() = default;
void VoiceRecordButton::requestPaintLevel(quint16 level) {
if (_blobsHideLastTime) {
return;
}
_blobs->setLevel(level);
update();
}
void VoiceRecordButton::init() {
const auto currentState = lifetime().make_state<Type>(_state.current());
rpl::single(
anim::Disabled()
) | rpl::then(
anim::Disables()
) | rpl::on_next([=](bool hide) {
if (hide) {
_blobs->setLevel(0.);
}
_blobsHideLastTime = hide ? crl::now() : 0;
if (!hide && !_animation.animating() && isVisible()) {
_animation.start();
}
}, lifetime());
const auto &mainRadiusMin = st::historyRecordMainBlobMinRadius;
const auto mainRadiusDiff = st::historyRecordMainBlobMaxRadius
- mainRadiusMin;
paintRequest(
) | rpl::on_next([=](const QRect &clip) {
auto p = QPainter(this);
const auto hideProgress = _blobsHideLastTime
? 1. - std::clamp(
((crl::now() - _blobsHideLastTime)
/ (float64)kBlobsScaleEnterDuration),
0.,
1.)
: 1.;
const auto showProgress = _showProgress.current();
const auto complete = (showProgress == 1.);
p.translate(_center, _center);
PainterHighQualityEnabler hq(p);
const auto brush = QBrush(anim::color(
st::historyRecordVoiceFgInactive,
st::historyRecordVoiceFgActive,
_colorProgress));
_blobs->paint(p, brush, showProgress * hideProgress);
const auto radius = (mainRadiusMin
+ (mainRadiusDiff * _blobs->currentLevel())) * showProgress;
p.setPen(Qt::NoPen);
p.setBrush(brush);
p.drawEllipse(QPointF(), radius, radius);
if (!complete) {
p.setOpacity(showProgress);
}
// Paint icon.
{
const auto stateProgress = _stateChangedAnimation.value(0.);
const auto scale = (std::cos(M_PI * 2 * stateProgress) + 1.) * .5;
if (scale < 1.) {
p.scale(scale, scale);
}
const auto state = *currentState;
const auto icon = (state == Type::Send)
? st::historySendIcon
: (state == Type::Record)
? st::historyRecordVoiceActive
: st::historyRecordRoundActive;
const auto position = (state == Type::Send)
? st::historyRecordSendIconPosition
: (state == Type::Record)
? QPoint(0, 0)
: st::historyRecordRoundIconPosition;
icon.paint(
p,
-icon.width() / 2 + position.x(),
-icon.height() / 2 + position.y(),
0,
st::historyRecordVoiceFgActiveIcon->c);
}
}, lifetime());
_animation.init([=](crl::time now) {
if (const auto &last = _blobsHideLastTime; (last > 0)
&& (now - last >= kBlobsScaleEnterDuration)) {
_animation.stop();
return false;
}
_blobs->updateLevel(now - _lastUpdateTime);
_lastUpdateTime = now;
update();
return true;
});
rpl::merge(
shownValue(),
_showProgress.value(
) | rpl::map(rpl::mappers::_1 != 0.) | rpl::distinct_until_changed()
) | rpl::on_next([=](bool show) {
setVisible(show);
setMouseTracking(show);
if (!show) {
_animation.stop();
_showProgress = 0.;
_blobs->resetLevel();
_state = Type::Record;
} else {
if (!_animation.animating()) {
_animation.start();
}
}
}, lifetime());
actives(
) | rpl::distinct_until_changed(
) | rpl::on_next([=](bool active) {
setPointerCursor(active);
}, lifetime());
_state.changes(
) | rpl::on_next([=](Type newState) {
const auto to = 1.;
auto callback = [=](float64 value) {
if (value >= (to * .5)) {
*currentState = newState;
}
update();
};
constexpr auto kDuration = st::universalDuration * 2;
_stateChangedAnimation.start(std::move(callback), 0., to, kDuration);
}, lifetime());
}
rpl::producer<bool> VoiceRecordButton::actives() const {
return events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::MouseMove
|| e->type() == QEvent::Leave
|| e->type() == QEvent::Enter);
}) | rpl::map([=](not_null<QEvent*> e) {
switch(e->type()) {
case QEvent::MouseMove:
return inCircle((static_cast<QMouseEvent*>(e.get()))->pos());
case QEvent::Leave: return false;
case QEvent::Enter: return inCircle(mapFromGlobal(QCursor::pos()));
default: return false;
}
});
}
rpl::producer<> VoiceRecordButton::clicks() const {
return Ui::AbstractButton::clicks(
) | rpl::to_empty | rpl::filter([=] {
return inCircle(mapFromGlobal(QCursor::pos()));
});
}
bool VoiceRecordButton::inCircle(const QPoint &localPos) const {
const auto &radii = st::historyRecordMainBlobMaxRadius;
const auto dx = std::abs(localPos.x() - _center);
if (dx > radii) {
return false;
}
const auto dy = std::abs(localPos.y() - _center);
if (dy > radii) {
return false;
} else if (dx + dy <= radii) {
return true;
}
return ((dx * dx + dy * dy) <= (radii * radii));
}
void VoiceRecordButton::requestPaintProgress(float64 progress) {
_showProgress = progress;
update();
}
void VoiceRecordButton::requestPaintColor(float64 progress) {
if (_colorProgress == progress) {
return;
}
_colorProgress = progress;
update();
}
void VoiceRecordButton::setType(Type state) {
_state = state;
}
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,67 @@
/*
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/abstract_button.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
namespace style {
struct RecordBar;
} // namespace style
namespace Ui::Paint {
class Blobs;
} // namespace Ui::Paint
namespace HistoryView::Controls {
class VoiceRecordButton final : public Ui::AbstractButton {
public:
VoiceRecordButton(
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st);
~VoiceRecordButton();
enum class Type {
Send,
Record,
Round,
};
void setType(Type state);
void requestPaintColor(float64 progress);
void requestPaintProgress(float64 progress);
void requestPaintLevel(quint16 level);
[[nodiscard]] rpl::producer<bool> actives() const;
[[nodiscard]] rpl::producer<> clicks() const;
[[nodiscard]] bool inCircle(const QPoint &localPos) const;
private:
void init();
std::unique_ptr<Ui::Paint::Blobs> _blobs;
crl::time _lastUpdateTime = 0;
crl::time _blobsHideLastTime = 0;
const int _center;
rpl::variable<float64> _showProgress = 0.;
float64 _colorProgress = 0.;
rpl::variable<Type> _state = Type::Record;
// This can animate for a very long time (like in music playing),
// so it should be a Basic, not a Simple animation.
Ui::Animations::Basic _animation;
Ui::Animations::Simple _stateChangedAnimation;
};
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,406 @@
/*
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 "history/view/controls/history_view_webpage_processor.h"
#include "base/unixtime.h"
#include "data/data_chat_participant_status.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
namespace HistoryView::Controls {
WebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d) {
QString resultTitle, resultDescription;
const auto document = d->document;
const auto author = d->author;
const auto siteName = d->siteName;
const auto title = d->title;
const auto description = d->description;
const auto filenameOrUrl = [&] {
return ((document && !document->filename().isEmpty())
? document->filename()
: d->url);
};
const auto authorOrFilename = [&] {
return (author.isEmpty()
? filenameOrUrl()
: author);
};
const auto descriptionOrAuthor = [&] {
return (description.text.isEmpty()
? authorOrFilename()
: description.text);
};
if (siteName.isEmpty()) {
if (title.isEmpty()) {
if (description.text.isEmpty()) {
resultTitle = author;
resultDescription = filenameOrUrl();
} else {
resultTitle = description.text;
resultDescription = authorOrFilename();
}
} else {
resultTitle = title;
resultDescription = descriptionOrAuthor();
}
} else {
resultTitle = siteName;
resultDescription = title.isEmpty()
? descriptionOrAuthor()
: title;
}
return { resultTitle, resultDescription };
}
bool DrawWebPageDataPreview(
QPainter &p,
not_null<WebPageData*> webpage,
not_null<PeerData*> context,
QRect to) {
const auto document = webpage->document;
const auto photo = webpage->photo;
if ((!photo || photo->isNull())
&& (!document
|| !document->hasThumbnail()
|| document->isPatternWallPaper())) {
return false;
}
const auto preview = photo
? photo->getReplyPreview(Data::FileOrigin(), context, false)
: document->getReplyPreview(Data::FileOrigin(), context, false);
if (preview) {
const auto w = preview->width();
const auto h = preview->height();
if (w == h) {
p.drawPixmap(to.x(), to.y(), preview->pix());
} else {
const auto from = (w > h)
? QRect((w - h) / 2, 0, h, h)
: QRect(0, (h - w) / 2, w, w);
p.drawPixmap(to, preview->pix(), from);
}
}
return true;
}
[[nodiscard]] bool ShowWebPagePreview(WebPageData *page) {
return page && !page->failed;
}
WebPageText ProcessWebPageData(WebPageData *page) {
auto previewText = TitleAndDescriptionFromWebPage(page);
if (previewText.title.isEmpty()) {
if (page->document) {
previewText.title = tr::lng_attach_file(tr::now);
} else if (page->photo) {
previewText.title = tr::lng_attach_photo(tr::now);
}
}
return previewText;
}
WebpageResolver::WebpageResolver(not_null<Main::Session*> session)
: _session(session)
, _api(&session->mtp()) {
}
std::optional<WebPageData*> WebpageResolver::lookup(
const QString &link) const {
const auto i = _cache.find(link);
return (i == end(_cache))
? std::optional<WebPageData*>()
: (i->second && !i->second->failed)
? i->second
: nullptr;
}
QString WebpageResolver::find(not_null<WebPageData*> page) const {
for (const auto &[link, cached] : _cache) {
if (cached == page) {
return link;
}
}
return QString();
}
void WebpageResolver::request(const QString &link, bool force) {
if (_requestLink == link && !force) {
return;
}
const auto done = [=](const MTPDmessageMediaWebPage &data) {
const auto page = _session->data().processWebpage(data.vwebpage());
if (page->pendingTill > 0
&& page->pendingTill < base::unixtime::now()) {
page->pendingTill = 0;
page->failed = true;
}
_cache.emplace(link, page->failed ? nullptr : page.get());
_resolved.fire_copy(link);
};
const auto fail = [=] {
_cache.emplace(link, nullptr);
_resolved.fire_copy(link);
};
_requestLink = link;
_requestId = _api.request(
MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(link),
MTPVector<MTPMessageEntity>()
)).done([=](
const MTPmessages_WebPagePreview &result,
mtpRequestId requestId) {
if (_requestId == requestId) {
_requestId = 0;
}
const auto &data = result.data();
_session->data().processUsers(data.vusers());
data.vmedia().match([=](const MTPDmessageMediaWebPage &data) {
done(data);
}, [&](const auto &d) {
fail();
});
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
if (_requestId == requestId) {
_requestId = 0;
}
fail();
}).send();
}
void WebpageResolver::cancel(const QString &link) {
if (_requestLink == link) {
_api.request(base::take(_requestId)).cancel();
}
}
WebpageProcessor::WebpageProcessor(
not_null<History*> history,
not_null<Ui::InputField*> field)
: _history(history)
, _resolver(std::make_shared<WebpageResolver>(&history->session()))
, _parser(field)
, _timer([=] {
if (!ShowWebPagePreview(_data) || _link.isEmpty()) {
return;
}
_resolver->request(_link, true);
}) {
_history->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _data && (_data->document || _data->photo);
}) | rpl::on_next([=] {
_repaintRequests.fire({});
}, _lifetime);
_history->owner().webPageUpdates(
) | rpl::filter([=](not_null<WebPageData*> page) {
return (_data == page.get());
}) | rpl::on_next([=] {
_draft.id = _data->id;
_draft.url = _data->url;
updateFromData();
}, _lifetime);
_parser.list().changes(
) | rpl::on_next([=](QStringList &&parsed) {
_parsedLinks = std::move(parsed);
checkPreview();
}, _lifetime);
_resolver->resolved() | rpl::on_next([=](QString link) {
if (_link != link
|| _draft.removed
|| (_draft.manual && _draft.url != link)) {
return;
}
_data = _resolver->lookup(link).value_or(nullptr);
if (_data) {
_draft.id = _data->id;
_draft.url = _data->url;
updateFromData();
} else {
_links = QStringList();
checkPreview();
}
}, _lifetime);
}
rpl::producer<> WebpageProcessor::repaintRequests() const {
return _repaintRequests.events();
}
Data::WebPageDraft WebpageProcessor::draft() const {
return _draft;
}
std::shared_ptr<WebpageResolver> WebpageProcessor::resolver() const {
return _resolver;
}
const std::vector<MessageLinkRange> &WebpageProcessor::links() const {
return _parser.ranges();
}
QString WebpageProcessor::link() const {
return _link;
}
void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
const auto was = _link;
if (draft.removed) {
_draft = draft;
_parsedLinks = _parser.list().current();
if (_parsedLinks.empty()) {
_draft.removed = false;
}
_data = nullptr;
_links = QStringList();
_link = QString();
_parsed = WebpageParsed();
updateFromData();
} else if (draft.manual && !draft.url.isEmpty()) {
_draft = draft;
_parsedLinks = QStringList();
_links = QStringList();
_link = _draft.url;
const auto page = draft.id
? _history->owner().webpage(draft.id).get()
: nullptr;
if (page && page->url == draft.url) {
_data = page;
if (const auto link = _resolver->find(page); !link.isEmpty()) {
_link = link;
}
updateFromData();
} else {
_resolver->request(_link);
return;
}
} else if (!draft.manual && !_draft.manual) {
_draft = draft;
checkNow(reparse);
}
if (_link != was) {
_resolver->cancel(was);
}
}
void WebpageProcessor::updateFromData() {
_timer.cancel();
auto parsed = WebpageParsed();
if (ShowWebPagePreview(_data)) {
if (const auto till = _data->pendingTill) {
parsed.drawPreview = [](QPainter &p, QRect to) {
return false;
};
parsed.title = tr::lng_preview_loading(tr::now);
parsed.description = _link;
const auto timeout = till - base::unixtime::now();
_timer.callOnce(
std::max(timeout, 0) * crl::time(1000));
} else {
const auto webpage = _data;
const auto context = _history->peer;
const auto preview = ProcessWebPageData(_data);
parsed.title = preview.title;
parsed.description = preview.description;
parsed.drawPreview = [=](QPainter &p, QRect to) {
return DrawWebPageDataPreview(p, webpage, context, to);
};
}
}
_parsed = std::move(parsed);
_repaintRequests.fire({});
}
void WebpageProcessor::setDisabled(bool disabled) {
_parser.setDisabled(disabled);
if (disabled) {
apply({ .removed = true });
} else {
checkNow(false);
}
}
void WebpageProcessor::checkNow(bool force) {
_parser.parseNow();
if (force) {
_link = QString();
_links = QStringList();
if (_parsedLinks.isEmpty()) {
_data = nullptr;
updateFromData();
return;
}
}
checkPreview();
}
void WebpageProcessor::checkPreview() {
const auto previewRestricted = _history->peer
&& _history->peer->amRestricted(ChatRestriction::EmbedLinks);
if (_parsedLinks.empty()) {
_draft.removed = false;
}
if (_draft.removed) {
return;
} else if (previewRestricted) {
apply({ .removed = true });
_draft.removed = false;
return;
} else if (_draft.manual) {
return;
} else if (_links == _parsedLinks) {
return;
}
_links = _parsedLinks;
auto page = (WebPageData*)nullptr;
auto chosen = QString();
for (const auto &link : _links) {
const auto value = _resolver->lookup(link);
if (!value) {
chosen = link;
break;
} else if (*value) {
chosen = link;
page = *value;
break;
}
}
if (_link != chosen) {
_resolver->cancel(_link);
_link = chosen;
if (!page && !_link.isEmpty()) {
_resolver->request(_link);
}
}
if (page) {
_data = page;
_draft.id = _data->id;
_draft.url = _data->url;
} else {
_data = nullptr;
_draft = {};
}
updateFromData();
}
rpl::producer<WebpageParsed> WebpageProcessor::parsedValue() const {
return _parsed.value();
}
} // namespace HistoryView::Controls

View File

@@ -0,0 +1,129 @@
/*
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 "data/data_drafts.h"
#include "chat_helpers/message_field.h"
#include "mtproto/sender.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class InputField;
} // namespace Ui
namespace HistoryView::Controls {
struct WebPageText {
QString title;
QString description;
};
[[nodiscard]] WebPageText TitleAndDescriptionFromWebPage(
not_null<WebPageData*> data);
bool DrawWebPageDataPreview(
QPainter &p,
not_null<WebPageData*> webpage,
not_null<PeerData*> context,
QRect to);
[[nodiscard]] bool ShowWebPagePreview(WebPageData *page);
[[nodiscard]] WebPageText ProcessWebPageData(WebPageData *page);
struct WebpageParsed {
Fn<bool(QPainter &p, QRect to)> drawPreview;
QString title;
QString description;
explicit operator bool() const {
return drawPreview != nullptr;
}
};
class WebpageResolver final {
public:
explicit WebpageResolver(not_null<Main::Session*> session);
[[nodiscard]] std::optional<WebPageData*> lookup(
const QString &link) const;
[[nodiscard]] rpl::producer<QString> resolved() const {
return _resolved.events();
}
[[nodiscard]] QString find(not_null<WebPageData*> page) const;
void request(const QString &link, bool force = false);
void cancel(const QString &link);
private:
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<QString, WebPageData*> _cache;
rpl::event_stream<QString> _resolved;
QString _requestLink;
mtpRequestId _requestId = 0;
};
class WebpageProcessor final {
public:
WebpageProcessor(
not_null<History*> history,
not_null<Ui::InputField*> field);
void setDisabled(bool disabled);
void checkNow(bool force);
// If editing a message without a preview we don't want to show
// parsed preview until links set is changed in the message.
//
// If writing a new message we want to parse links immediately,
// unless preview was removed in the draft or manual.
void apply(Data::WebPageDraft draft, bool reparse = true);
[[nodiscard]] Data::WebPageDraft draft() const;
[[nodiscard]] std::shared_ptr<WebpageResolver> resolver() const;
[[nodiscard]] const std::vector<MessageLinkRange> &links() const;
[[nodiscard]] QString link() const;
[[nodiscard]] rpl::producer<> repaintRequests() const;
[[nodiscard]] rpl::producer<WebpageParsed> parsedValue() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
void updateFromData();
void checkPreview();
const not_null<History*> _history;
const std::shared_ptr<WebpageResolver> _resolver;
MessageLinksParser _parser;
QStringList _parsedLinks;
QStringList _links;
QString _link;
WebPageData *_data = nullptr;
Data::WebPageDraft _draft;
rpl::event_stream<> _repaintRequests;
rpl::variable<WebpageParsed> _parsed;
base::Timer _timer;
rpl::lifetime _lifetime;
};
} // namespace HistoryView::Controls