mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-02-26 18:41:01 +00:00
forward dialog send file dialog edit caption dialog notification replay schedule messages new channel dialog group description edit dialog create poll dialog rate call dialog report bot dialog support mode
621 lines
16 KiB
C++
621 lines
16 KiB
C++
/*
|
|
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 "support/support_helper.h"
|
|
|
|
#include "dialogs/dialogs_key.h"
|
|
#include "data/data_drafts.h"
|
|
#include "data/data_user.h"
|
|
#include "data/data_session.h"
|
|
#include "api/api_text_entities.h"
|
|
#include "history/history.h"
|
|
#include "boxes/abstract_box.h"
|
|
#include "ui/toast/toast.h"
|
|
#include "ui/widgets/input_fields.h"
|
|
#include "ui/text/text_entity.h"
|
|
#include "ui/text_options.h"
|
|
#include "chat_helpers/message_field.h"
|
|
#include "chat_helpers/emoji_suggestions_widget.h"
|
|
#include "base/unixtime.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "storage/storage_media_prepare.h"
|
|
#include "storage/localimageloader.h"
|
|
#include "core/sandbox.h"
|
|
#include "main/main_session.h"
|
|
#include "observer_peer.h"
|
|
#include "apiwrap.h"
|
|
#include "facades.h"
|
|
#include "styles/style_layers.h"
|
|
#include "styles/style_boxes.h"
|
|
|
|
namespace Main {
|
|
class Session;
|
|
} // namespace Main
|
|
|
|
namespace Support {
|
|
namespace {
|
|
|
|
constexpr auto kOccupyFor = TimeId(60);
|
|
constexpr auto kReoccupyEach = 30 * crl::time(1000);
|
|
constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4;
|
|
|
|
class EditInfoBox : public Ui::BoxContent {
|
|
public:
|
|
EditInfoBox(
|
|
QWidget*,
|
|
not_null<Main::Session*> session,
|
|
const TextWithTags &text,
|
|
Fn<void(TextWithTags, Fn<void(bool success)>)> submit);
|
|
|
|
protected:
|
|
void prepare() override;
|
|
void setInnerFocus() override;
|
|
|
|
private:
|
|
not_null<Main::Session*> _session;
|
|
object_ptr<Ui::InputField> _field = { nullptr };
|
|
Fn<void(TextWithTags, Fn<void(bool success)>)> _submit;
|
|
|
|
};
|
|
|
|
EditInfoBox::EditInfoBox(
|
|
QWidget*,
|
|
not_null<Main::Session*> session,
|
|
const TextWithTags &text,
|
|
Fn<void(TextWithTags, Fn<void(bool success)>)> submit)
|
|
: _session(session)
|
|
, _field(
|
|
this,
|
|
st::supportInfoField,
|
|
Ui::InputField::Mode::MultiLine,
|
|
rpl::single(qsl("Support information")), // #TODO hard_lang
|
|
text)
|
|
, _submit(std::move(submit)) {
|
|
_field->setMaxLength(kMaxSupportInfoLength);
|
|
_field->setSubmitSettings(session->settings().sendSubmitWay());
|
|
_field->setInstantReplaces(Ui::InstantReplaces::Default());
|
|
_field->setInstantReplacesEnabled(
|
|
session->settings().replaceEmojiValue());
|
|
_field->setMarkdownReplacesEnabled(rpl::single(true));
|
|
_field->setEditLinkCallback(DefaultEditLinkCallback(session, _field));
|
|
}
|
|
|
|
void EditInfoBox::prepare() {
|
|
setTitle(rpl::single(qsl("Edit support information"))); // #TODO hard_lang
|
|
|
|
const auto save = [=] {
|
|
const auto done = crl::guard(this, [=](bool success) {
|
|
if (success) {
|
|
closeBox();
|
|
} else {
|
|
_field->showError();
|
|
}
|
|
});
|
|
_submit(_field->getTextWithAppliedMarkdown(), done);
|
|
};
|
|
addButton(tr::lng_settings_save(), save);
|
|
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
|
|
|
connect(_field, &Ui::InputField::submitted, save);
|
|
connect(_field, &Ui::InputField::cancelled, [=] { closeBox(); });
|
|
Ui::Emoji::SuggestionsController::Init(
|
|
getDelegate()->outerContainer(),
|
|
_field,
|
|
_session);
|
|
|
|
auto cursor = _field->textCursor();
|
|
cursor.movePosition(QTextCursor::End);
|
|
_field->setTextCursor(cursor);
|
|
|
|
widthValue(
|
|
) | rpl::start_with_next([=](int width) {
|
|
_field->resizeToWidth(
|
|
width - st::boxPadding.left() - st::boxPadding.right());
|
|
_field->moveToLeft(st::boxPadding.left(), st::boxPadding.bottom());
|
|
}, _field->lifetime());
|
|
|
|
_field->heightValue(
|
|
) | rpl::start_with_next([=](int height) {
|
|
setDimensions(
|
|
st::boxWideWidth,
|
|
st::boxPadding.bottom() + height + st::boxPadding.bottom());
|
|
}, _field->lifetime());
|
|
}
|
|
|
|
void EditInfoBox::setInnerFocus() {
|
|
_field->setFocusFast();
|
|
}
|
|
|
|
QString FormatDateTime(TimeId value) {
|
|
const auto now = QDateTime::currentDateTime();
|
|
const auto date = base::unixtime::parse(value);
|
|
if (date.date() == now.date()) {
|
|
return tr::lng_mediaview_today(
|
|
tr::now,
|
|
lt_time,
|
|
date.time().toString(cTimeFormat()));
|
|
} else if (date.date().addDays(1) == now.date()) {
|
|
return tr::lng_mediaview_yesterday(
|
|
tr::now,
|
|
lt_time,
|
|
date.time().toString(cTimeFormat()));
|
|
} else {
|
|
return tr::lng_mediaview_date_time(
|
|
tr::now,
|
|
lt_date,
|
|
date.date().toString(qsl("dd.MM.yy")),
|
|
lt_time,
|
|
date.time().toString(cTimeFormat()));
|
|
}
|
|
}
|
|
|
|
uint32 OccupationTag() {
|
|
return uint32(Core::Sandbox::Instance().installationTag() & 0xFFFFFFFF);
|
|
}
|
|
|
|
QString NormalizeName(QString name) {
|
|
return name.replace(':', '_').replace(';', '_');
|
|
}
|
|
|
|
Data::Draft OccupiedDraft(const QString &normalizedName) {
|
|
const auto now = base::unixtime::now(), till = now + kOccupyFor;
|
|
return {
|
|
TextWithTags{ "t:"
|
|
+ QString::number(till)
|
|
+ ";u:"
|
|
+ QString::number(OccupationTag())
|
|
+ ";n:"
|
|
+ normalizedName },
|
|
MsgId(0),
|
|
MessageCursor(),
|
|
false
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] bool TrackHistoryOccupation(History *history) {
|
|
if (!history) {
|
|
return false;
|
|
} else if (const auto user = history->peer->asUser()) {
|
|
return !user->isBot();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
uint32 ParseOccupationTag(History *history) {
|
|
if (!TrackHistoryOccupation(history)) {
|
|
return 0;
|
|
}
|
|
const auto draft = history->cloudDraft();
|
|
if (!draft) {
|
|
return 0;
|
|
}
|
|
const auto &text = draft->textWithTags.text;
|
|
#ifndef OS_MAC_OLD
|
|
const auto parts = text.splitRef(';');
|
|
#else // OS_MAC_OLD
|
|
const auto parts = text.split(';');
|
|
#endif // OS_MAC_OLD
|
|
auto valid = false;
|
|
auto result = uint32();
|
|
for (const auto &part : parts) {
|
|
if (part.startsWith(qstr("t:"))) {
|
|
if (part.mid(2).toInt() >= base::unixtime::now()) {
|
|
valid = true;
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (part.startsWith(qstr("u:"))) {
|
|
result = part.mid(2).toUInt();
|
|
}
|
|
}
|
|
return valid ? result : 0;
|
|
}
|
|
|
|
QString ParseOccupationName(History *history) {
|
|
if (!TrackHistoryOccupation(history)) {
|
|
return QString();
|
|
}
|
|
const auto draft = history->cloudDraft();
|
|
if (!draft) {
|
|
return QString();
|
|
}
|
|
const auto &text = draft->textWithTags.text;
|
|
#ifndef OS_MAC_OLD
|
|
const auto parts = text.splitRef(';');
|
|
#else // OS_MAC_OLD
|
|
const auto parts = text.split(';');
|
|
#endif // OS_MAC_OLD
|
|
auto valid = false;
|
|
auto result = QString();
|
|
for (const auto &part : parts) {
|
|
if (part.startsWith(qstr("t:"))) {
|
|
if (part.mid(2).toInt() >= base::unixtime::now()) {
|
|
valid = true;
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (part.startsWith(qstr("n:"))) {
|
|
#ifndef OS_MAC_OLD
|
|
result = part.mid(2).toString();
|
|
#else // OS_MAC_OLD
|
|
result = part.mid(2);
|
|
#endif // OS_MAC_OLD
|
|
}
|
|
}
|
|
return valid ? result : QString();
|
|
}
|
|
|
|
TimeId OccupiedBySomeoneTill(History *history) {
|
|
if (!TrackHistoryOccupation(history)) {
|
|
return 0;
|
|
}
|
|
const auto draft = history->cloudDraft();
|
|
if (!draft) {
|
|
return 0;
|
|
}
|
|
const auto &text = draft->textWithTags.text;
|
|
#ifndef OS_MAC_OLD
|
|
const auto parts = text.splitRef(';');
|
|
#else // OS_MAC_OLD
|
|
const auto parts = text.split(';');
|
|
#endif // OS_MAC_OLD
|
|
auto valid = false;
|
|
auto result = TimeId();
|
|
for (const auto &part : parts) {
|
|
if (part.startsWith(qstr("t:"))) {
|
|
if (part.mid(2).toInt() >= base::unixtime::now()) {
|
|
result = part.mid(2).toInt();
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (part.startsWith(qstr("u:"))) {
|
|
if (part.mid(2).toUInt() != OccupationTag()) {
|
|
valid = true;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
return valid ? result : 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Helper::Helper(not_null<Main::Session*> session)
|
|
: _session(session)
|
|
, _api(_session->api().instance())
|
|
, _templates(_session)
|
|
, _reoccupyTimer([=] { reoccupy(); })
|
|
, _checkOccupiedTimer([=] { checkOccupiedChats(); }) {
|
|
_api.request(MTPhelp_GetSupportName(
|
|
)).done([=](const MTPhelp_SupportName &result) {
|
|
result.match([&](const MTPDhelp_supportName &data) {
|
|
setSupportName(qs(data.vname()));
|
|
});
|
|
}).fail([=](const RPCError &error) {
|
|
setSupportName(
|
|
qsl("[rand^")
|
|
+ QString::number(Core::Sandbox::Instance().installationTag())
|
|
+ ']');
|
|
}).send();
|
|
}
|
|
|
|
std::unique_ptr<Helper> Helper::Create(not_null<Main::Session*> session) {
|
|
//return std::make_unique<Helper>(session); AssertIsDebug();
|
|
const auto valid = session->user()->phone().startsWith(qstr("424"));
|
|
return valid ? std::make_unique<Helper>(session) : nullptr;
|
|
}
|
|
|
|
void Helper::registerWindow(not_null<Window::SessionController*> controller) {
|
|
controller->activeChatValue(
|
|
) | rpl::map([](Dialogs::Key key) {
|
|
const auto history = key.history();
|
|
return TrackHistoryOccupation(history) ? history : nullptr;
|
|
}) | rpl::distinct_until_changed(
|
|
) | rpl::start_with_next([=](History *history) {
|
|
updateOccupiedHistory(controller, history);
|
|
}, controller->lifetime());
|
|
}
|
|
|
|
void Helper::cloudDraftChanged(not_null<History*> history) {
|
|
chatOccupiedUpdated(history);
|
|
if (history != _occupiedHistory) {
|
|
return;
|
|
}
|
|
occupyIfNotYet();
|
|
}
|
|
|
|
void Helper::chatOccupiedUpdated(not_null<History*> history) {
|
|
if (const auto till = OccupiedBySomeoneTill(history)) {
|
|
_occupiedChats[history] = till + 2;
|
|
Notify::peerUpdatedDelayed(
|
|
history->peer,
|
|
Notify::PeerUpdate::Flag::UserOccupiedChanged);
|
|
checkOccupiedChats();
|
|
} else if (_occupiedChats.take(history)) {
|
|
Notify::peerUpdatedDelayed(
|
|
history->peer,
|
|
Notify::PeerUpdate::Flag::UserOccupiedChanged);
|
|
}
|
|
}
|
|
|
|
void Helper::checkOccupiedChats() {
|
|
const auto now = base::unixtime::now();
|
|
while (!_occupiedChats.empty()) {
|
|
const auto nearest = ranges::min_element(
|
|
_occupiedChats,
|
|
std::less<>(),
|
|
[](const auto &pair) { return pair.second; });
|
|
if (nearest->second <= now) {
|
|
const auto history = nearest->first;
|
|
_occupiedChats.erase(nearest);
|
|
Notify::peerUpdatedDelayed(
|
|
history->peer,
|
|
Notify::PeerUpdate::Flag::UserOccupiedChanged);
|
|
} else {
|
|
_checkOccupiedTimer.callOnce(
|
|
(nearest->second - now) * crl::time(1000));
|
|
return;
|
|
}
|
|
}
|
|
_checkOccupiedTimer.cancel();
|
|
}
|
|
|
|
void Helper::updateOccupiedHistory(
|
|
not_null<Window::SessionController*> controller,
|
|
History *history) {
|
|
if (isOccupiedByMe(_occupiedHistory)) {
|
|
_occupiedHistory->clearCloudDraft();
|
|
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
|
|
}
|
|
_occupiedHistory = history;
|
|
occupyInDraft();
|
|
}
|
|
|
|
void Helper::setSupportName(const QString &name) {
|
|
_supportName = name;
|
|
_supportNameNormalized = NormalizeName(name);
|
|
occupyIfNotYet();
|
|
}
|
|
|
|
void Helper::occupyIfNotYet() {
|
|
if (!isOccupiedByMe(_occupiedHistory)) {
|
|
occupyInDraft();
|
|
}
|
|
}
|
|
|
|
void Helper::occupyInDraft() {
|
|
if (_occupiedHistory
|
|
&& !isOccupiedBySomeone(_occupiedHistory)
|
|
&& !_supportName.isEmpty()) {
|
|
const auto draft = OccupiedDraft(_supportNameNormalized);
|
|
_occupiedHistory->createCloudDraft(&draft);
|
|
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
|
|
_reoccupyTimer.callEach(kReoccupyEach);
|
|
}
|
|
}
|
|
|
|
void Helper::reoccupy() {
|
|
if (isOccupiedByMe(_occupiedHistory)) {
|
|
const auto draft = OccupiedDraft(_supportNameNormalized);
|
|
_occupiedHistory->createCloudDraft(&draft);
|
|
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
|
|
}
|
|
}
|
|
|
|
bool Helper::isOccupiedByMe(History *history) const {
|
|
if (const auto tag = ParseOccupationTag(history)) {
|
|
return (tag == OccupationTag());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Helper::isOccupiedBySomeone(History *history) const {
|
|
if (const auto tag = ParseOccupationTag(history)) {
|
|
return (tag != OccupationTag());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Helper::refreshInfo(not_null<UserData*> user) {
|
|
_api.request(MTPhelp_GetUserInfo(
|
|
user->inputUser
|
|
)).done([=](const MTPhelp_UserInfo &result) {
|
|
applyInfo(user, result);
|
|
if (_userInfoEditPending.contains(user)) {
|
|
_userInfoEditPending.erase(user);
|
|
showEditInfoBox(user);
|
|
}
|
|
}).send();
|
|
}
|
|
|
|
void Helper::applyInfo(
|
|
not_null<UserData*> user,
|
|
const MTPhelp_UserInfo &result) {
|
|
const auto notify = [&] {
|
|
Notify::peerUpdatedDelayed(
|
|
user,
|
|
Notify::PeerUpdate::Flag::UserSupportInfoChanged);
|
|
};
|
|
const auto remove = [&] {
|
|
if (_userInformation.take(user)) {
|
|
notify();
|
|
}
|
|
};
|
|
result.match([&](const MTPDhelp_userInfo &data) {
|
|
auto info = UserInfo();
|
|
info.author = qs(data.vauthor());
|
|
info.date = data.vdate().v;
|
|
info.text = TextWithEntities{
|
|
qs(data.vmessage()),
|
|
Api::EntitiesFromMTP(data.ventities().v) };
|
|
if (info.text.empty()) {
|
|
remove();
|
|
} else if (_userInformation[user] != info) {
|
|
_userInformation[user] = info;
|
|
notify();
|
|
}
|
|
}, [&](const MTPDhelp_userInfoEmpty &) {
|
|
remove();
|
|
});
|
|
}
|
|
|
|
rpl::producer<UserInfo> Helper::infoValue(not_null<UserData*> user) const {
|
|
return Notify::PeerUpdateValue(
|
|
user,
|
|
Notify::PeerUpdate::Flag::UserSupportInfoChanged
|
|
) | rpl::map([=] {
|
|
return infoCurrent(user);
|
|
});
|
|
}
|
|
|
|
rpl::producer<QString> Helper::infoLabelValue(
|
|
not_null<UserData*> user) const {
|
|
return infoValue(
|
|
user
|
|
) | rpl::map([](const Support::UserInfo &info) {
|
|
return info.author + ", " + FormatDateTime(info.date);
|
|
});
|
|
}
|
|
|
|
rpl::producer<TextWithEntities> Helper::infoTextValue(
|
|
not_null<UserData*> user) const {
|
|
return infoValue(
|
|
user
|
|
) | rpl::map([](const Support::UserInfo &info) {
|
|
return info.text;
|
|
});
|
|
}
|
|
|
|
UserInfo Helper::infoCurrent(not_null<UserData*> user) const {
|
|
const auto i = _userInformation.find(user);
|
|
return (i != end(_userInformation)) ? i->second : UserInfo();
|
|
}
|
|
|
|
void Helper::editInfo(not_null<UserData*> user) {
|
|
if (!_userInfoEditPending.contains(user)) {
|
|
_userInfoEditPending.emplace(user);
|
|
refreshInfo(user);
|
|
}
|
|
}
|
|
|
|
void Helper::showEditInfoBox(not_null<UserData*> user) {
|
|
const auto info = infoCurrent(user);
|
|
const auto editData = TextWithTags{
|
|
info.text.text,
|
|
TextUtilities::ConvertEntitiesToTextTags(info.text.entities)
|
|
};
|
|
|
|
const auto save = [=](TextWithTags result, Fn<void(bool)> done) {
|
|
saveInfo(user, TextWithEntities{
|
|
result.text,
|
|
TextUtilities::ConvertTextTagsToEntities(result.tags)
|
|
}, done);
|
|
};
|
|
Ui::show(
|
|
Box<EditInfoBox>(&user->session(), editData, save),
|
|
Ui::LayerOption::KeepOther);
|
|
}
|
|
|
|
void Helper::saveInfo(
|
|
not_null<UserData*> user,
|
|
TextWithEntities text,
|
|
Fn<void(bool success)> done) {
|
|
const auto i = _userInfoSaving.find(user);
|
|
if (i != end(_userInfoSaving)) {
|
|
if (i->second.data == text) {
|
|
return;
|
|
} else {
|
|
i->second.data = text;
|
|
_api.request(base::take(i->second.requestId)).cancel();
|
|
}
|
|
} else {
|
|
_userInfoSaving.emplace(user, SavingInfo{ text });
|
|
}
|
|
|
|
TextUtilities::PrepareForSending(
|
|
text,
|
|
Ui::ItemTextDefaultOptions().flags);
|
|
TextUtilities::Trim(text);
|
|
|
|
const auto entities = Api::EntitiesToMTP(
|
|
text.entities,
|
|
Api::ConvertOption::SkipLocal);
|
|
_userInfoSaving[user].requestId = _api.request(MTPhelp_EditUserInfo(
|
|
user->inputUser,
|
|
MTP_string(text.text),
|
|
entities
|
|
)).done([=](const MTPhelp_UserInfo &result) {
|
|
applyInfo(user, result);
|
|
done(true);
|
|
}).fail([=](const RPCError &error) {
|
|
done(false);
|
|
}).send();
|
|
}
|
|
|
|
Templates &Helper::templates() {
|
|
return _templates;
|
|
}
|
|
|
|
QString ChatOccupiedString(not_null<History*> history) {
|
|
const auto hand = QString::fromUtf8("\xe2\x9c\x8b\xef\xb8\x8f");
|
|
const auto name = ParseOccupationName(history);
|
|
return (name.isEmpty() || name.startsWith(qstr("[rand^")))
|
|
? hand + " chat taken"
|
|
: hand + ' ' + name + " is here";
|
|
}
|
|
|
|
QString InterpretSendPath(const QString &path) {
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
return "App Error: Could not open interpret file: " + path;
|
|
}
|
|
const auto content = QString::fromUtf8(f.readAll());
|
|
f.close();
|
|
const auto lines = content.split('\n');
|
|
auto toId = PeerId(0);
|
|
auto filePath = QString();
|
|
auto caption = QString();
|
|
for (const auto &line : lines) {
|
|
if (line.startsWith(qstr("from: "))) {
|
|
if (Auth().userId() != line.mid(qstr("from: ").size()).toInt()) {
|
|
return "App Error: Wrong current user.";
|
|
}
|
|
} else if (line.startsWith(qstr("channel: "))) {
|
|
const auto channelId = line.mid(qstr("channel: ").size()).toInt();
|
|
toId = peerFromChannel(channelId);
|
|
} else if (line.startsWith(qstr("file: "))) {
|
|
const auto path = line.mid(qstr("file: ").size());
|
|
if (!QFile(path).exists()) {
|
|
return "App Error: Could not find file with path: " + path;
|
|
}
|
|
filePath = path;
|
|
} else if (line.startsWith(qstr("caption: "))) {
|
|
caption = line.mid(qstr("caption: ").size());
|
|
} else if (!caption.isEmpty()) {
|
|
caption += '\n' + line;
|
|
} else {
|
|
return "App Error: Invalid command: " + line;
|
|
}
|
|
}
|
|
const auto history = Auth().data().historyLoaded(toId);
|
|
if (!history) {
|
|
return "App Error: Could not find channel with id: " + QString::number(peerToChannel(toId));
|
|
}
|
|
Ui::showPeerHistory(history, ShowAtUnreadMsgId);
|
|
history->session().api().sendFiles(
|
|
Storage::PrepareMediaList(QStringList(filePath), st::sendMediaPreviewSize),
|
|
SendMediaType::File,
|
|
{ caption },
|
|
nullptr,
|
|
Api::SendAction(history));
|
|
return QString();
|
|
}
|
|
|
|
} // namespace Support
|