mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 14:17:12 +02:00
Send comments to group stories.
This commit is contained in:
parent
11f0847295
commit
f674ace805
12 changed files with 140 additions and 74 deletions
|
@ -2611,6 +2611,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_broadcast_silent_ph" = "Silent broadcast...";
|
||||
"lng_send_anonymous_ph" = "Send anonymously...";
|
||||
"lng_story_reply_ph" = "Reply privately...";
|
||||
"lng_story_comment_ph" = "Comment story...";
|
||||
"lng_send_text_no" = "Text not allowed.";
|
||||
"lng_send_text_no_about" = "The admins of this group only allow sending {types}.";
|
||||
"lng_send_text_type_and_last" = "{types} and {last}";
|
||||
|
|
|
@ -448,7 +448,7 @@ void Row::paintUserpic(
|
|||
? _cornerBadgeShown
|
||||
: !_cornerBadgeUserpic->layersManager.isDisplayedNone();
|
||||
const auto storiesPeer = peer
|
||||
? ((peer->isUser() || peer->isBroadcast()) ? peer : nullptr)
|
||||
? ((peer->isUser() || peer->isChannel()) ? peer : nullptr)
|
||||
: nullptr;
|
||||
const auto storiesFolder = peer ? nullptr : _id.folder();
|
||||
const auto storiesHas = storiesPeer
|
||||
|
|
|
@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "base/event_filter.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/edit_caption_box.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
|
@ -95,6 +96,7 @@ constexpr auto kMouseEvents = {
|
|||
QEvent::MouseButtonPress,
|
||||
QEvent::MouseButtonRelease
|
||||
};
|
||||
constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200);
|
||||
|
||||
constexpr auto kCommonModifiers = 0
|
||||
| Qt::ShiftModifier
|
||||
|
@ -3343,4 +3345,46 @@ void ComposeControls::checkCharsLimitation() {
|
|||
}
|
||||
}
|
||||
|
||||
rpl::producer<int> SlowmodeSecondsLeft(not_null<PeerData*> peer) {
|
||||
return peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::Slowmode
|
||||
) | rpl::map([=] {
|
||||
return peer->slowmodeSecondsLeft();
|
||||
}) | rpl::map([=](int delay) -> rpl::producer<int> {
|
||||
auto start = rpl::single(delay);
|
||||
if (!delay) {
|
||||
return start;
|
||||
}
|
||||
return std::move(
|
||||
start
|
||||
) | rpl::then(base::timer_each(
|
||||
kRefreshSlowmodeLabelTimeout
|
||||
) | rpl::map([=] {
|
||||
return peer->slowmodeSecondsLeft();
|
||||
}) | rpl::take_while([=](int delay) {
|
||||
return delay > 0;
|
||||
})) | rpl::then(rpl::single(0));
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
rpl::producer<bool> SendDisabledBySlowmode(not_null<PeerData*> peer) {
|
||||
const auto history = peer->owner().history(peer);
|
||||
auto hasSendingMessage = peer->session().changes().historyFlagsValue(
|
||||
history,
|
||||
Data::HistoryUpdate::Flag::ClientSideMessages
|
||||
) | rpl::map([=] {
|
||||
return history->latestSendingMessage() != nullptr;
|
||||
}) | rpl::distinct_until_changed();
|
||||
|
||||
using namespace rpl::mappers;
|
||||
const auto channel = peer->asChannel();
|
||||
return (!channel || channel->amCreator())
|
||||
? (rpl::single(false) | rpl::type_erased())
|
||||
: rpl::combine(
|
||||
channel->slowmodeAppliedValue(),
|
||||
std::move(hasSendingMessage),
|
||||
_1 && _2);
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
||||
|
|
|
@ -439,4 +439,9 @@ private:
|
|||
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<int> SlowmodeSecondsLeft(
|
||||
not_null<PeerData*> peer);
|
||||
[[nodiscard]] rpl::producer<bool> SendDisabledBySlowmode(
|
||||
not_null<PeerData*> peer);
|
||||
|
||||
} // namespace HistoryView
|
||||
|
|
|
@ -99,8 +99,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200);
|
||||
|
||||
rpl::producer<Ui::MessageBarContent> RootViewContent(
|
||||
not_null<History*> history,
|
||||
MsgId rootId,
|
||||
|
@ -631,45 +629,6 @@ bool RepliesWidget::computeAreComments() const {
|
|||
}
|
||||
|
||||
void RepliesWidget::setupComposeControls() {
|
||||
auto slowmodeSecondsLeft = session().changes().peerFlagsValue(
|
||||
_history->peer,
|
||||
Data::PeerUpdate::Flag::Slowmode
|
||||
) | rpl::map([=] {
|
||||
return _history->peer->slowmodeSecondsLeft();
|
||||
}) | rpl::map([=](int delay) -> rpl::producer<int> {
|
||||
auto start = rpl::single(delay);
|
||||
if (!delay) {
|
||||
return start;
|
||||
}
|
||||
return std::move(
|
||||
start
|
||||
) | rpl::then(base::timer_each(
|
||||
kRefreshSlowmodeLabelTimeout
|
||||
) | rpl::map([=] {
|
||||
return _history->peer->slowmodeSecondsLeft();
|
||||
}) | rpl::take_while([=](int delay) {
|
||||
return delay > 0;
|
||||
})) | rpl::then(rpl::single(0));
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
const auto channel = _history->peer->asChannel();
|
||||
Assert(channel != nullptr);
|
||||
|
||||
auto hasSendingMessage = session().changes().historyFlagsValue(
|
||||
_history,
|
||||
Data::HistoryUpdate::Flag::ClientSideMessages
|
||||
) | rpl::map([=] {
|
||||
return _history->latestSendingMessage() != nullptr;
|
||||
}) | rpl::distinct_until_changed();
|
||||
|
||||
using namespace rpl::mappers;
|
||||
auto sendDisabledBySlowmode = (!channel || channel->amCreator())
|
||||
? (rpl::single(false) | rpl::type_erased())
|
||||
: rpl::combine(
|
||||
channel->slowmodeAppliedValue(),
|
||||
std::move(hasSendingMessage),
|
||||
_1 && _2);
|
||||
|
||||
auto topicWriteRestrictions = rpl::single(
|
||||
) | rpl::then(session().changes().topicUpdates(
|
||||
Data::TopicUpdate::Flag::Closed
|
||||
|
@ -719,8 +678,8 @@ void RepliesWidget::setupComposeControls() {
|
|||
.topicRootId = _topic ? _topic->rootId() : MsgId(0),
|
||||
.showSlowmodeError = [=] { return showSlowmodeError(); },
|
||||
.sendActionFactory = [=] { return prepareSendAction({}); },
|
||||
.slowmodeSecondsLeft = std::move(slowmodeSecondsLeft),
|
||||
.sendDisabledBySlowmode = std::move(sendDisabledBySlowmode),
|
||||
.slowmodeSecondsLeft = SlowmodeSecondsLeft(_history->peer),
|
||||
.sendDisabledBySlowmode = SendDisabledBySlowmode(_history->peer),
|
||||
.writeRestriction = std::move(writeRestriction),
|
||||
});
|
||||
|
||||
|
|
|
@ -299,7 +299,9 @@ Controller::Controller(not_null<Delegate*> delegate)
|
|||
|
||||
_reactions->chosen(
|
||||
) | rpl::start_with_next([=](Reactions::Chosen chosen) {
|
||||
reactionChosen(chosen.mode, chosen.reaction);
|
||||
if (reactionChosen(chosen.mode, chosen.reaction)) {
|
||||
_reactions->animateAndProcess(std::move(chosen));
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
_delegate->storiesLayerShown(
|
||||
|
@ -628,13 +630,15 @@ void Controller::toggleLiked() {
|
|||
_reactions->toggleLiked();
|
||||
}
|
||||
|
||||
void Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {
|
||||
bool Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {
|
||||
auto result = true;
|
||||
if (mode == ReactionsMode::Message) {
|
||||
_replyArea->sendReaction(chosen.id);
|
||||
result = _replyArea->sendReaction(chosen.id);
|
||||
} else if (const auto peer = shownPeer()) {
|
||||
peer->owner().stories().sendReaction(_shown, chosen.id);
|
||||
}
|
||||
unfocusReply();
|
||||
return result;
|
||||
}
|
||||
|
||||
void Controller::showFullCaption() {
|
||||
|
@ -907,7 +911,7 @@ void Controller::show(
|
|||
.views = story->views(),
|
||||
.total = story->interactions(),
|
||||
.type = RecentViewsTypeFor(peer),
|
||||
.canViewReactions = CanViewReactionsFor(peer),
|
||||
.canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(),
|
||||
}, _reactions->likedValue());
|
||||
if (const auto nowLikeButton = _recentViews->likeButton()) {
|
||||
if (wasLikeButton != nowLikeButton) {
|
||||
|
@ -1007,7 +1011,7 @@ void Controller::subscribeToSession() {
|
|||
.views = update.story->views(),
|
||||
.total = update.story->interactions(),
|
||||
.type = RecentViewsTypeFor(peer),
|
||||
.canViewReactions = CanViewReactionsFor(peer),
|
||||
.canViewReactions = CanViewReactionsFor(peer) && !peer->isMegagroup(),
|
||||
});
|
||||
updateAreas(update.story);
|
||||
}
|
||||
|
|
|
@ -260,7 +260,7 @@ private:
|
|||
|
||||
[[nodiscard]] int repostSkipTop() const;
|
||||
void updateAreas(Data::Story *story);
|
||||
void reactionChosen(ReactionsMode mode, ChosenReaction chosen);
|
||||
bool reactionChosen(ReactionsMode mode, ChosenReaction chosen);
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
|
||||
|
|
|
@ -793,7 +793,7 @@ Reactions::Reactions(not_null<Controller*> controller)
|
|||
: _controller(controller)
|
||||
, _panel(std::make_unique<Panel>(_controller)) {
|
||||
_panel->chosen() | rpl::start_with_next([=](Chosen &&chosen) {
|
||||
animateAndProcess(std::move(chosen));
|
||||
_chosen.fire(std::move(chosen));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
|
@ -887,7 +887,7 @@ auto Reactions::attachToMenu(
|
|||
|
||||
selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) {
|
||||
menu->hideMenu();
|
||||
animateAndProcess({ reaction, ReactionsMode::Reaction });
|
||||
_chosen.fire({ reaction, ReactionsMode::Reaction });
|
||||
}, selector->lifetime());
|
||||
|
||||
return AttachSelectorResult::Attached;
|
||||
|
@ -933,7 +933,7 @@ void Reactions::toggleLiked() {
|
|||
|
||||
void Reactions::applyLike(Data::ReactionId id) {
|
||||
if (_liked.current() != id) {
|
||||
animateAndProcess({ { .id = id }, ReactionsMode::Reaction });
|
||||
_chosen.fire({ { .id = id }, ReactionsMode::Reaction });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -971,8 +971,6 @@ void Reactions::animateAndProcess(Chosen &&chosen) {
|
|||
.scaleOutTarget = scaleOutTarget,
|
||||
}, target, std::move(done));
|
||||
}
|
||||
|
||||
_chosen.fire(std::move(chosen));
|
||||
}
|
||||
|
||||
void Reactions::assignLikedId(Data::ReactionId id) {
|
||||
|
|
|
@ -87,6 +87,8 @@ public:
|
|||
void attachToReactionButton(not_null<Ui::RpWidget*> button);
|
||||
void setReactionIconWidget(Ui::RpWidget *widget);
|
||||
|
||||
void animateAndProcess(Chosen &&chosen);
|
||||
|
||||
using AttachStripResult = HistoryView::Reactions::AttachSelectorResult;
|
||||
[[nodiscard]] AttachStripResult attachToMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
|
@ -95,8 +97,6 @@ public:
|
|||
private:
|
||||
class Panel;
|
||||
|
||||
void animateAndProcess(Chosen &&chosen);
|
||||
|
||||
void assignLikedId(Data::ReactionId id);
|
||||
[[nodiscard]] Fn<void(Ui::ReactionFlyCenter)> setLikedIdIconInit(
|
||||
not_null<Data::Session*> owner,
|
||||
|
|
|
@ -139,7 +139,7 @@ constexpr auto kLoadViewsPages = 2;
|
|||
RecentViewsType RecentViewsTypeFor(not_null<PeerData*> peer) {
|
||||
return peer->isSelf()
|
||||
? RecentViewsType::Self
|
||||
: peer->isChannel()
|
||||
: peer->isBroadcast()
|
||||
? RecentViewsType::Channel
|
||||
: peer->isServiceUser()
|
||||
? RecentViewsType::Changelog
|
||||
|
|
|
@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "storage/storage_account.h"
|
||||
#include "storage/storage_media_prepare.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "styles/style_boxes.h" // sendMediaPreviewSize.
|
||||
|
@ -49,11 +50,15 @@ namespace Media::Stories {
|
|||
namespace {
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> PlaceholderText(
|
||||
const std::shared_ptr<ChatHelpers::Show> &show) {
|
||||
return show->session().data().stories().stealthModeValue(
|
||||
) | rpl::map([](Data::StealthMode value) {
|
||||
return value.enabledTill;
|
||||
}) | rpl::distinct_until_changed() | rpl::map([](TimeId till) {
|
||||
const std::shared_ptr<ChatHelpers::Show> &show,
|
||||
rpl::producer<bool> isComment) {
|
||||
return rpl::combine(
|
||||
show->session().data().stories().stealthModeValue(),
|
||||
std::move(isComment)
|
||||
) | rpl::map([](Data::StealthMode value, bool isComment) {
|
||||
return std::tuple(value.enabledTill, isComment);
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::map([](TimeId till, bool isComment) {
|
||||
return rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(
|
||||
|
@ -64,11 +69,13 @@ namespace {
|
|||
return left > 0;
|
||||
}) | rpl::then(
|
||||
rpl::single(0)
|
||||
) | rpl::map([](TimeId left) {
|
||||
) | rpl::map([=](TimeId left) {
|
||||
return left
|
||||
? tr::lng_stealth_mode_countdown(
|
||||
lt_left,
|
||||
rpl::single(TimeLeftText(left)))
|
||||
: isComment
|
||||
? tr::lng_story_comment_ph()
|
||||
: tr::lng_story_reply_ph();
|
||||
}) | rpl::flatten_latest();
|
||||
}) | rpl::flatten_latest();
|
||||
|
@ -118,7 +125,9 @@ ReplyArea::ReplyArea(not_null<Controller*> controller)
|
|||
.mode = HistoryView::ComposeControlsMode::Normal,
|
||||
.sendMenuType = SendMenu::Type::SilentOnly,
|
||||
.stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(),
|
||||
.customPlaceholder = PlaceholderText(_controller->uiShow()),
|
||||
.customPlaceholder = PlaceholderText(
|
||||
_controller->uiShow(),
|
||||
rpl::deferred([=] { return _isComment.value(); })),
|
||||
.voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now),
|
||||
.voiceLockFromBottom = true,
|
||||
.features = {
|
||||
|
@ -171,7 +180,7 @@ void ReplyArea::initGeometry() {
|
|||
}, _lifetime);
|
||||
}
|
||||
|
||||
void ReplyArea::sendReaction(const Data::ReactionId &id) {
|
||||
bool ReplyArea::sendReaction(const Data::ReactionId &id) {
|
||||
Expects(_data.peer != nullptr);
|
||||
|
||||
auto message = Api::MessageToSend(prepareSendAction({}));
|
||||
|
@ -188,9 +197,8 @@ void ReplyArea::sendReaction(const Data::ReactionId &id) {
|
|||
};
|
||||
}
|
||||
}
|
||||
if (!message.textWithTags.empty()) {
|
||||
send(std::move(message), {}, true);
|
||||
}
|
||||
return !message.textWithTags.empty()
|
||||
&& send(std::move(message), {}, true);
|
||||
}
|
||||
|
||||
void ReplyArea::send(Api::SendOptions options) {
|
||||
|
@ -203,10 +211,14 @@ void ReplyArea::send(Api::SendOptions options) {
|
|||
send(std::move(message), options);
|
||||
}
|
||||
|
||||
void ReplyArea::send(
|
||||
bool ReplyArea::send(
|
||||
Api::MessageToSend message,
|
||||
Api::SendOptions options,
|
||||
bool skipToast) {
|
||||
if (!options.scheduled && showSlowmodeError()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto error = GetErrorTextForSending(
|
||||
_data.peer,
|
||||
{
|
||||
|
@ -216,12 +228,14 @@ void ReplyArea::send(
|
|||
});
|
||||
if (!error.isEmpty()) {
|
||||
_controller->uiShow()->showToast(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
session().api().sendMessage(std::move(message));
|
||||
|
||||
finishSending(skipToast);
|
||||
_controls->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReplyArea::sendVoice(VoiceToSend &&data) {
|
||||
|
@ -249,7 +263,8 @@ bool ReplyArea::sendExistingDocument(
|
|||
if (error) {
|
||||
show->showToast(*error);
|
||||
return false;
|
||||
} else if (Window::ShowSendPremiumError(show, document)) {
|
||||
} else if (showSlowmodeError()
|
||||
|| Window::ShowSendPremiumError(show, document)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -279,6 +294,8 @@ bool ReplyArea::sendExistingPhoto(
|
|||
if (error) {
|
||||
show->showToast(*error);
|
||||
return false;
|
||||
} else if (showSlowmodeError()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Api::SendExistingPhoto(
|
||||
|
@ -409,6 +426,8 @@ void ReplyArea::chooseAttach(
|
|||
if (const auto error = Data::AnyFileRestrictionError(peer)) {
|
||||
_controller->uiShow()->showToast(*error);
|
||||
return;
|
||||
} else if (showSlowmodeError()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto filter = (overrideSendImagesAsPhotos == true)
|
||||
|
@ -666,6 +685,7 @@ void ReplyArea::show(
|
|||
const auto peer = data.peer;
|
||||
const auto history = peer ? peer->owner().history(peer).get() : nullptr;
|
||||
const auto user = peer->asUser();
|
||||
_isComment = peer->isMegagroup();
|
||||
auto writeRestriction = Data::CanSendAnythingValue(
|
||||
peer
|
||||
) | rpl::map([=](bool can) {
|
||||
|
@ -681,8 +701,13 @@ void ReplyArea::show(
|
|||
.type = WriteRestrictionType::PremiumRequired,
|
||||
};
|
||||
});
|
||||
using namespace HistoryView;
|
||||
_controls->setHistory({
|
||||
.history = history,
|
||||
.showSlowmodeError = [=] { return showSlowmodeError(); },
|
||||
.sendActionFactory = [=] { return prepareSendAction({}); },
|
||||
.slowmodeSecondsLeft = SlowmodeSecondsLeft(history->peer),
|
||||
.sendDisabledBySlowmode = SendDisabledBySlowmode(history->peer),
|
||||
.liked = std::move(
|
||||
likedValue
|
||||
) | rpl::map([](const Data::ReactionId &id) {
|
||||
|
@ -692,7 +717,7 @@ void ReplyArea::show(
|
|||
});
|
||||
_controls->clear();
|
||||
const auto hidden = peer
|
||||
&& (!peer->isUser() || peer->isSelf() || peer->isServiceUser());
|
||||
&& (peer->isBroadcast() || peer->isSelf() || peer->isServiceUser());
|
||||
const auto cant = !peer;
|
||||
if (!hidden && !cant) {
|
||||
_controls->show();
|
||||
|
@ -714,6 +739,33 @@ void ReplyArea::show(
|
|||
}
|
||||
}
|
||||
|
||||
bool ReplyArea::showSlowmodeError() {
|
||||
const auto text = [&] {
|
||||
const auto story = _controller->story();
|
||||
if (!story) {
|
||||
return QString();
|
||||
}
|
||||
const auto peer = story->peer();
|
||||
if (const auto left = peer->slowmodeSecondsLeft()) {
|
||||
return tr::lng_slowmode_enabled(
|
||||
tr::now,
|
||||
lt_left,
|
||||
Ui::FormatDurationWordsSlowmode(left));
|
||||
} else if (peer->slowmodeApplied()) {
|
||||
const auto history = peer->owner().history(peer);
|
||||
if (const auto item = history->latestSendingMessage()) {
|
||||
return tr::lng_slowmode_no_many(tr::now);
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}();
|
||||
if (text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
_controller->uiShow()->showToast(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
Main::Session &ReplyArea::session() const {
|
||||
Expects(_data.peer != nullptr);
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ public:
|
|||
void show(
|
||||
ReplyAreaData data,
|
||||
rpl::producer<Data::ReactionId> likedValue);
|
||||
void sendReaction(const Data::ReactionId &id);
|
||||
bool sendReaction(const Data::ReactionId &id);
|
||||
|
||||
[[nodiscard]] bool focused() const;
|
||||
[[nodiscard]] rpl::producer<bool> focusedValue() const;
|
||||
|
@ -84,7 +84,7 @@ private:
|
|||
[[nodiscard]] Main::Session &session() const;
|
||||
[[nodiscard]] not_null<History*> history() const;
|
||||
|
||||
void send(
|
||||
bool send(
|
||||
Api::MessageToSend message,
|
||||
Api::SendOptions options,
|
||||
bool skipToast = false);
|
||||
|
@ -142,8 +142,11 @@ private:
|
|||
void chooseAttach(std::optional<bool> overrideSendImagesAsPhotos);
|
||||
|
||||
void showPremiumToast(not_null<DocumentData*> emoji);
|
||||
[[nodiscard]] bool showSlowmodeError();
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
rpl::variable<bool> _isComment;
|
||||
|
||||
const std::unique_ptr<HistoryView::ComposeControls> _controls;
|
||||
std::unique_ptr<Cant> _cant;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue