init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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 ¶ms);
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user