Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
744eccc51e | ||
|
|
ce49714533 | ||
|
|
ae2c858dc9 | ||
|
|
f0b5dc42f9 | ||
|
|
9c213bf1c0 | ||
|
|
0c1175f9cd | ||
|
|
0c1312419a | ||
|
|
7e9695b213 | ||
|
|
093fcc3821 | ||
|
|
6f89598a7b | ||
|
|
6ccd53689d | ||
|
|
cd506dfff5 | ||
|
|
5a3733b5b6 | ||
|
|
22a85016e3 | ||
|
|
26c7a95a9f | ||
|
|
9acf617c9f | ||
|
|
72af170484 | ||
|
|
4db2505f5d | ||
|
|
4d40336be0 | ||
|
|
616531b0d0 | ||
|
|
473803edb8 | ||
|
|
a33ca97298 | ||
|
|
a711c89409 | ||
|
|
24ec0e0866 | ||
|
|
e6df927e30 | ||
|
|
638ea3111f | ||
|
|
983d9e6eee | ||
|
|
4b6d74dd9b | ||
|
|
8d70a62ee8 | ||
|
|
a0af748fc5 | ||
|
|
f10ef26226 | ||
|
|
4ebc62afd2 | ||
|
|
18cb26fed6 | ||
|
|
d965385356 | ||
|
|
5cc4066b65 | ||
|
|
6b084301be | ||
|
|
498e82b804 | ||
|
|
8c224f7aca | ||
|
|
f3a2460a54 | ||
|
|
04212140cc | ||
|
|
4e1904b137 | ||
|
|
0299ba4873 | ||
|
|
46ce0df832 | ||
|
|
d66debd802 | ||
|
|
454fe8cdf7 | ||
|
|
c4dfc634d0 | ||
|
|
b08fa069b4 | ||
|
|
3d20958bb4 | ||
|
|
c693fcb2b0 | ||
|
|
ddad42d80e | ||
|
|
c6f66e83ee | ||
|
|
8bb3b7fada | ||
|
|
1d24d29afa | ||
|
|
0536a479f9 | ||
|
|
7fef7e6315 | ||
|
|
c6ef2b057e | ||
|
|
784f10678c | ||
|
|
92dbd7089b | ||
|
|
415990c913 | ||
|
|
e42af74dd2 | ||
|
|
bd1a46252d | ||
|
|
874e5e0a61 | ||
|
|
81457693f1 | ||
|
|
3a700650be | ||
|
|
d642c3f3b5 | ||
|
|
4be03ffc25 | ||
|
|
dcac3146c7 | ||
|
|
10012d6b31 | ||
|
|
3aa1b1e9ae | ||
|
|
4e8a1f8d29 |
1
.github/workflows/linux.yml
vendored
1
.github/workflows/linux.yml
vendored
@@ -443,6 +443,7 @@ jobs:
|
||||
-qt-harfbuzz \
|
||||
-qt-pcre \
|
||||
-qt-xcb \
|
||||
-no-icu \
|
||||
-no-gtk \
|
||||
-static \
|
||||
-dbus-runtime \
|
||||
|
||||
@@ -21,7 +21,6 @@ add_subdirectory(lib_qr)
|
||||
add_subdirectory(lib_webrtc)
|
||||
add_subdirectory(codegen)
|
||||
|
||||
include(CheckCXXSourceCompiles)
|
||||
include(lib_ui/cmake/generate_styles.cmake)
|
||||
include(cmake/generate_numbers.cmake)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="2.4.0.0" />
|
||||
Version="2.4.3.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,4,0,0
|
||||
PRODUCTVERSION 2,4,0,0
|
||||
FILEVERSION 2,4,3,0
|
||||
PRODUCTVERSION 2,4,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "2.4.0.0"
|
||||
VALUE "FileVersion", "2.4.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.0.0"
|
||||
VALUE "ProductVersion", "2.4.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,4,0,0
|
||||
PRODUCTVERSION 2,4,0,0
|
||||
FILEVERSION 2,4,3,0
|
||||
PRODUCTVERSION 2,4,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "2.4.0.0"
|
||||
VALUE "FileVersion", "2.4.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.0.0"
|
||||
VALUE "ProductVersion", "2.4.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -179,6 +179,9 @@ void SessionsContent::setupContent() {
|
||||
}
|
||||
|
||||
void SessionsContent::parse(const Api::Authorizations::List &list) {
|
||||
if (list.empty()) {
|
||||
return;
|
||||
}
|
||||
_data = Full();
|
||||
for (const auto &auth : list) {
|
||||
auto entry = Entry(auth);
|
||||
|
||||
@@ -274,7 +274,8 @@ void BoxController::prepare() {
|
||||
session().changes().messageUpdates(
|
||||
Data::MessageUpdate::Flag::NewAdded
|
||||
) | rpl::filter([=](const Data::MessageUpdate &update) {
|
||||
return (update.item->media()->call() != nullptr);
|
||||
const auto media = update.item->media();
|
||||
return (media != nullptr) && (media->call() != nullptr);
|
||||
}) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
|
||||
insertRow(update.item, InsertWay::Prepend);
|
||||
}, lifetime());
|
||||
|
||||
@@ -310,7 +310,7 @@ QString AllFilesFilter() {
|
||||
}
|
||||
|
||||
QString AlbumFilesFilter() {
|
||||
return qsl("Image and Video Files (*.png *.jpg *.mp4 *.jpeg)");
|
||||
return qsl("Image and Video Files (*.png *.jpg *.jpeg *.mp4 *.mov)");
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 2004000;
|
||||
constexpr auto AppVersionStr = "2.4";
|
||||
constexpr auto AppVersion = 2004003;
|
||||
constexpr auto AppVersionStr = "2.4.3";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -1676,7 +1676,7 @@ mde mdt mdw mdz mht mhtml mjs mmc mof msc msg msh msh1 msh2 msh1xml msh2xml \
|
||||
mshxml msi msp mst ops osd paf pcd phar php php3 php4 php5 php7 phps php-s \
|
||||
pht phtml pif pl plg pm pod prf prg ps1 ps2 ps1xml ps2xml psc1 psc2 psd1 \
|
||||
psm1 pssc pst py py3 pyc pyd pyi pyo pyw pywz pyz rb reg rgs scf scr sct \
|
||||
search-ms settingcontent-ms shb shs slk sys t tmp u3p url vb vbe vbp vbs \
|
||||
search-ms settingcontent-ms sh shb shs slk sys t tmp u3p url vb vbe vbp vbs \
|
||||
vbscript vdx vsmacros vsd vsdm vsdx vss vssm vssx vst vstm vstx vsw vsx vtx \
|
||||
website ws wsc wsf wsh xbap xll xnk xs");
|
||||
#endif // !Q_OS_MAC && !Q_OS_UNIX
|
||||
|
||||
@@ -479,9 +479,15 @@ void MessagesSliceBuilder::requestMessagesCount() {
|
||||
MessagesSlice MessagesSliceBuilder::snapshot() const {
|
||||
auto result = MessagesSlice();
|
||||
result.ids.reserve(_ids.size());
|
||||
auto nearestToAround = std::optional<FullMsgId>();
|
||||
for (const auto &position : _ids) {
|
||||
result.ids.push_back(position.fullId);
|
||||
if (!nearestToAround && position >= _key) {
|
||||
nearestToAround = position.fullId;
|
||||
}
|
||||
}
|
||||
result.nearestToAround = nearestToAround.value_or(
|
||||
_ids.empty() ? FullMsgId() : _ids.back().fullId);
|
||||
result.skippedBefore = _skippedBefore;
|
||||
result.skippedAfter = _skippedAfter;
|
||||
result.fullCount = _fullCount;
|
||||
|
||||
@@ -93,10 +93,10 @@ constexpr auto UnreadMessagePosition = MessagePosition(
|
||||
|
||||
struct MessagesSlice {
|
||||
std::vector<FullMsgId> ids;
|
||||
FullMsgId nearestToAround;
|
||||
std::optional<int> skippedBefore;
|
||||
std::optional<int> skippedAfter;
|
||||
std::optional<int> fullCount;
|
||||
|
||||
};
|
||||
|
||||
struct MessagesQuery {
|
||||
@@ -112,7 +112,6 @@ struct MessagesQuery {
|
||||
MessagePosition aroundId;
|
||||
int limitBefore = 0;
|
||||
int limitAfter = 0;
|
||||
|
||||
};
|
||||
|
||||
struct MessagesResult {
|
||||
|
||||
@@ -227,6 +227,7 @@ void RepliesList::injectRootDivider(
|
||||
bool RepliesList::buildFromData(not_null<Viewer*> viewer) {
|
||||
if (_list.empty() && _skippedBefore == 0 && _skippedAfter == 0) {
|
||||
viewer->slice.ids.clear();
|
||||
viewer->slice.nearestToAround = FullMsgId();
|
||||
viewer->slice.fullCount
|
||||
= viewer->slice.skippedBefore
|
||||
= viewer->slice.skippedAfter
|
||||
@@ -268,10 +269,20 @@ bool RepliesList::buildFromData(not_null<Viewer*> viewer) {
|
||||
|
||||
const auto channelId = _history->channelId();
|
||||
slice->ids.clear();
|
||||
auto nearestToAround = std::optional<MsgId>();
|
||||
slice->ids.reserve(useAfter + useBefore);
|
||||
for (auto j = i - useAfter, e = i + useBefore; j != e; ++j) {
|
||||
if (!nearestToAround && *j < around) {
|
||||
nearestToAround = (j == i - useAfter)
|
||||
? *j
|
||||
: *(j - 1);
|
||||
}
|
||||
slice->ids.emplace_back(channelId, *j);
|
||||
}
|
||||
slice->nearestToAround = FullMsgId(
|
||||
channelId,
|
||||
nearestToAround.value_or(
|
||||
slice->ids.empty() ? 0 : slice->ids.back().msg));
|
||||
slice->fullCount = _fullCount.current();
|
||||
|
||||
injectRootMessageAndReverse(slice);
|
||||
@@ -521,6 +532,7 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
|
||||
if (toFront) {
|
||||
refreshed.reserve(_list.size() + list.size());
|
||||
}
|
||||
auto skipped = 0;
|
||||
for (const auto &message : list) {
|
||||
if (const auto item = owner.addNewMessage(message, clientFlags, type)) {
|
||||
if (item->replyToTop() == _rootId) {
|
||||
@@ -529,7 +541,11 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
|
||||
} else {
|
||||
_list.push_back(item->id);
|
||||
}
|
||||
} else {
|
||||
++skipped;
|
||||
}
|
||||
} else {
|
||||
++skipped;
|
||||
}
|
||||
}
|
||||
if (toFront) {
|
||||
@@ -545,7 +561,7 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
|
||||
0);
|
||||
}
|
||||
|
||||
const auto checkedCount = std::max(fullCount, nowSize);
|
||||
const auto checkedCount = std::max(fullCount - skipped, nowSize);
|
||||
if (_skippedBefore && _skippedAfter) {
|
||||
auto &correct = toFront ? _skippedBefore : _skippedAfter;
|
||||
*correct = std::max(
|
||||
@@ -563,13 +579,13 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
|
||||
_fullCount = checkedCount;
|
||||
|
||||
if (const auto item = lookupRoot()) {
|
||||
if (_skippedAfter == 0) {
|
||||
if (_skippedAfter == 0 && !_list.empty()) {
|
||||
item->setRepliesMaxId(_list.front());
|
||||
} else {
|
||||
item->setRepliesPossibleMaxId(maxId);
|
||||
}
|
||||
if (const auto original = item->lookupDiscussionPostOriginal()) {
|
||||
if (_skippedAfter == 0) {
|
||||
if (_skippedAfter == 0 && !_list.empty()) {
|
||||
original->setRepliesMaxId(_list.front());
|
||||
} else {
|
||||
original->setRepliesPossibleMaxId(maxId);
|
||||
@@ -577,7 +593,8 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
Ensures(list.size() >= skipped);
|
||||
return (list.size() == skipped);
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "data/data_scheduled_messages.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "api/api_hash.h"
|
||||
@@ -26,6 +27,11 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
|
||||
return (received > 0) && (received + kRequestTimeLimit > crl::now());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool HasScheduledDate(not_null<HistoryItem*> item) {
|
||||
return (item->date() != ScheduledMessages::kScheduledUntilOnlineTimestamp)
|
||||
&& (item->date() > base::unixtime::now());
|
||||
}
|
||||
|
||||
MTPMessage PrepareMessage(const MTPMessage &message, MsgId id) {
|
||||
return message.match([&](const MTPDmessageEmpty &) {
|
||||
return MTP_messageEmpty(MTP_int(id));
|
||||
@@ -147,7 +153,11 @@ void ScheduledMessages::sendNowSimpleMessage(
|
||||
not_null<HistoryItem*> local) {
|
||||
Expects(local->isSending());
|
||||
Expects(local->isScheduled());
|
||||
Expects(local->date() == kScheduledUntilOnlineTimestamp);
|
||||
if (HasScheduledDate(local)) {
|
||||
LOG(("Error: trying to put to history a new local message, "
|
||||
"that has scheduled date."));
|
||||
return;
|
||||
}
|
||||
|
||||
// When the user sends a text message scheduled until online
|
||||
// while the recipient is already online, the server sends
|
||||
@@ -243,16 +253,18 @@ void ScheduledMessages::checkEntitiesAndUpdate(const MTPDmessage &data) {
|
||||
}
|
||||
|
||||
const auto existing = j->second;
|
||||
Assert(existing->date() == kScheduledUntilOnlineTimestamp);
|
||||
existing->updateSentContent({
|
||||
qs(data.vmessage()),
|
||||
Api::EntitiesFromMTP(_session, data.ventities().value_or_empty())
|
||||
}, data.vmedia());
|
||||
existing->updateReplyMarkup(data.vreply_markup());
|
||||
existing->updateForwardedInfo(data.vfwd_from());
|
||||
_session->data().requestItemTextRefresh(existing);
|
||||
if (!HasScheduledDate(existing)) {
|
||||
// Destroy a local message, that should be in history.
|
||||
existing->updateSentContent({
|
||||
qs(data.vmessage()),
|
||||
Api::EntitiesFromMTP(_session, data.ventities().value_or_empty())
|
||||
}, data.vmedia());
|
||||
existing->updateReplyMarkup(data.vreply_markup());
|
||||
existing->updateForwardedInfo(data.vfwd_from());
|
||||
_session->data().requestItemTextRefresh(existing);
|
||||
|
||||
existing->destroy();
|
||||
existing->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void ScheduledMessages::apply(
|
||||
|
||||
@@ -1764,6 +1764,8 @@ bool Session::checkEntitiesAndViewsUpdate(const MTPDmessage &data) {
|
||||
existing->setViewsCount(data.vviews().value_or(-1));
|
||||
if (const auto replies = data.vreplies()) {
|
||||
existing->setReplies(*replies);
|
||||
} else {
|
||||
existing->clearReplies();
|
||||
}
|
||||
existing->setForwardsCount(data.vforwards().value_or(-1));
|
||||
if (const auto reply = data.vreply_to()) {
|
||||
@@ -1773,6 +1775,7 @@ bool Session::checkEntitiesAndViewsUpdate(const MTPDmessage &data) {
|
||||
data.vreply_to_msg_id().v));
|
||||
});
|
||||
}
|
||||
existing->setPostAuthor(data.vpost_author().value_or_empty());
|
||||
existing->indexAsNewItem();
|
||||
existing->contributeToSlowmode(data.vdate().v);
|
||||
requestItemTextRefresh(existing);
|
||||
|
||||
@@ -1187,6 +1187,8 @@ Message ParseMessage(
|
||||
});
|
||||
result.forwarded = result.forwardedFromId
|
||||
|| !result.forwardedFromName.isEmpty();
|
||||
result.showForwardedAsOriginal = result.forwarded
|
||||
&& result.savedFromChatId;
|
||||
}
|
||||
if (const auto postAuthor = data.vpost_author()) {
|
||||
result.signature = ParseString(*postAuthor);
|
||||
|
||||
@@ -520,6 +520,7 @@ struct Message {
|
||||
Utf8String forwardedFromName;
|
||||
TimeId forwardedDate = 0;
|
||||
bool forwarded = false;
|
||||
bool showForwardedAsOriginal = false;
|
||||
PeerId savedFromChatId = 0;
|
||||
Utf8String signature;
|
||||
int32 viaBotId = 0;
|
||||
|
||||
@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/output/export_output_abstract.h"
|
||||
|
||||
#include "export/output/export_output_text.h"
|
||||
#include "export/output/export_output_html.h"
|
||||
#include "export/output/export_output_json.h"
|
||||
#include "export/output/export_output_stats.h"
|
||||
@@ -50,7 +49,6 @@ QString NormalizePath(const Settings &settings) {
|
||||
std::unique_ptr<AbstractWriter> CreateWriter(Format format) {
|
||||
switch (format) {
|
||||
case Format::Html: return std::make_unique<HtmlWriter>();
|
||||
case Format::Text: return std::make_unique<TextWriter>();
|
||||
case Format::Json: return std::make_unique<JsonWriter>();
|
||||
}
|
||||
Unexpected("Format in Export::Output::CreateWriter.");
|
||||
|
||||
@@ -35,8 +35,6 @@ class Stats;
|
||||
enum class Format {
|
||||
Html,
|
||||
Json,
|
||||
Text,
|
||||
Yaml,
|
||||
};
|
||||
|
||||
class AbstractWriter {
|
||||
|
||||
@@ -516,11 +516,13 @@ struct HtmlWriter::MessageInfo {
|
||||
};
|
||||
int32 id = 0;
|
||||
Type type = Type::Service;
|
||||
int32 fromId = 0;
|
||||
Data::PeerId fromId = 0;
|
||||
int32 viaBotId = 0;
|
||||
TimeId date = 0;
|
||||
Data::PeerId forwardedFromId = 0;
|
||||
QString forwardedFromName;
|
||||
bool forwarded = false;
|
||||
bool showForwardedAsOriginal = false;
|
||||
TimeId forwardedDate = 0;
|
||||
};
|
||||
|
||||
@@ -968,11 +970,13 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||
auto info = MessageInfo();
|
||||
info.id = message.id;
|
||||
info.fromId = message.fromId;
|
||||
info.viaBotId = message.viaBotId;
|
||||
info.date = message.date;
|
||||
info.forwardedFromId = message.forwardedFromId;
|
||||
info.forwardedFromName = message.forwardedFromName;
|
||||
info.forwardedDate = message.forwardedDate;
|
||||
info.forwarded = message.forwarded;
|
||||
info.showForwardedAsOriginal = message.showForwardedAsOriginal;
|
||||
if (v::is<UnsupportedMedia>(message.media.content)) {
|
||||
return { info, pushServiceMessage(
|
||||
message.id,
|
||||
@@ -989,7 +993,7 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||
using DialogType = Data::DialogInfo::Type;
|
||||
const auto isChannel = (dialog.type == DialogType::PrivateChannel)
|
||||
|| (dialog.type == DialogType::PublicChannel);
|
||||
const auto serviceFrom = peers.wrapUserName(message.fromId);
|
||||
const auto serviceFrom = peers.wrapPeerName(message.fromId);
|
||||
const auto serviceText = v::match(message.action.content, [&](
|
||||
const ActionChatCreate &data) {
|
||||
return serviceFrom
|
||||
@@ -1107,13 +1111,31 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||
info.type = MessageInfo::Type::Default;
|
||||
|
||||
const auto wrap = messageNeedsWrap(message, previous);
|
||||
const auto fromPeerId = message.fromId
|
||||
? UserPeerId(message.fromId)
|
||||
: ChatPeerId(message.chatId);
|
||||
const auto fromPeerId = message.fromId;
|
||||
const auto showForwardedInfo = message.forwarded
|
||||
&& !message.showForwardedAsOriginal;
|
||||
auto forwardedUserpic = UserpicData();
|
||||
if (message.forwarded) {
|
||||
forwardedUserpic.colorIndex = message.forwardedFromId
|
||||
? PeerColorIndex(BarePeerId(message.forwardedFromId))
|
||||
: PeerColorIndex(message.id);
|
||||
forwardedUserpic.pixelSize = kHistoryUserpicSize;
|
||||
if (message.forwardedFromId) {
|
||||
FillUserpicNames(
|
||||
forwardedUserpic,
|
||||
peers.peer(message.forwardedFromId));
|
||||
} else {
|
||||
FillUserpicNames(forwardedUserpic, message.forwardedFromName);
|
||||
}
|
||||
}
|
||||
auto userpic = UserpicData();
|
||||
userpic.colorIndex = PeerColorIndex(BarePeerId(fromPeerId));
|
||||
userpic.pixelSize = kHistoryUserpicSize;
|
||||
FillUserpicNames(userpic, peers.peer(fromPeerId));
|
||||
if (message.showForwardedAsOriginal) {
|
||||
userpic = forwardedUserpic;
|
||||
} else {
|
||||
userpic.colorIndex = PeerColorIndex(BarePeerId(fromPeerId));
|
||||
userpic.pixelSize = kHistoryUserpicSize;
|
||||
FillUserpicNames(userpic, peers.peer(fromPeerId));
|
||||
}
|
||||
|
||||
const auto via = [&] {
|
||||
if (message.viaBotId) {
|
||||
@@ -1148,25 +1170,13 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||
block.append(pushDiv("from_name"));
|
||||
block.append(SerializeString(
|
||||
ComposeName(userpic, "Deleted Account")));
|
||||
if (!via.isEmpty() && !message.forwarded) {
|
||||
if (!via.isEmpty()
|
||||
&& (!message.forwarded || message.showForwardedAsOriginal)) {
|
||||
block.append(" via @" + via);
|
||||
}
|
||||
block.append(popTag());
|
||||
}
|
||||
if (message.forwarded) {
|
||||
auto forwardedUserpic = UserpicData();
|
||||
forwardedUserpic.colorIndex = message.forwardedFromId
|
||||
? PeerColorIndex(BarePeerId(message.forwardedFromId))
|
||||
: PeerColorIndex(message.id);
|
||||
forwardedUserpic.pixelSize = kHistoryUserpicSize;
|
||||
if (message.forwardedFromId) {
|
||||
FillUserpicNames(
|
||||
forwardedUserpic,
|
||||
peers.peer(message.forwardedFromId));
|
||||
} else {
|
||||
FillUserpicNames(forwardedUserpic, message.forwardedFromName);
|
||||
}
|
||||
|
||||
if (showForwardedInfo) {
|
||||
const auto forwardedWrap = forwardedNeedsWrap(message, previous);
|
||||
if (forwardedWrap) {
|
||||
block.append(pushDiv("pull_left forwarded userpic_wrap"));
|
||||
@@ -1214,7 +1224,7 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||
block.append(SerializeString(message.signature));
|
||||
block.append(popTag());
|
||||
}
|
||||
if (message.forwarded) {
|
||||
if (showForwardedInfo) {
|
||||
block.append(popTag());
|
||||
}
|
||||
block.append(popTag());
|
||||
@@ -1232,10 +1242,15 @@ bool HtmlWriter::Wrap::messageNeedsWrap(
|
||||
return true;
|
||||
} else if (!message.fromId || previous->fromId != message.fromId) {
|
||||
return true;
|
||||
} else if (message.viaBotId != previous->viaBotId) {
|
||||
return true;
|
||||
} else if (QDateTime::fromTime_t(previous->date).date()
|
||||
!= QDateTime::fromTime_t(message.date).date()) {
|
||||
return true;
|
||||
} else if (message.forwarded != previous->forwarded) {
|
||||
} else if (message.forwarded != previous->forwarded
|
||||
|| message.showForwardedAsOriginal != previous->showForwardedAsOriginal
|
||||
|| message.forwardedFromId != previous->forwardedFromId
|
||||
|| message.forwardedFromName != previous->forwardedFromName) {
|
||||
return true;
|
||||
} else if (std::abs(message.date - previous->date)
|
||||
> ((message.forwardedFromId || !message.forwardedFromName.isEmpty())
|
||||
|
||||
@@ -295,8 +295,8 @@ QByteArray SerializeMessage(
|
||||
};
|
||||
const auto pushFrom = [&](const QByteArray &label = "from") {
|
||||
if (message.fromId) {
|
||||
pushBare(label, wrapUserName(message.fromId));
|
||||
pushBare(label+"_id", Data::NumberToString(message.fromId));
|
||||
pushBare(label, wrapPeerName(message.fromId));
|
||||
push(label+"_id", message.fromId);
|
||||
}
|
||||
};
|
||||
const auto pushReplyToMsgId = [&](
|
||||
|
||||
@@ -1,997 +0,0 @@
|
||||
/*
|
||||
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 "export/output/export_output_text.h"
|
||||
|
||||
#include "export/output/export_output_result.h"
|
||||
#include "export/data/export_data_types.h"
|
||||
#include "core/utils.h"
|
||||
|
||||
#include <QtCore/QFile>
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
namespace {
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
const auto kLineBreak = QByteArrayLiteral("\r\n");
|
||||
#else // Q_OS_WIN
|
||||
const auto kLineBreak = QByteArrayLiteral("\n");
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
void SerializeMultiline(
|
||||
QByteArray &appendTo,
|
||||
const QByteArray &value,
|
||||
int newline) {
|
||||
const auto data = value.data();
|
||||
auto offset = 0;
|
||||
do {
|
||||
appendTo.append("> ");
|
||||
const auto win = (newline > 0 && *(data + newline - 1) == '\r');
|
||||
if (win) --newline;
|
||||
appendTo.append(data + offset, newline - offset).append(kLineBreak);
|
||||
if (win) ++newline;
|
||||
offset = newline + 1;
|
||||
newline = value.indexOf('\n', offset);
|
||||
} while (newline > 0);
|
||||
if (const auto size = value.size(); size > offset) {
|
||||
appendTo.append("> ");
|
||||
appendTo.append(data + offset, size - offset).append(kLineBreak);
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray JoinList(
|
||||
const QByteArray &separator,
|
||||
const std::vector<QByteArray> &list) {
|
||||
if (list.empty()) {
|
||||
return QByteArray();
|
||||
} else if (list.size() == 1) {
|
||||
return list[0];
|
||||
}
|
||||
auto size = (list.size() - 1) * separator.size();
|
||||
for (const auto &value : list) {
|
||||
size += value.size();
|
||||
}
|
||||
auto result = QByteArray();
|
||||
result.reserve(size);
|
||||
auto counter = 0;
|
||||
while (true) {
|
||||
result.append(list[counter]);
|
||||
if (++counter == list.size()) {
|
||||
break;
|
||||
} else {
|
||||
result.append(separator);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray SerializeKeyValue(
|
||||
std::vector<std::pair<QByteArray, QByteArray>> &&values) {
|
||||
auto result = QByteArray();
|
||||
for (const auto &[key, value] : values) {
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
result.append(key);
|
||||
if (const auto newline = value.indexOf('\n'); newline >= 0) {
|
||||
result.append(':').append(kLineBreak);
|
||||
SerializeMultiline(result, value, newline);
|
||||
} else {
|
||||
result.append(": ").append(value).append(kLineBreak);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Data::Utf8String FormatUsername(const Data::Utf8String &username) {
|
||||
return username.isEmpty() ? username : ('@' + username);
|
||||
}
|
||||
|
||||
QByteArray FormatFilePath(const Data::File &file) {
|
||||
return file.relativePath.toUtf8();
|
||||
}
|
||||
|
||||
QByteArray SerializeMessage(
|
||||
const Data::Message &message,
|
||||
const std::map<Data::PeerId, Data::Peer> &peers,
|
||||
const QString &internalLinksDomain) {
|
||||
using namespace Data;
|
||||
|
||||
if (v::is<UnsupportedMedia>(message.media.content)) {
|
||||
return "Error! This message is not supported "
|
||||
"by this version of Telegram Desktop. "
|
||||
"Please update the application.";
|
||||
}
|
||||
|
||||
const auto peer = [&](PeerId peerId) -> const Peer& {
|
||||
if (const auto i = peers.find(peerId); i != end(peers)) {
|
||||
return i->second;
|
||||
}
|
||||
static auto empty = Peer{ User() };
|
||||
return empty;
|
||||
};
|
||||
const auto user = [&](int32 userId) -> const User& {
|
||||
if (const auto result = peer(UserPeerId(userId)).user()) {
|
||||
return *result;
|
||||
}
|
||||
static auto empty = User();
|
||||
return empty;
|
||||
};
|
||||
const auto chat = [&](int32 chatId) -> const Chat& {
|
||||
if (const auto result = peer(ChatPeerId(chatId)).chat()) {
|
||||
return *result;
|
||||
}
|
||||
static auto empty = Chat();
|
||||
return empty;
|
||||
};
|
||||
|
||||
auto values = std::vector<std::pair<QByteArray, QByteArray>>{
|
||||
{ "ID", NumberToString(message.id) },
|
||||
{ "Date", FormatDateTime(message.date) },
|
||||
{ "Edited", FormatDateTime(message.edited) },
|
||||
};
|
||||
const auto push = [&](const QByteArray &key, const QByteArray &value) {
|
||||
if (!value.isEmpty()) {
|
||||
values.emplace_back(key, value);
|
||||
}
|
||||
};
|
||||
const auto wrapPeerName = [&](PeerId peerId) {
|
||||
const auto result = peer(peerId).name();
|
||||
return result.isEmpty() ? QByteArray("(deleted peer)") : result;
|
||||
};
|
||||
const auto wrapUserName = [&](int32 userId) {
|
||||
const auto result = user(userId).name();
|
||||
return result.isEmpty() ? QByteArray("(deleted user)") : result;
|
||||
};
|
||||
const auto pushFrom = [&](const QByteArray &label = "From") {
|
||||
if (message.fromId) {
|
||||
push(label, wrapUserName(message.fromId));
|
||||
}
|
||||
};
|
||||
const auto pushReplyToMsgId = [&](
|
||||
const QByteArray &label = "Reply to message") {
|
||||
if (message.replyToMsgId) {
|
||||
push(label, "ID-" + NumberToString(message.replyToMsgId));
|
||||
}
|
||||
};
|
||||
const auto pushUserNames = [&](
|
||||
const std::vector<int32> &data,
|
||||
const QByteArray &labelOne = "Member",
|
||||
const QByteArray &labelMany = "Members") {
|
||||
auto list = std::vector<QByteArray>();
|
||||
for (const auto userId : data) {
|
||||
list.push_back(wrapUserName(userId));
|
||||
}
|
||||
if (list.size() == 1) {
|
||||
push(labelOne, list[0]);
|
||||
} else if (!list.empty()) {
|
||||
push(labelMany, JoinList(", ", list));
|
||||
}
|
||||
};
|
||||
const auto pushActor = [&] {
|
||||
pushFrom("Actor");
|
||||
};
|
||||
const auto pushAction = [&](const QByteArray &action) {
|
||||
push("Action", action);
|
||||
};
|
||||
const auto pushTTL = [&](
|
||||
const QByteArray &label = "Self destruct period") {
|
||||
if (const auto ttl = message.media.ttl) {
|
||||
push(label, NumberToString(ttl) + " sec.");
|
||||
}
|
||||
};
|
||||
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
const auto pushPath = [&](
|
||||
const Data::File &file,
|
||||
const QByteArray &label,
|
||||
const QByteArray &name = QByteArray()) {
|
||||
Expects(!file.relativePath.isEmpty()
|
||||
|| file.skipReason != SkipReason::None);
|
||||
|
||||
push(label, [&]() -> QByteArray {
|
||||
const auto pre = name.isEmpty() ? QByteArray() : name + ' ';
|
||||
switch (file.skipReason) {
|
||||
case SkipReason::Unavailable:
|
||||
return pre + "(" + label + " unavailable, "
|
||||
"please try again later)";
|
||||
case SkipReason::FileSize:
|
||||
return pre + "(" + label + " exceeds maximum size. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::FileType:
|
||||
return pre + "(" + label + " not included. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::None: return FormatFilePath(file);
|
||||
}
|
||||
Unexpected("Skip reason while writing file path.");
|
||||
}());
|
||||
};
|
||||
const auto pushPhoto = [&](const Image &image) {
|
||||
pushPath(image.file, "Photo");
|
||||
if (image.width && image.height) {
|
||||
push("Width", NumberToString(image.width));
|
||||
push("Height", NumberToString(image.height));
|
||||
}
|
||||
};
|
||||
|
||||
v::match(message.action.content, [&](const ActionChatCreate &data) {
|
||||
pushActor();
|
||||
pushAction("Create group");
|
||||
push("Title", data.title);
|
||||
pushUserNames(data.userIds);
|
||||
}, [&](const ActionChatEditTitle &data) {
|
||||
pushActor();
|
||||
pushAction("Edit group title");
|
||||
push("New title", data.title);
|
||||
}, [&](const ActionChatEditPhoto &data) {
|
||||
pushActor();
|
||||
pushAction("Edit group photo");
|
||||
pushPhoto(data.photo.image);
|
||||
}, [&](const ActionChatDeletePhoto &data) {
|
||||
pushActor();
|
||||
pushAction("Delete group photo");
|
||||
}, [&](const ActionChatAddUser &data) {
|
||||
pushActor();
|
||||
pushAction("Invite members");
|
||||
pushUserNames(data.userIds);
|
||||
}, [&](const ActionChatDeleteUser &data) {
|
||||
pushActor();
|
||||
pushAction("Remove members");
|
||||
push("Member", wrapUserName(data.userId));
|
||||
}, [&](const ActionChatJoinedByLink &data) {
|
||||
pushActor();
|
||||
pushAction("Join group by link");
|
||||
push("Inviter", wrapUserName(data.inviterId));
|
||||
}, [&](const ActionChannelCreate &data) {
|
||||
pushActor();
|
||||
pushAction("Create channel");
|
||||
push("Title", data.title);
|
||||
}, [&](const ActionChatMigrateTo &data) {
|
||||
pushActor();
|
||||
pushAction("Convert this group to supergroup");
|
||||
}, [&](const ActionChannelMigrateFrom &data) {
|
||||
pushActor();
|
||||
pushAction("Basic group converted to supergroup");
|
||||
push("Title", data.title);
|
||||
}, [&](const ActionPinMessage &data) {
|
||||
pushActor();
|
||||
pushAction("Pin message");
|
||||
pushReplyToMsgId("Message");
|
||||
}, [&](const ActionHistoryClear &data) {
|
||||
pushActor();
|
||||
pushAction("Clear history");
|
||||
}, [&](const ActionGameScore &data) {
|
||||
pushActor();
|
||||
pushAction("Score in a game");
|
||||
pushReplyToMsgId("Game message");
|
||||
push("Score", NumberToString(data.score));
|
||||
}, [&](const ActionPaymentSent &data) {
|
||||
pushAction("Send payment");
|
||||
push(
|
||||
"Amount",
|
||||
Data::FormatMoneyAmount(data.amount, data.currency));
|
||||
pushReplyToMsgId("Invoice message");
|
||||
}, [&](const ActionPhoneCall &data) {
|
||||
pushActor();
|
||||
pushAction("Phone call");
|
||||
if (data.duration) {
|
||||
push("Duration", NumberToString(data.duration) + " sec.");
|
||||
}
|
||||
using Reason = ActionPhoneCall::DiscardReason;
|
||||
push("Discard reason", [&] {
|
||||
switch (data.discardReason) {
|
||||
case Reason::Busy: return "Busy";
|
||||
case Reason::Disconnect: return "Disconnect";
|
||||
case Reason::Hangup: return "Hangup";
|
||||
case Reason::Missed: return "Missed";
|
||||
}
|
||||
return "";
|
||||
}());
|
||||
}, [&](const ActionScreenshotTaken &data) {
|
||||
pushActor();
|
||||
pushAction("Take screenshot");
|
||||
}, [&](const ActionCustomAction &data) {
|
||||
pushActor();
|
||||
push("Information", data.message);
|
||||
}, [&](const ActionBotAllowed &data) {
|
||||
pushAction("Allow sending messages");
|
||||
push("Reason", "Login on \"" + data.domain + "\"");
|
||||
}, [&](const ActionSecureValuesSent &data) {
|
||||
pushAction("Send Telegram Passport values");
|
||||
auto list = std::vector<QByteArray>();
|
||||
for (const auto type : data.types) {
|
||||
list.push_back([&] {
|
||||
using Type = ActionSecureValuesSent::Type;
|
||||
switch (type) {
|
||||
case Type::PersonalDetails: return "Personal details";
|
||||
case Type::Passport: return "Passport";
|
||||
case Type::DriverLicense: return "Driver license";
|
||||
case Type::IdentityCard: return "Identity card";
|
||||
case Type::InternalPassport: return "Internal passport";
|
||||
case Type::Address: return "Address information";
|
||||
case Type::UtilityBill: return "Utility bill";
|
||||
case Type::BankStatement: return "Bank statement";
|
||||
case Type::RentalAgreement: return "Rental agreement";
|
||||
case Type::PassportRegistration:
|
||||
return "Passport registration";
|
||||
case Type::TemporaryRegistration:
|
||||
return "Temporary registration";
|
||||
case Type::Phone: return "Phone number";
|
||||
case Type::Email: return "Email";
|
||||
}
|
||||
return "";
|
||||
}());
|
||||
}
|
||||
if (list.size() == 1) {
|
||||
push("Value", list[0]);
|
||||
} else if (!list.empty()) {
|
||||
push("Values", JoinList(", ", list));
|
||||
}
|
||||
}, [&](const ActionContactSignUp &data) {
|
||||
pushActor();
|
||||
pushAction("Join Telegram");
|
||||
}, [&](const ActionPhoneNumberRequest &data) {
|
||||
pushActor();
|
||||
pushAction("Request Phone Number");
|
||||
}, [](v::null_t) {});
|
||||
|
||||
if (v::is_null(message.action.content)) {
|
||||
pushFrom();
|
||||
push("Author", message.signature);
|
||||
if (message.forwardedFromId) {
|
||||
push("Forwarded from", wrapPeerName(message.forwardedFromId));
|
||||
} else if (!message.forwardedFromName.isEmpty()) {
|
||||
push("Forwarded from", message.forwardedFromName);
|
||||
}
|
||||
if (message.savedFromChatId) {
|
||||
push("Saved from", wrapPeerName(message.savedFromChatId));
|
||||
}
|
||||
pushReplyToMsgId();
|
||||
if (message.viaBotId) {
|
||||
push("Via", user(message.viaBotId).username);
|
||||
}
|
||||
}
|
||||
|
||||
v::match(message.media.content, [&](const Photo &photo) {
|
||||
pushPhoto(photo.image);
|
||||
pushTTL();
|
||||
}, [&](const Document &data) {
|
||||
const auto pushMyPath = [&](const QByteArray &label) {
|
||||
return pushPath(data.file, label);
|
||||
};
|
||||
if (data.isSticker) {
|
||||
pushMyPath("Sticker");
|
||||
push("Emoji", data.stickerEmoji);
|
||||
} else if (data.isVideoMessage) {
|
||||
pushMyPath("Video message");
|
||||
} else if (data.isVoiceMessage) {
|
||||
pushMyPath("Voice message");
|
||||
} else if (data.isAnimated) {
|
||||
pushMyPath("Animation");
|
||||
} else if (data.isVideoFile) {
|
||||
pushMyPath("Video file");
|
||||
} else if (data.isAudioFile) {
|
||||
pushMyPath("Audio file");
|
||||
push("Performer", data.songPerformer);
|
||||
push("Title", data.songTitle);
|
||||
} else {
|
||||
pushMyPath("File");
|
||||
}
|
||||
if (!data.isSticker) {
|
||||
push("Mime type", data.mime);
|
||||
}
|
||||
if (data.duration) {
|
||||
push("Duration", NumberToString(data.duration) + " sec.");
|
||||
}
|
||||
if (data.width && data.height) {
|
||||
push("Width", NumberToString(data.width));
|
||||
push("Height", NumberToString(data.height));
|
||||
}
|
||||
pushTTL();
|
||||
}, [&](const SharedContact &data) {
|
||||
push("Contact information", SerializeKeyValue({
|
||||
{ "First name", data.info.firstName },
|
||||
{ "Last name", data.info.lastName },
|
||||
{ "Phone number", FormatPhoneNumber(data.info.phoneNumber) },
|
||||
}));
|
||||
if (!data.vcard.content.isEmpty()) {
|
||||
pushPath(data.vcard, "Contact vcard");
|
||||
}
|
||||
}, [&](const GeoPoint &data) {
|
||||
push("Location", data.valid ? SerializeKeyValue({
|
||||
{ "Latitude", NumberToString(data.latitude) },
|
||||
{ "Longitude", NumberToString(data.longitude) },
|
||||
}) : QByteArray("(empty value)"));
|
||||
pushTTL("Live location period");
|
||||
}, [&](const Venue &data) {
|
||||
push("Place name", data.title);
|
||||
push("Address", data.address);
|
||||
if (data.point.valid) {
|
||||
push("Location", SerializeKeyValue({
|
||||
{ "Latitude", NumberToString(data.point.latitude) },
|
||||
{ "Longitude", NumberToString(data.point.longitude) },
|
||||
}));
|
||||
}
|
||||
}, [&](const Game &data) {
|
||||
push("Game", data.title);
|
||||
push("Description", data.description);
|
||||
if (data.botId != 0 && !data.shortName.isEmpty()) {
|
||||
const auto bot = user(data.botId);
|
||||
if (bot.isBot && !bot.username.isEmpty()) {
|
||||
push("Link", internalLinksDomain.toUtf8()
|
||||
+ bot.username
|
||||
+ "?game="
|
||||
+ data.shortName);
|
||||
}
|
||||
}
|
||||
}, [&](const Invoice &data) {
|
||||
push("Invoice", SerializeKeyValue({
|
||||
{ "Title", data.title },
|
||||
{ "Description", data.description },
|
||||
{
|
||||
"Amount",
|
||||
Data::FormatMoneyAmount(data.amount, data.currency)
|
||||
},
|
||||
{ "Receipt message", (data.receiptMsgId
|
||||
? "ID-" + NumberToString(data.receiptMsgId)
|
||||
: QByteArray()) }
|
||||
}));
|
||||
}, [&](const Poll &data) {
|
||||
push("Poll", SerializeKeyValue({
|
||||
{ "Question", data.question },
|
||||
{ "Closed", data.closed ? QByteArray("Yes") : QByteArray() },
|
||||
{ "Votes", NumberToString(data.totalVotes) },
|
||||
}));
|
||||
for (const auto &answer : data.answers) {
|
||||
push("Answer", SerializeKeyValue({
|
||||
{ "Text", answer.text },
|
||||
{ "Votes", NumberToString(answer.votes) },
|
||||
{ "Chosen", answer.my ? QByteArray("Yes") : QByteArray() }
|
||||
}));
|
||||
}
|
||||
}, [](const UnsupportedMedia &data) {
|
||||
Unexpected("Unsupported message.");
|
||||
}, [](v::null_t) {});
|
||||
|
||||
auto value = JoinList(QByteArray(), ranges::view::all(
|
||||
message.text
|
||||
) | ranges::view::transform([](const Data::TextPart &part) {
|
||||
return part.text;
|
||||
}) | ranges::to_vector);
|
||||
push("Text", value);
|
||||
|
||||
return SerializeKeyValue(std::move(values));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result TextWriter::start(
|
||||
const Settings &settings,
|
||||
const Environment &environment,
|
||||
Stats *stats) {
|
||||
Expects(settings.path.endsWith('/'));
|
||||
|
||||
_settings = base::duplicate(settings);
|
||||
_environment = environment;
|
||||
_stats = stats;
|
||||
_summary = fileWithRelativePath(mainFileRelativePath());
|
||||
return _summary->writeBlock(_environment.aboutTelegram
|
||||
+ kLineBreak
|
||||
+ kLineBreak);
|
||||
}
|
||||
|
||||
Result TextWriter::writePersonal(const Data::PersonalInfo &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
const auto &info = data.user.info;
|
||||
const auto serialized = SerializeKeyValue({
|
||||
{ "First name", info.firstName },
|
||||
{ "Last name", info.lastName },
|
||||
{ "Phone number", Data::FormatPhoneNumber(info.phoneNumber) },
|
||||
{ "Username", FormatUsername(data.user.username) },
|
||||
{ "Bio", data.bio },
|
||||
})
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(serialized);
|
||||
}
|
||||
|
||||
Result TextWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {
|
||||
Expects(_summary != nullptr);
|
||||
Expects(_userpics == nullptr);
|
||||
|
||||
_userpicsCount = data.count;
|
||||
if (!_userpicsCount) {
|
||||
return Result::Success();
|
||||
}
|
||||
const auto filename = "lists/profile_pictures.txt";
|
||||
_userpics = fileWithRelativePath(filename);
|
||||
|
||||
const auto serialized = "Profile pictures"
|
||||
"(" + Data::NumberToString(_userpicsCount) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(serialized);
|
||||
}
|
||||
|
||||
Result TextWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) {
|
||||
Expects(_userpics != nullptr);
|
||||
Expects(!data.list.empty());
|
||||
|
||||
auto lines = std::vector<QByteArray>();
|
||||
lines.reserve(data.list.size());
|
||||
for (const auto &userpic : data.list) {
|
||||
if (!userpic.date) {
|
||||
lines.push_back("(deleted photo)");
|
||||
} else {
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
const auto &file = userpic.image.file;
|
||||
Assert(!file.relativePath.isEmpty()
|
||||
|| file.skipReason != SkipReason::None);
|
||||
const auto path = [&]() -> Data::Utf8String {
|
||||
switch (file.skipReason) {
|
||||
case SkipReason::Unavailable:
|
||||
return "(Photo unavailable, please try again later)";
|
||||
case SkipReason::FileSize:
|
||||
return "(Photo exceeds maximum size. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::FileType:
|
||||
return "(Photo not included. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::None: return FormatFilePath(file);
|
||||
}
|
||||
Unexpected("Skip reason while writing photo path.");
|
||||
}();
|
||||
lines.push_back(SerializeKeyValue({
|
||||
{ "Added", Data::FormatDateTime(userpic.date) },
|
||||
{ "Photo", path },
|
||||
}));
|
||||
}
|
||||
}
|
||||
return _userpics->writeBlock(JoinList(kLineBreak, lines) + kLineBreak);
|
||||
}
|
||||
|
||||
Result TextWriter::writeUserpicsEnd() {
|
||||
_userpics = nullptr;
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeContactsList(const Data::ContactsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (const auto result = writeSavedContacts(data); !result) {
|
||||
return result;
|
||||
} else if (const auto result = writeFrequentContacts(data); !result) {
|
||||
return result;
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeSavedContacts(const Data::ContactsList &data) {
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/contacts.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.list.size());
|
||||
for (const auto index : Data::SortedContactsIndices(data)) {
|
||||
const auto &contact = data.list[index];
|
||||
if (contact.firstName.isEmpty()
|
||||
&& contact.lastName.isEmpty()
|
||||
&& contact.phoneNumber.isEmpty()) {
|
||||
list.push_back("(deleted user)" + kLineBreak);
|
||||
} else {
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "First name", contact.firstName },
|
||||
{ "Last name", contact.lastName },
|
||||
{
|
||||
"Phone number",
|
||||
Data::FormatPhoneNumber(contact.phoneNumber)
|
||||
},
|
||||
{ "Added", Data::FormatDateTime(contact.date) }
|
||||
}));
|
||||
}
|
||||
}
|
||||
const auto full = _environment.aboutContacts
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Contacts "
|
||||
"(" + Data::NumberToString(data.list.size()) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeFrequentContacts(const Data::ContactsList &data) {
|
||||
const auto size = data.correspondents.size()
|
||||
+ data.inlineBots.size()
|
||||
+ data.phoneCalls.size();
|
||||
if (!size) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/frequent.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(size);
|
||||
const auto writeList = [&](
|
||||
const std::vector<Data::TopPeer> &peers,
|
||||
Data::Utf8String category) {
|
||||
for (const auto &top : peers) {
|
||||
const auto user = [&]() -> Data::Utf8String {
|
||||
if (!top.peer.user() || top.peer.user()->isSelf) {
|
||||
return Data::Utf8String();
|
||||
} else if (top.peer.name().isEmpty()) {
|
||||
return "(deleted user)";
|
||||
}
|
||||
return top.peer.name();
|
||||
}();
|
||||
const auto chatType = [&] {
|
||||
if (const auto chat = top.peer.chat()) {
|
||||
return chat->username.isEmpty()
|
||||
? (chat->isBroadcast
|
||||
? "Private channel"
|
||||
: (chat->isSupergroup
|
||||
? "Private supergroup"
|
||||
: "Private group"))
|
||||
: (chat->isBroadcast
|
||||
? "Public channel"
|
||||
: "Public supergroup");
|
||||
}
|
||||
return "";
|
||||
}();
|
||||
const auto chat = [&]() -> Data::Utf8String {
|
||||
if (!top.peer.chat()) {
|
||||
return Data::Utf8String();
|
||||
} else if (top.peer.name().isEmpty()) {
|
||||
return "(deleted chat)";
|
||||
}
|
||||
return top.peer.name();
|
||||
}();
|
||||
const auto saved = [&]() -> Data::Utf8String {
|
||||
if (!top.peer.user() || !top.peer.user()->isSelf) {
|
||||
return Data::Utf8String();
|
||||
}
|
||||
return "Saved messages";
|
||||
}();
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "Category", category },
|
||||
{ "User", top.peer.user() ? user : QByteArray() },
|
||||
{ "Chat", saved },
|
||||
{ chatType, chat },
|
||||
{ "Rating", Data::NumberToString(top.rating) }
|
||||
}));
|
||||
}
|
||||
};
|
||||
writeList(data.correspondents, "People");
|
||||
writeList(data.inlineBots, "Inline bots");
|
||||
writeList(data.phoneCalls, "Calls");
|
||||
const auto full = _environment.aboutFrequent
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Frequent contacts "
|
||||
"(" + Data::NumberToString(size) + ") - lists/frequent.txt"
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeSessionsList(const Data::SessionsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (const auto result = writeSessions(data); !result) {
|
||||
return result;
|
||||
} else if (const auto result = writeWebSessions(data); !result) {
|
||||
return result;
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeSessions(const Data::SessionsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/sessions.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.list.size());
|
||||
for (const auto &session : data.list) {
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "Last active", Data::FormatDateTime(session.lastActive) },
|
||||
{ "Last IP address", session.ip },
|
||||
{ "Last country", session.country },
|
||||
{ "Last region", session.region },
|
||||
{
|
||||
"Application name",
|
||||
(session.applicationName.isEmpty()
|
||||
? Data::Utf8String("(unknown)")
|
||||
: session.applicationName)
|
||||
},
|
||||
{ "Application version", session.applicationVersion },
|
||||
{ "Device model", session.deviceModel },
|
||||
{ "Platform", session.platform },
|
||||
{ "System version", session.systemVersion },
|
||||
{ "Created", Data::FormatDateTime(session.created) },
|
||||
}));
|
||||
}
|
||||
const auto full = _environment.aboutSessions
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Sessions "
|
||||
"(" + Data::NumberToString(data.list.size()) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeWebSessions(const Data::SessionsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (data.webList.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/web_sessions.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.webList.size());
|
||||
for (const auto &session : data.webList) {
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "Last active", Data::FormatDateTime(session.lastActive) },
|
||||
{ "Last IP address", session.ip },
|
||||
{ "Last region", session.region },
|
||||
{
|
||||
"Bot username",
|
||||
(session.botUsername.isEmpty()
|
||||
? Data::Utf8String("(unknown)")
|
||||
: session.botUsername)
|
||||
},
|
||||
{
|
||||
"Domain name",
|
||||
(session.domain.isEmpty()
|
||||
? Data::Utf8String("(unknown)")
|
||||
: session.domain)
|
||||
},
|
||||
{ "Browser", session.browser },
|
||||
{ "Platform", session.platform },
|
||||
{ "Created", Data::FormatDateTime(session.created) },
|
||||
}));
|
||||
}
|
||||
const auto full = _environment.aboutWebSessions
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Web sessions "
|
||||
"(" + Data::NumberToString(data.webList.size()) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeOtherData(const Data::File &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
const auto header = "Other data - " + data.relativePath.toUtf8()
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogsStart(const Data::DialogsInfo &data) {
|
||||
_dialogsCount = data.chats.size();
|
||||
_leftChannelsCount = data.left.size();
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogStart(const Data::DialogInfo &data) {
|
||||
Expects(_chat == nullptr);
|
||||
|
||||
const auto result = validateDialogsMode(data.isLeftChannel);
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
_chat = fileWithRelativePath(data.relativePath + "messages.txt");
|
||||
_messagesCount = 0;
|
||||
_dialog = data;
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::validateDialogsMode(bool isLeftChannel) {
|
||||
const auto mode = isLeftChannel
|
||||
? DialogsMode::Left
|
||||
: DialogsMode::Chats;
|
||||
if (_dialogsMode == mode) {
|
||||
return Result::Success();
|
||||
} else if (_dialogsMode != DialogsMode::None) {
|
||||
if (const auto result = writeChatsEnd(); !result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
_dialogsMode = mode;
|
||||
return writeChatsStart(
|
||||
isLeftChannel ? _leftChannelsCount : _dialogsCount,
|
||||
isLeftChannel ? "Left chats" : "Chats",
|
||||
(isLeftChannel
|
||||
? _environment.aboutLeftChats
|
||||
: _environment.aboutChats),
|
||||
isLeftChannel ? "lists/left_chats.txt" : "lists/chats.txt");
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogSlice(const Data::MessagesSlice &data) {
|
||||
Expects(_chat != nullptr);
|
||||
Expects(!data.list.empty());
|
||||
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.list.size());
|
||||
for (const auto &message : data.list) {
|
||||
if (Data::SkipMessageByDate(message, _settings)) {
|
||||
continue;
|
||||
}
|
||||
list.push_back(SerializeMessage(
|
||||
message,
|
||||
data.peers,
|
||||
_environment.internalLinksDomain));
|
||||
++_messagesCount;
|
||||
}
|
||||
if (list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
const auto full = _chat->empty()
|
||||
? JoinList(kLineBreak, list)
|
||||
: kLineBreak + JoinList(kLineBreak, list);
|
||||
return _chat->writeBlock(full);
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogEnd() {
|
||||
Expects(_chats != nullptr);
|
||||
Expects(_chat != nullptr);
|
||||
|
||||
_chat = nullptr;
|
||||
|
||||
using Type = Data::DialogInfo::Type;
|
||||
const auto TypeString = [](Type type) {
|
||||
switch (type) {
|
||||
case Type::Unknown: return "(unknown)";
|
||||
case Type::Self:
|
||||
case Type::Replies:
|
||||
case Type::Personal: return "Personal chat";
|
||||
case Type::Bot: return "Bot chat";
|
||||
case Type::PrivateGroup: return "Private group";
|
||||
case Type::PrivateSupergroup: return "Private supergroup";
|
||||
case Type::PublicSupergroup: return "Public supergroup";
|
||||
case Type::PrivateChannel: return "Private channel";
|
||||
case Type::PublicChannel: return "Public channel";
|
||||
}
|
||||
Unexpected("Dialog type in TypeString.");
|
||||
};
|
||||
const auto NameString = [](
|
||||
const Data::DialogInfo &dialog,
|
||||
Type type) -> QByteArray {
|
||||
if (dialog.type == Type::Self) {
|
||||
return "Saved messages";
|
||||
} else if (dialog.type == Type::Replies) {
|
||||
return "Replies";
|
||||
}
|
||||
const auto name = dialog.name;
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
switch (type) {
|
||||
case Type::Unknown: return "(unknown)";
|
||||
case Type::Personal: return "(deleted user)";
|
||||
case Type::Bot: return "(deleted bot)";
|
||||
case Type::PrivateGroup:
|
||||
case Type::PrivateSupergroup:
|
||||
case Type::PublicSupergroup: return "(deleted group)";
|
||||
case Type::PrivateChannel:
|
||||
case Type::PublicChannel: return "(deleted channel)";
|
||||
}
|
||||
Unexpected("Dialog type in TypeString.");
|
||||
};
|
||||
return _chats->writeBlock(kLineBreak + SerializeKeyValue({
|
||||
{ "Name", NameString(_dialog, _dialog.type) },
|
||||
{ "Type", TypeString(_dialog.type) },
|
||||
{
|
||||
(_dialog.onlyMyMessages
|
||||
? "Outgoing messages count"
|
||||
: "Messages count"),
|
||||
Data::NumberToString(_messagesCount)
|
||||
},
|
||||
{
|
||||
"Content",
|
||||
(_messagesCount > 0
|
||||
? (_dialog.relativePath + "messages.txt").toUtf8()
|
||||
: QByteArray())
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogsEnd() {
|
||||
return writeChatsEnd();
|
||||
}
|
||||
|
||||
Result TextWriter::writeChatsStart(
|
||||
int count,
|
||||
const QByteArray &listName,
|
||||
const QByteArray &about,
|
||||
const QString &fileName) {
|
||||
Expects(_summary != nullptr);
|
||||
Expects(_chats == nullptr);
|
||||
|
||||
if (!count) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
_chats = fileWithRelativePath(fileName);
|
||||
|
||||
const auto block = about + kLineBreak;
|
||||
if (const auto result = _chats->writeBlock(block); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = listName + " "
|
||||
"(" + Data::NumberToString(count) + ") - "
|
||||
+ fileName.toUtf8()
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeChatsEnd() {
|
||||
_chats = nullptr;
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::finish() {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
QString TextWriter::mainFilePath() {
|
||||
return pathWithRelativePath(mainFileRelativePath());
|
||||
}
|
||||
|
||||
QString TextWriter::mainFileRelativePath() const {
|
||||
return "export_results.txt";
|
||||
}
|
||||
|
||||
QString TextWriter::pathWithRelativePath(const QString &path) const {
|
||||
return _settings.path + path;
|
||||
}
|
||||
|
||||
std::unique_ptr<File> TextWriter::fileWithRelativePath(
|
||||
const QString &path) const {
|
||||
return std::make_unique<File>(pathWithRelativePath(path), _stats);
|
||||
}
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
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 "export/output/export_output_abstract.h"
|
||||
#include "export/output/export_output_file.h"
|
||||
#include "export/export_settings.h"
|
||||
#include "export/data/export_data_types.h"
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
class TextWriter : public AbstractWriter {
|
||||
public:
|
||||
Format format() override {
|
||||
return Format::Text;
|
||||
}
|
||||
|
||||
Result start(
|
||||
const Settings &settings,
|
||||
const Environment &environment,
|
||||
Stats *stats) override;
|
||||
|
||||
Result writePersonal(const Data::PersonalInfo &data) override;
|
||||
|
||||
Result writeUserpicsStart(const Data::UserpicsInfo &data) override;
|
||||
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
|
||||
Result writeUserpicsEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
|
||||
Result writeOtherData(const Data::File &data) override;
|
||||
|
||||
Result writeDialogsStart(const Data::DialogsInfo &data) override;
|
||||
Result writeDialogStart(const Data::DialogInfo &data) override;
|
||||
Result writeDialogSlice(const Data::MessagesSlice &data) override;
|
||||
Result writeDialogEnd() override;
|
||||
Result writeDialogsEnd() override;
|
||||
|
||||
Result finish() override;
|
||||
|
||||
QString mainFilePath() override;
|
||||
|
||||
private:
|
||||
enum class DialogsMode {
|
||||
None,
|
||||
Chats,
|
||||
Left,
|
||||
};
|
||||
|
||||
[[nodiscard]] QString mainFileRelativePath() const;
|
||||
[[nodiscard]] QString pathWithRelativePath(const QString &path) const;
|
||||
[[nodiscard]] std::unique_ptr<File> fileWithRelativePath(
|
||||
const QString &path) const;
|
||||
|
||||
[[nodiscard]] Result writeSavedContacts(const Data::ContactsList &data);
|
||||
[[nodiscard]] Result writeFrequentContacts(const Data::ContactsList &data);
|
||||
|
||||
[[nodiscard]] Result writeSessions(const Data::SessionsList &data);
|
||||
[[nodiscard]] Result writeWebSessions(const Data::SessionsList &data);
|
||||
|
||||
[[nodiscard]] Result validateDialogsMode(bool isLeftChannel);
|
||||
[[nodiscard]] Result writeChatsStart(
|
||||
int count,
|
||||
const QByteArray &listName,
|
||||
const QByteArray &about,
|
||||
const QString &fileName);
|
||||
[[nodiscard]] Result writeChatsEnd();
|
||||
|
||||
Settings _settings;
|
||||
Environment _environment;
|
||||
Stats *_stats = nullptr;
|
||||
|
||||
std::unique_ptr<File> _summary;
|
||||
|
||||
int _userpicsCount = 0;
|
||||
std::unique_ptr<File> _userpics;
|
||||
|
||||
int _dialogsCount = 0;
|
||||
int _leftChannelsCount = 0;
|
||||
Data::DialogInfo _dialog;
|
||||
DialogsMode _dialogsMode = DialogsMode::None;
|
||||
|
||||
int _messagesCount = 0;
|
||||
std::unique_ptr<File> _chats;
|
||||
std::unique_ptr<File> _chat;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
@@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
@@ -591,12 +592,16 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
|
||||
p.setTextPalette(st::inTextPalette);
|
||||
App::roundRect(p, _botAbout->rect, st::msgInBg, MessageInCorners, &st::msgInShadow);
|
||||
|
||||
p.setFont(st::msgNameFont);
|
||||
p.setPen(st::dialogsNameFg);
|
||||
p.drawText(_botAbout->rect.left() + st::msgPadding.left(), _botAbout->rect.top() + st::msgPadding.top() + st::msgNameFont->ascent, tr::lng_bot_description(tr::now));
|
||||
auto top = _botAbout->rect.top() + st::msgPadding.top();
|
||||
if (!_history->peer->isRepliesChat()) {
|
||||
p.setFont(st::msgNameFont);
|
||||
p.setPen(st::dialogsNameFg);
|
||||
p.drawText(_botAbout->rect.left() + st::msgPadding.left(), top + st::msgNameFont->ascent, tr::lng_bot_description(tr::now));
|
||||
top += +st::msgNameFont->height + st::botDescSkip;
|
||||
}
|
||||
|
||||
p.setPen(st::historyTextInFg);
|
||||
_botAbout->info->text.draw(p, _botAbout->rect.left() + st::msgPadding.left(), _botAbout->rect.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip, _botAbout->width);
|
||||
_botAbout->info->text.draw(p, _botAbout->rect.left() + st::msgPadding.left(), top, _botAbout->width);
|
||||
|
||||
p.restoreTextPalette();
|
||||
}
|
||||
@@ -1546,7 +1551,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
const auto repliesCount = item->repliesCount();
|
||||
const auto withReplies = IsServerMsgId(item->id)
|
||||
&& (repliesCount > 0 || item->replyToTop());
|
||||
if (withReplies && item->history()->peer->isMegagroup()) {
|
||||
if (withReplies
|
||||
&& item->history()->peer->isMegagroup()
|
||||
&& item->history()->peer->asMegagroup()->linkedChat()) {
|
||||
const auto rootId = repliesCount ? item->id : item->replyToTop();
|
||||
const auto phrase = (repliesCount > 0)
|
||||
? tr::lng_replies_view(
|
||||
@@ -2131,13 +2138,19 @@ void HistoryInner::recountHistoryGeometry() {
|
||||
int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
|
||||
if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
|
||||
tw -= st::msgPadding.left() + st::msgPadding.right();
|
||||
int32 mw = qMax(_botAbout->info->text.maxWidth(), st::msgNameFont->width(tr::lng_bot_description(tr::now)));
|
||||
const auto descriptionWidth = _history->peer->isRepliesChat()
|
||||
? 0
|
||||
: st::msgNameFont->width(tr::lng_bot_description(tr::now));
|
||||
int32 mw = qMax(_botAbout->info->text.maxWidth(), descriptionWidth);
|
||||
if (tw > mw) tw = mw;
|
||||
|
||||
_botAbout->width = tw;
|
||||
_botAbout->height = _botAbout->info->text.countHeight(_botAbout->width);
|
||||
|
||||
int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
|
||||
const auto descriptionHeight = _history->peer->isRepliesChat()
|
||||
? 0
|
||||
: (st::msgNameFont->height + st::botDescSkip);
|
||||
int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
|
||||
int32 descMaxWidth = _scroll->width();
|
||||
if (Core::App().settings().chatWide()) {
|
||||
descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
|
||||
@@ -2178,7 +2191,10 @@ void HistoryInner::updateBotInfo(bool recount) {
|
||||
int32 tw = _scroll->width() - st::msgMargin.left() - st::msgMargin.right();
|
||||
if (tw > st::msgMaxWidth) tw = st::msgMaxWidth;
|
||||
tw -= st::msgPadding.left() + st::msgPadding.right();
|
||||
int32 mw = qMax(_botAbout->info->text.maxWidth(), st::msgNameFont->width(tr::lng_bot_description(tr::now)));
|
||||
const auto descriptionWidth = _history->peer->isRepliesChat()
|
||||
? 0
|
||||
: st::msgNameFont->width(tr::lng_bot_description(tr::now));
|
||||
int32 mw = qMax(_botAbout->info->text.maxWidth(), descriptionWidth);
|
||||
if (tw > mw) tw = mw;
|
||||
|
||||
_botAbout->width = tw;
|
||||
@@ -2194,7 +2210,10 @@ void HistoryInner::updateBotInfo(bool recount) {
|
||||
updateSize();
|
||||
}
|
||||
if (_botAbout->height > 0) {
|
||||
int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
|
||||
const auto descriptionHeight = _history->peer->isRepliesChat()
|
||||
? 0
|
||||
: (st::msgNameFont->height + st::botDescSkip);
|
||||
int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
|
||||
int32 descAtX = (_scroll->width() - _botAbout->width) / 2 - st::msgPadding.left();
|
||||
int32 descAtY = qMin(_historyPaddingTop - descH, (_scroll->height() - descH) / 2) + st::msgMargin.top();
|
||||
|
||||
@@ -2335,7 +2354,10 @@ void HistoryInner::updateSize() {
|
||||
}
|
||||
|
||||
if (_botAbout && _botAbout->height > 0) {
|
||||
int32 descH = st::msgMargin.top() + st::msgPadding.top() + st::msgNameFont->height + st::botDescSkip + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
|
||||
const auto descriptionHeight = _history->peer->isRepliesChat()
|
||||
? 0
|
||||
: (st::msgNameFont->height + st::botDescSkip);
|
||||
int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom();
|
||||
int32 descMaxWidth = _scroll->width();
|
||||
if (Core::App().settings().chatWide()) {
|
||||
descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
|
||||
@@ -3290,7 +3312,7 @@ QString HistoryInner::tooltipText() const {
|
||||
}
|
||||
}
|
||||
if (const auto msgsigned = view->data()->Get<HistoryMessageSigned>()) {
|
||||
if (msgsigned->isElided) {
|
||||
if (msgsigned->isElided && !msgsigned->isAnonymousRank) {
|
||||
dateText += '\n' + tr::lng_signed_author(tr::now, lt_user, msgsigned->author);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,8 +191,7 @@ TimeId HistoryItem::date() const {
|
||||
}
|
||||
|
||||
TimeId HistoryItem::NewMessageDate(TimeId scheduled) {
|
||||
const auto now = base::unixtime::now();
|
||||
return scheduled ? std::max(scheduled, now + 60) : now;
|
||||
return scheduled ? scheduled : base::unixtime::now();
|
||||
}
|
||||
|
||||
void HistoryItem::finishEdition(int oldKeyboardTop) {
|
||||
@@ -599,9 +598,7 @@ bool HistoryItem::canDeleteForEveryone(TimeId now) const {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!peer->isUser() && !toHistoryMessage()) {
|
||||
return false;
|
||||
} else if (const auto media = this->media()) {
|
||||
if (const auto media = this->media()) {
|
||||
if (!media->allowsRevoke(now)) {
|
||||
return false;
|
||||
}
|
||||
@@ -639,7 +636,7 @@ bool HistoryItem::suggestBanReport() const {
|
||||
if (!channel || !fromUser || !channel->canRestrictUser(fromUser)) {
|
||||
return false;
|
||||
}
|
||||
return !isPost() && !out() && toHistoryMessage();
|
||||
return !isPost() && !out();
|
||||
}
|
||||
|
||||
bool HistoryItem::suggestDeleteAllReport() const {
|
||||
@@ -647,7 +644,7 @@ bool HistoryItem::suggestDeleteAllReport() const {
|
||||
if (!channel || !channel->canDeleteMessages()) {
|
||||
return false;
|
||||
}
|
||||
return !isPost() && !out() && from()->isUser() && toHistoryMessage();
|
||||
return !isPost() && !out() && from()->isUser();
|
||||
}
|
||||
|
||||
bool HistoryItem::hasDirectLink() const {
|
||||
@@ -717,7 +714,9 @@ QString HistoryItem::authorOriginal() const {
|
||||
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
||||
return forwarded->originalAuthor;
|
||||
} else if (const auto msgsigned = Get<HistoryMessageSigned>()) {
|
||||
return msgsigned->author;
|
||||
if (!msgsigned->isAnonymousRank) {
|
||||
return msgsigned->author;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
@@ -287,10 +287,14 @@ public:
|
||||
}
|
||||
virtual void setReplies(const MTPMessageReplies &data) {
|
||||
}
|
||||
virtual void clearReplies() {
|
||||
}
|
||||
virtual void changeRepliesCount(int delta, PeerId replier) {
|
||||
}
|
||||
virtual void setReplyToTop(MsgId replyToTop) {
|
||||
}
|
||||
virtual void setPostAuthor(const QString &author) {
|
||||
}
|
||||
virtual void setRealId(MsgId newId);
|
||||
virtual void incrementReplyToTopCounter() {
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ void HistoryMessageVia::resize(int32 availw) const {
|
||||
}
|
||||
|
||||
void HistoryMessageSigned::refresh(const QString &date) {
|
||||
Expects(!isAnonymousRank);
|
||||
|
||||
auto name = author;
|
||||
const auto time = qsl(", ") + date;
|
||||
const auto timew = st::msgDateFont->width(time);
|
||||
|
||||
@@ -56,9 +56,10 @@ struct HistoryMessageSigned : public RuntimeComponent<HistoryMessageSigned, Hist
|
||||
void refresh(const QString &date);
|
||||
int maxWidth() const;
|
||||
|
||||
bool isElided = false;
|
||||
QString author;
|
||||
Ui::Text::String signature;
|
||||
bool isElided = false;
|
||||
bool isAnonymousRank = false;
|
||||
};
|
||||
|
||||
struct HistoryMessageEdited : public RuntimeComponent<HistoryMessageEdited, HistoryItem> {
|
||||
|
||||
@@ -374,18 +374,22 @@ MTPDmessage::Flags NewMessageFlags(not_null<PeerData*> peer) {
|
||||
return result;
|
||||
}
|
||||
|
||||
MsgId LookupReplyToTop(not_null<History*> history, MsgId replyToId) {
|
||||
const auto &owner = history->owner();
|
||||
if (const auto item = owner.message(history->channelId(), replyToId)) {
|
||||
return item->replyToTop();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
|
||||
if (const auto id = action.replyTo) {
|
||||
const auto history = action.history;
|
||||
const auto &owner = history->owner();
|
||||
if (const auto item = owner.message(history->channelId(), id)) {
|
||||
if (item->replyToId()) {
|
||||
return MTP_messageReplyHeader(
|
||||
MTP_flags(MTPDmessageReplyHeader::Flag::f_reply_to_top_id),
|
||||
MTP_int(id),
|
||||
MTPPeer(),
|
||||
MTP_int(item->replyToTop()));
|
||||
}
|
||||
if (const auto replyToTop = LookupReplyToTop(action.history, id)) {
|
||||
return MTP_messageReplyHeader(
|
||||
MTP_flags(MTPDmessageReplyHeader::Flag::f_reply_to_top_id),
|
||||
MTP_int(id),
|
||||
MTPPeer(),
|
||||
MTP_int(replyToTop));
|
||||
}
|
||||
return MTP_messageReplyHeader(
|
||||
MTP_flags(0),
|
||||
@@ -741,7 +745,9 @@ void HistoryMessage::createComponentsHelper(
|
||||
|
||||
if (flags & MTPDmessage::Flag::f_via_bot_id) config.viaBotId = viaBotId;
|
||||
if (flags & MTPDmessage::Flag::f_reply_to) {
|
||||
config.replyToTop = config.replyTo = replyTo;
|
||||
config.replyTo = replyTo;
|
||||
const auto replyToTop = LookupReplyToTop(history(), replyTo);
|
||||
config.replyToTop = replyToTop ? replyToTop : replyTo;
|
||||
}
|
||||
if (flags & MTPDmessage::Flag::f_reply_markup) config.mtpMarkup = &markup;
|
||||
if (flags & MTPDmessage::Flag::f_post_author) config.author = postAuthor;
|
||||
@@ -988,6 +994,14 @@ void HistoryMessage::createComponents(const CreateConfig &config) {
|
||||
}
|
||||
if (!config.author.isEmpty()) {
|
||||
mask |= HistoryMessageSigned::Bit();
|
||||
} else if (_history->peer->isMegagroup() // Discussion posts signatures.
|
||||
&& config.savedFromPeer
|
||||
&& !config.authorOriginal.isEmpty()) {
|
||||
const auto savedFrom = _history->owner().peerLoaded(
|
||||
config.savedFromPeer);
|
||||
if (savedFrom && savedFrom->isChannel()) {
|
||||
mask |= HistoryMessageSigned::Bit();
|
||||
}
|
||||
}
|
||||
if (config.editDate != TimeId(0)) {
|
||||
mask |= HistoryMessageEdited::Bit();
|
||||
@@ -1028,7 +1042,7 @@ void HistoryMessage::createComponents(const CreateConfig &config) {
|
||||
setViewsCount(config.viewsCount);
|
||||
if (config.mtpReplies) {
|
||||
setReplies(*config.mtpReplies);
|
||||
} else if (isSending()) {
|
||||
} else if (isSending() && !config.mtpMarkup) {
|
||||
if (const auto broadcast = history()->peer->asBroadcast()) {
|
||||
if (const auto linked = broadcast->linkedChat()) {
|
||||
setReplies(MTP_messageReplies(
|
||||
@@ -1048,7 +1062,11 @@ void HistoryMessage::createComponents(const CreateConfig &config) {
|
||||
edited->date = config.editDate;
|
||||
}
|
||||
if (const auto msgsigned = Get<HistoryMessageSigned>()) {
|
||||
msgsigned->author = config.author;
|
||||
msgsigned->author = config.author.isEmpty()
|
||||
? config.authorOriginal
|
||||
: config.author;
|
||||
msgsigned->isAnonymousRank = !isDiscussionPost()
|
||||
&& author()->isMegagroup();
|
||||
}
|
||||
setupForwardedComponent(config);
|
||||
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
|
||||
@@ -1298,6 +1316,11 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) {
|
||||
setViewsCount(message.vviews().value_or(-1));
|
||||
setForwardsCount(message.vforwards().value_or(-1));
|
||||
setText(_media ? textWithEntities : EnsureNonEmpty(textWithEntities));
|
||||
if (const auto replies = message.vreplies()) {
|
||||
setReplies(*replies);
|
||||
} else {
|
||||
clearReplies();
|
||||
}
|
||||
|
||||
finishEdition(keyboardTop);
|
||||
}
|
||||
@@ -1584,6 +1607,28 @@ void HistoryMessage::setViewsCount(int count) {
|
||||
void HistoryMessage::setForwardsCount(int count) {
|
||||
}
|
||||
|
||||
void HistoryMessage::setPostAuthor(const QString &author) {
|
||||
auto msgsigned = Get<HistoryMessageSigned>();
|
||||
if (author.isEmpty()) {
|
||||
if (!msgsigned) {
|
||||
return;
|
||||
}
|
||||
RemoveComponents(HistoryMessageSigned::Bit());
|
||||
history()->owner().requestItemResize(this);
|
||||
return;
|
||||
}
|
||||
if (!msgsigned) {
|
||||
AddComponents(HistoryMessageSigned::Bit());
|
||||
msgsigned = Get<HistoryMessageSigned>();
|
||||
} else if (msgsigned->author == author) {
|
||||
return;
|
||||
}
|
||||
msgsigned->author = author;
|
||||
msgsigned->isAnonymousRank = !isDiscussionPost()
|
||||
&& this->author()->isMegagroup();
|
||||
history()->owner().requestItemResize(this);
|
||||
}
|
||||
|
||||
void HistoryMessage::setReplies(const MTPMessageReplies &data) {
|
||||
data.match([&](const MTPDmessageReplies &data) {
|
||||
auto views = Get<HistoryMessageViews>();
|
||||
@@ -1631,6 +1676,21 @@ void HistoryMessage::setReplies(const MTPMessageReplies &data) {
|
||||
});
|
||||
}
|
||||
|
||||
void HistoryMessage::clearReplies() {
|
||||
auto views = Get<HistoryMessageViews>();
|
||||
if (!views) {
|
||||
return;
|
||||
}
|
||||
const auto viewsPart = views->views;
|
||||
if (viewsPart.count < 0) {
|
||||
RemoveComponents(HistoryMessageViews::Bit());
|
||||
} else {
|
||||
*views = HistoryMessageViews();
|
||||
views->views = viewsPart;
|
||||
}
|
||||
history()->owner().requestItemResize(this);
|
||||
}
|
||||
|
||||
void HistoryMessage::refreshRepliesText(
|
||||
not_null<HistoryMessageViews*> views,
|
||||
bool forceResize) {
|
||||
|
||||
@@ -21,16 +21,20 @@ struct HistoryMessageEdited;
|
||||
struct HistoryMessageReply;
|
||||
struct HistoryMessageViews;
|
||||
|
||||
Fn<void(ChannelData*, MsgId)> HistoryDependentItemCallback(
|
||||
[[nodiscard]] Fn<void(ChannelData*, MsgId)> HistoryDependentItemCallback(
|
||||
not_null<HistoryItem*> item);
|
||||
MTPDmessage::Flags NewMessageFlags(not_null<PeerData*> peer);
|
||||
MTPDmessage_ClientFlags NewMessageClientFlags();
|
||||
MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action);
|
||||
QString GetErrorTextForSending(
|
||||
[[nodiscard]] MTPDmessage::Flags NewMessageFlags(not_null<PeerData*> peer);
|
||||
[[nodiscard]] MTPDmessage_ClientFlags NewMessageClientFlags();
|
||||
[[nodiscard]] MsgId LookupReplyToTop(
|
||||
not_null<History*> history,
|
||||
MsgId replyToId);
|
||||
[[nodiscard]] MTPMessageReplyHeader NewMessageReplyHeader(
|
||||
const Api::SendAction &action);
|
||||
[[nodiscard]] QString GetErrorTextForSending(
|
||||
not_null<PeerData*> peer,
|
||||
const HistoryItemsList &items,
|
||||
bool ignoreSlowmodeCountdown = false);
|
||||
QString GetErrorTextForSending(
|
||||
[[nodiscard]] QString GetErrorTextForSending(
|
||||
not_null<PeerData*> peer,
|
||||
const HistoryItemsList &items,
|
||||
const TextWithTags &comment,
|
||||
@@ -126,8 +130,10 @@ public:
|
||||
void setViewsCount(int count) override;
|
||||
void setForwardsCount(int count) override;
|
||||
void setReplies(const MTPMessageReplies &data) override;
|
||||
void clearReplies() override;
|
||||
void changeRepliesCount(int delta, PeerId replier) override;
|
||||
void setReplyToTop(MsgId replyToTop) override;
|
||||
void setPostAuthor(const QString &author) override;
|
||||
void setRealId(MsgId newId) override;
|
||||
void incrementReplyToTopCounter() override;
|
||||
|
||||
|
||||
@@ -232,10 +232,6 @@ HistoryWidget::HistoryWidget(
|
||||
|
||||
initTabbedSelector();
|
||||
|
||||
connect(Media::Capture::instance(), SIGNAL(error()), this, SLOT(onRecordError()));
|
||||
connect(Media::Capture::instance(), SIGNAL(updated(quint16,qint32)), this, SLOT(onRecordUpdate(quint16,qint32)));
|
||||
connect(Media::Capture::instance(), SIGNAL(done(QByteArray,VoiceWaveform,qint32)), this, SLOT(onRecordDone(QByteArray,VoiceWaveform,qint32)));
|
||||
|
||||
_attachToggle->addClickHandler(App::LambdaDelayed(
|
||||
st::historyAttach.ripple.hideDuration,
|
||||
this,
|
||||
@@ -1367,15 +1363,13 @@ void HistoryWidget::setInnerFocus() {
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryWidget::onRecordError() {
|
||||
stopRecording(false);
|
||||
}
|
||||
|
||||
void HistoryWidget::onRecordDone(
|
||||
void HistoryWidget::recordDone(
|
||||
QByteArray result,
|
||||
VoiceWaveform waveform,
|
||||
qint32 samples) {
|
||||
if (!canWriteMessage() || result.isEmpty()) return;
|
||||
int samples) {
|
||||
if (!canWriteMessage() || result.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Window::ActivateWindow(controller());
|
||||
const auto duration = samples / Media::Player::kDefaultFrequency;
|
||||
@@ -1384,7 +1378,7 @@ void HistoryWidget::onRecordDone(
|
||||
session().api().sendVoiceMessage(result, waveform, duration, action);
|
||||
}
|
||||
|
||||
void HistoryWidget::onRecordUpdate(quint16 level, qint32 samples) {
|
||||
void HistoryWidget::recordUpdate(ushort level, int samples) {
|
||||
if (!_recording) {
|
||||
return;
|
||||
}
|
||||
@@ -1948,6 +1942,7 @@ void HistoryWidget::updateNotifyControls() {
|
||||
if (!session().data().notifySilentPostsUnknown(_peer)) {
|
||||
if (_silent) {
|
||||
_silent->setChecked(session().data().notifySilentPosts(_peer));
|
||||
updateFieldPlaceholder();
|
||||
} else if (hasSilentToggle()) {
|
||||
refreshSilentToggle();
|
||||
updateControlsVisibility();
|
||||
@@ -3138,10 +3133,6 @@ void HistoryWidget::toggleMuteUnmute() {
|
||||
session().data().updateNotifySettings(_peer, muteForSeconds);
|
||||
}
|
||||
|
||||
void HistoryWidget::onBroadcastSilentChange() {
|
||||
updateFieldPlaceholder();
|
||||
}
|
||||
|
||||
History *HistoryWidget::history() const {
|
||||
return _history;
|
||||
}
|
||||
@@ -3361,6 +3352,8 @@ void HistoryWidget::leaveToChildEvent(QEvent *e, QWidget *child) { // e -- from
|
||||
}
|
||||
|
||||
void HistoryWidget::recordStartCallback() {
|
||||
using namespace Media::Capture;
|
||||
|
||||
const auto error = _peer
|
||||
? Data::RestrictionError(_peer, ChatRestriction::f_send_media)
|
||||
: std::nullopt;
|
||||
@@ -3369,11 +3362,17 @@ void HistoryWidget::recordStartCallback() {
|
||||
return;
|
||||
} else if (showSlowmodeError()) {
|
||||
return;
|
||||
} else if (!Media::Capture::instance()->available()) {
|
||||
} else if (!instance()->available()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit Media::Capture::instance()->start();
|
||||
instance()->start();
|
||||
instance()->updated(
|
||||
) | rpl::start_with_next_error([=](const Update &update) {
|
||||
recordUpdate(update.level, update.samples);
|
||||
}, [=] {
|
||||
stopRecording(false);
|
||||
}, _recordingLifetime);
|
||||
|
||||
_recording = _inField = true;
|
||||
updateControlsVisibility();
|
||||
@@ -3403,11 +3402,20 @@ void HistoryWidget::mouseReleaseEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
void HistoryWidget::stopRecording(bool send) {
|
||||
emit Media::Capture::instance()->stop(send);
|
||||
if (send) {
|
||||
const auto weak = Ui::MakeWeak(this);
|
||||
Media::Capture::instance()->stop(crl::guard(this, [=](
|
||||
const Media::Capture::Result &result) {
|
||||
recordDone(result.bytes, result.waveform, result.samples);
|
||||
}));
|
||||
} else {
|
||||
Media::Capture::instance()->stop();
|
||||
}
|
||||
|
||||
_recordingLevel = anim::value();
|
||||
_recordingAnimation.stop();
|
||||
|
||||
_recordingLifetime.destroy();
|
||||
_recording = false;
|
||||
_recordingSamples = 0;
|
||||
if (_history) {
|
||||
@@ -3662,7 +3670,10 @@ bool HistoryWidget::isMuteUnmute() const {
|
||||
}
|
||||
|
||||
bool HistoryWidget::showRecordButton() const {
|
||||
return Media::Capture::instance()->available() && !HasSendText(_field) && !readyToForward() && !_editMsgId;
|
||||
return Media::Capture::instance()->available()
|
||||
&& !HasSendText(_field)
|
||||
&& !readyToForward()
|
||||
&& !_editMsgId;
|
||||
}
|
||||
|
||||
bool HistoryWidget::showInlineBotCancel() const {
|
||||
|
||||
@@ -294,8 +294,6 @@ signals:
|
||||
public slots:
|
||||
void onScroll();
|
||||
|
||||
void onBroadcastSilentChange();
|
||||
|
||||
void activate();
|
||||
void onTextChange();
|
||||
|
||||
@@ -312,10 +310,6 @@ public slots:
|
||||
void onDraftSave(bool delayed = false);
|
||||
void onCloudDraftSave();
|
||||
|
||||
void onRecordError();
|
||||
void onRecordDone(QByteArray result, VoiceWaveform waveform, qint32 samples);
|
||||
void onRecordUpdate(quint16 level, qint32 samples);
|
||||
|
||||
void onUpdateHistoryItems();
|
||||
|
||||
// checks if we are too close to the top or to the bottom
|
||||
@@ -412,6 +406,8 @@ private:
|
||||
|
||||
void animationCallback();
|
||||
void updateOverStates(QPoint pos);
|
||||
void recordDone(QByteArray result, VoiceWaveform waveform, int samples);
|
||||
void recordUpdate(ushort level, int samples);
|
||||
void recordStartCallback();
|
||||
void recordStopCallback(bool active);
|
||||
void recordUpdateCallback(QPoint globalPos);
|
||||
@@ -707,6 +703,7 @@ private:
|
||||
bool _inClickable = false;
|
||||
int _recordingSamples = 0;
|
||||
int _recordCancelWidth;
|
||||
rpl::lifetime _recordingLifetime;
|
||||
|
||||
// This can animate for a very long time (like in music playing),
|
||||
// so it should be a Basic, not a Simple animation.
|
||||
|
||||
@@ -130,6 +130,8 @@ private:
|
||||
const not_null<Data::Session*> _data;
|
||||
const not_null<Ui::IconButton*> _cancel;
|
||||
|
||||
QRect _clickableRect;
|
||||
|
||||
rpl::event_stream<bool> _visibleChanged;
|
||||
rpl::event_stream<FullMsgId> _scrollToItemRequests;
|
||||
|
||||
@@ -231,22 +233,16 @@ void FieldHeader::init() {
|
||||
events(
|
||||
) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return ranges::contains(kMouseEvents, event->type())
|
||||
&& isEditingMessage();
|
||||
&& (isEditingMessage() || replyingToMessage());
|
||||
}) | rpl::start_with_next([=](not_null<QEvent*> event) {
|
||||
const auto type = event->type();
|
||||
const auto e = static_cast<QMouseEvent*>(event.get());
|
||||
const auto pos = e ? e->pos() : mapFromGlobal(QCursor::pos());
|
||||
const auto inPreviewRect = QRect(
|
||||
st::historyReplySkip,
|
||||
0,
|
||||
width() - st::historyReplySkip - _cancel->width(),
|
||||
height()).contains(pos);
|
||||
const auto inPreviewRect = _clickableRect.contains(pos);
|
||||
|
||||
if (type == QEvent::MouseMove) {
|
||||
const auto inEdit = inPreviewRect;
|
||||
|
||||
if (inEdit != *inClickable) {
|
||||
*inClickable = inEdit;
|
||||
if (inPreviewRect != *inClickable) {
|
||||
*inClickable = inPreviewRect;
|
||||
setCursor(*inClickable
|
||||
? style::cur_pointer
|
||||
: style::cur_default);
|
||||
@@ -260,7 +256,10 @@ void FieldHeader::init() {
|
||||
*leftIconPressed = true;
|
||||
update();
|
||||
} else if (isLeftButton && inPreviewRect) {
|
||||
_scrollToItemRequests.fire(_editMsgId.current());
|
||||
auto id = isEditingMessage()
|
||||
? _editMsgId.current()
|
||||
: replyingToMessage();
|
||||
_scrollToItemRequests.fire(std::move(id));
|
||||
}
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
if (isLeftButton && *leftIconPressed) {
|
||||
@@ -451,6 +450,11 @@ WebPageId FieldHeader::webPageId() const {
|
||||
|
||||
void FieldHeader::updateControlsGeometry(QSize size) {
|
||||
_cancel->moveToRight(0, 0);
|
||||
_clickableRect = QRect(
|
||||
st::historyReplySkip,
|
||||
0,
|
||||
width() - st::historyReplySkip - _cancel->width(),
|
||||
height());
|
||||
}
|
||||
|
||||
void FieldHeader::editMessage(FullMsgId id) {
|
||||
@@ -732,6 +736,7 @@ void ComposeControls::showStarted() {
|
||||
_tabbedPanel->hideFast();
|
||||
}
|
||||
_wrap->hide();
|
||||
_writeRestricted->hide();
|
||||
}
|
||||
|
||||
void ComposeControls::showFinished() {
|
||||
@@ -741,7 +746,7 @@ void ComposeControls::showFinished() {
|
||||
if (_tabbedPanel) {
|
||||
_tabbedPanel->hideFast();
|
||||
}
|
||||
_wrap->show();
|
||||
updateWrappingVisibility();
|
||||
}
|
||||
|
||||
void ComposeControls::showForGrab() {
|
||||
@@ -786,25 +791,6 @@ void ComposeControls::init() {
|
||||
initSendButton();
|
||||
initWriteRestriction();
|
||||
|
||||
QObject::connect(
|
||||
::Media::Capture::instance(),
|
||||
&::Media::Capture::Instance::error,
|
||||
_wrap.get(),
|
||||
[=] { recordError(); });
|
||||
QObject::connect(
|
||||
::Media::Capture::instance(),
|
||||
&::Media::Capture::Instance::updated,
|
||||
_wrap.get(),
|
||||
[=](quint16 level, int samples) { recordUpdated(level, samples); });
|
||||
qRegisterMetaType<VoiceWaveform>();
|
||||
QObject::connect(
|
||||
::Media::Capture::instance(),
|
||||
&::Media::Capture::Instance::done,
|
||||
_wrap.get(),
|
||||
[=](QByteArray result, VoiceWaveform waveform, int samples) {
|
||||
recordDone(result, waveform, samples);
|
||||
});
|
||||
|
||||
_wrap->sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
updateControlsGeometry(size);
|
||||
@@ -855,10 +841,6 @@ void ComposeControls::init() {
|
||||
}
|
||||
}
|
||||
|
||||
void ComposeControls::recordError() {
|
||||
stopRecording(false);
|
||||
}
|
||||
|
||||
void ComposeControls::recordDone(
|
||||
QByteArray result,
|
||||
VoiceWaveform waveform,
|
||||
@@ -889,20 +871,26 @@ void ComposeControls::recordUpdated(quint16 level, int samples) {
|
||||
}
|
||||
|
||||
void ComposeControls::recordStartCallback() {
|
||||
//const auto error = _peer // #TODO restrictions
|
||||
// ? Data::RestrictionError(_peer, ChatRestriction::f_send_media)
|
||||
// : std::nullopt;
|
||||
const auto error = std::optional<QString>();
|
||||
using namespace ::Media::Capture;
|
||||
const auto error = _history
|
||||
? Data::RestrictionError(_history->peer, ChatRestriction::f_send_media)
|
||||
: std::nullopt;
|
||||
if (error) {
|
||||
Ui::show(Box<InformBox>(*error));
|
||||
return;
|
||||
} else if (_showSlowmodeError && _showSlowmodeError()) {
|
||||
return;
|
||||
} else if (!::Media::Capture::instance()->available()) {
|
||||
} else if (!instance()->available()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit ::Media::Capture::instance()->start();
|
||||
instance()->start();
|
||||
instance()->updated(
|
||||
) | rpl::start_with_next_error([=](const Update &update) {
|
||||
recordUpdated(update.level, update.samples);
|
||||
}, [=] {
|
||||
stopRecording(false);
|
||||
}, _recordingLifetime);
|
||||
|
||||
_recording = _inField = true;
|
||||
updateControlsVisibility();
|
||||
@@ -922,11 +910,19 @@ void ComposeControls::recordUpdateCallback(QPoint globalPos) {
|
||||
}
|
||||
|
||||
void ComposeControls::stopRecording(bool send) {
|
||||
emit ::Media::Capture::instance()->stop(send);
|
||||
if (send) {
|
||||
::Media::Capture::instance()->stop(crl::guard(_wrap.get(), [=](
|
||||
const ::Media::Capture::Result &result) {
|
||||
recordDone(result.bytes, result.waveform, result.samples);
|
||||
}));
|
||||
} else {
|
||||
::Media::Capture::instance()->stop();
|
||||
}
|
||||
|
||||
_recordingLevel = anim::value();
|
||||
_recordingAnimation.stop();
|
||||
|
||||
_recordingLifetime.destroy();
|
||||
_recording = false;
|
||||
_recordingSamples = 0;
|
||||
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
|
||||
@@ -1087,15 +1083,22 @@ void ComposeControls::initWriteRestriction() {
|
||||
}, _wrap->lifetime());
|
||||
|
||||
_writeRestriction.value(
|
||||
) | rpl::start_with_next([=](const std::optional<QString> &error) {
|
||||
_writeRestricted->setVisible(error.has_value());
|
||||
_wrap->setVisible(!error.has_value());
|
||||
if (!error.has_value()) {
|
||||
_wrap->raise();
|
||||
}
|
||||
) | rpl::filter([=] {
|
||||
return _wrap->isHidden() || _writeRestricted->isHidden();
|
||||
}) | rpl::start_with_next([=] {
|
||||
updateWrappingVisibility();
|
||||
}, _wrap->lifetime());
|
||||
}
|
||||
|
||||
void ComposeControls::updateWrappingVisibility() {
|
||||
const auto restricted = _writeRestriction.current().has_value();
|
||||
_writeRestricted->setVisible(restricted);
|
||||
_wrap->setVisible(!restricted);
|
||||
if (!restricted) {
|
||||
_wrap->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void ComposeControls::updateSendButtonType() {
|
||||
using Type = Ui::SendButton::Type;
|
||||
const auto type = [&] {
|
||||
|
||||
@@ -163,6 +163,7 @@ private:
|
||||
void initWriteRestriction();
|
||||
void updateSendButtonType();
|
||||
void updateHeight();
|
||||
void updateWrappingVisibility();
|
||||
void updateControlsVisibility();
|
||||
void updateControlsGeometry(QSize size);
|
||||
void updateOuterGeometry(QRect rect);
|
||||
@@ -176,7 +177,6 @@ private:
|
||||
|
||||
void setTextFromEditingMessage(not_null<HistoryItem*> item);
|
||||
|
||||
void recordError();
|
||||
void recordUpdated(quint16 level, int samples);
|
||||
void recordDone(QByteArray result, VoiceWaveform waveform, int samples);
|
||||
|
||||
@@ -230,6 +230,7 @@ private:
|
||||
//bool _inClickable = false;
|
||||
int _recordingSamples = 0;
|
||||
int _recordCancelWidth;
|
||||
rpl::lifetime _recordingLifetime;
|
||||
|
||||
rpl::lifetime _uploaderSubscriptions;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "chat_helpers/send_context_menu.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "boxes/sticker_set_box.h"
|
||||
#include "boxes/report_box.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_document.h"
|
||||
@@ -607,6 +608,34 @@ void AddDeleteAction(
|
||||
}
|
||||
}
|
||||
|
||||
void AddReportAction(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
const ContextMenuRequest &request,
|
||||
not_null<ListWidget*> list) {
|
||||
const auto item = request.item;
|
||||
if (!request.selectedItems.empty()) {
|
||||
return;
|
||||
} else if (!item || !item->suggestReport()) {
|
||||
return;
|
||||
}
|
||||
const auto owner = &item->history()->owner();
|
||||
const auto asGroup = (request.pointState != PointState::GroupPart);
|
||||
const auto controller = list->controller();
|
||||
const auto itemId = item->fullId();
|
||||
const auto callback = crl::guard(controller, [=] {
|
||||
if (const auto item = owner->message(itemId)) {
|
||||
const auto peer = item->history()->peer;
|
||||
const auto group = owner->groups().find(item);
|
||||
Ui::show(Box<ReportBox>(
|
||||
peer,
|
||||
(group
|
||||
? owner->itemsToIds(group->items)
|
||||
: MessageIdsList(1, itemId))));
|
||||
}
|
||||
});
|
||||
menu->addAction(tr::lng_context_report_msg(tr::now), callback);
|
||||
}
|
||||
|
||||
bool AddClearSelectionAction(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
const ContextMenuRequest &request,
|
||||
@@ -674,6 +703,7 @@ void AddMessageActions(
|
||||
AddForwardAction(menu, request, list);
|
||||
AddSendNowAction(menu, request, list);
|
||||
AddDeleteAction(menu, request, list);
|
||||
AddReportAction(menu, request, list);
|
||||
AddSelectionAction(menu, request, list);
|
||||
AddRescheduleMessageAction(menu, request);
|
||||
}
|
||||
|
||||
@@ -262,6 +262,9 @@ public:
|
||||
virtual bool hasOutLayout() const;
|
||||
virtual bool drawBubble() const;
|
||||
virtual bool hasBubble() const;
|
||||
virtual int minWidthForMedia() const {
|
||||
return 0;
|
||||
}
|
||||
virtual bool hasFastReply() const;
|
||||
virtual bool displayFastReply() const;
|
||||
virtual std::optional<QSize> rightActionSize() const;
|
||||
|
||||
@@ -336,12 +336,16 @@ void ListWidget::refreshRows() {
|
||||
|
||||
_items.clear();
|
||||
_items.reserve(_slice.ids.size());
|
||||
auto nearestIndex = -1;
|
||||
for (const auto &fullId : _slice.ids) {
|
||||
if (const auto item = session().data().message(fullId)) {
|
||||
if (_slice.nearestToAround == fullId) {
|
||||
nearestIndex = int(_items.size());
|
||||
}
|
||||
_items.push_back(enforceViewForItem(item));
|
||||
}
|
||||
}
|
||||
updateAroundPositionFromRows();
|
||||
updateAroundPositionFromNearest(nearestIndex);
|
||||
|
||||
updateItemsGeometry();
|
||||
checkUnreadBarCreation();
|
||||
@@ -573,8 +577,7 @@ not_null<Element*> ListWidget::enforceViewForItem(
|
||||
return i->second.get();
|
||||
}
|
||||
|
||||
void ListWidget::updateAroundPositionFromRows() {
|
||||
const auto nearestIndex = findNearestItem(_aroundPosition);
|
||||
void ListWidget::updateAroundPositionFromNearest(int nearestIndex) {
|
||||
if (nearestIndex < 0) {
|
||||
_aroundIndex = -1;
|
||||
return;
|
||||
@@ -2091,7 +2094,8 @@ void ListWidget::mouseActionStart(
|
||||
if (isPressInSelectedText(dragState)) {
|
||||
_mouseAction = MouseAction::PrepareDrag; // start text drag
|
||||
} else if (!_pressWasInactive) {
|
||||
if (requiredToStartDragging(pressElement)) {
|
||||
if (requiredToStartDragging(pressElement)
|
||||
&& _pressState.pointState != PointState::Outside) {
|
||||
_mouseAction = MouseAction::PrepareDrag;
|
||||
} else {
|
||||
if (dragState.afterSymbol) ++_mouseTextSymbol;
|
||||
|
||||
@@ -305,7 +305,7 @@ private:
|
||||
using CursorState = HistoryView::CursorState;
|
||||
|
||||
void refreshViewer();
|
||||
void updateAroundPositionFromRows();
|
||||
void updateAroundPositionFromNearest(int nearestIndex);
|
||||
void refreshRows();
|
||||
ScrollTopState countScrollState() const;
|
||||
void saveScrollState();
|
||||
|
||||
@@ -247,6 +247,11 @@ void Message::refreshRightBadge() {
|
||||
return (delegate()->elementContext() == Context::Replies)
|
||||
? QString()
|
||||
: tr::lng_channel_badge(tr::now);
|
||||
} else if (data()->author()->isMegagroup()) {
|
||||
if (const auto msgsigned = data()->Get<HistoryMessageSigned>()) {
|
||||
Assert(msgsigned->isAnonymousRank);
|
||||
return msgsigned->author;
|
||||
}
|
||||
}
|
||||
const auto channel = data()->history()->peer->asMegagroup();
|
||||
const auto user = data()->author()->asUser();
|
||||
@@ -281,7 +286,6 @@ void Message::applyGroupAdminChanges(
|
||||
const base::flat_set<UserId> &changes) {
|
||||
if (!data()->out()
|
||||
&& changes.contains(peerToUser(data()->author()->id))) {
|
||||
refreshRightBadge();
|
||||
history()->owner().requestViewResize(this);
|
||||
}
|
||||
}
|
||||
@@ -295,6 +299,7 @@ QSize Message::performCountOptimalSize() {
|
||||
|
||||
updateMediaInBubbleState();
|
||||
refreshEditedBadge();
|
||||
refreshRightBadge();
|
||||
|
||||
auto mediaOnBottom = (logEntryOriginal() != nullptr)
|
||||
|| (media && media->isDisplayed() && media->isBubbleBottom());
|
||||
@@ -416,30 +421,7 @@ QSize Message::performCountOptimalSize() {
|
||||
minHeight += entry->minHeight();
|
||||
}
|
||||
}
|
||||
if (item->repliesAreComments() && !views->replies.text.isEmpty()) {
|
||||
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
|
||||
const auto single = st::historyCommentsUserpicSize;
|
||||
const auto shift = st::historyCommentsUserpicOverlap;
|
||||
const auto added = single
|
||||
+ (limit - 1) * (single - shift)
|
||||
+ st::historyCommentsSkipLeft
|
||||
+ st::historyCommentsSkipRight
|
||||
+ st::historyCommentsSkipText
|
||||
+ st::historyCommentsOpenOutSelected.width()
|
||||
+ st::historyCommentsSkipRight
|
||||
+ st::mediaUnreadSkip
|
||||
+ st::mediaUnreadSize;
|
||||
accumulate_max(maxWidth, added + views->replies.textWidth);
|
||||
} else if (item->externalReply()) {
|
||||
const auto added = st::historyCommentsIn.width()
|
||||
+ st::historyCommentsSkipLeft
|
||||
+ st::historyCommentsSkipRight
|
||||
+ st::historyCommentsSkipText
|
||||
+ st::historyCommentsOpenOutSelected.width()
|
||||
+ st::historyCommentsSkipRight;
|
||||
accumulate_max(maxWidth, added + st::semiboldFont->width(
|
||||
tr::lng_replies_view_original(tr::now)));
|
||||
}
|
||||
accumulate_max(maxWidth, minWidthForMedia());
|
||||
} else if (media) {
|
||||
media->initDimensions();
|
||||
maxWidth = media->maxWidth();
|
||||
@@ -560,14 +542,14 @@ void Message::draw(
|
||||
auto displayTail = skipTail ? RectPart::None : (outbg && !Core::App().settings().chatWide()) ? RectPart::Right : RectPart::Left;
|
||||
PaintBubble(p, g, width(), selected, outbg, displayTail);
|
||||
|
||||
const auto gBubble = g;
|
||||
paintCommentsButton(p, g, selected);
|
||||
auto inner = g;
|
||||
paintCommentsButton(p, inner, selected);
|
||||
|
||||
// Entry page is always a bubble bottom.
|
||||
auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/);
|
||||
auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());
|
||||
|
||||
auto trect = g.marginsRemoved(st::msgPadding);
|
||||
auto trect = inner.marginsRemoved(st::msgPadding);
|
||||
if (mediaOnBottom) {
|
||||
trect.setHeight(trect.height() + st::msgPadding.bottom());
|
||||
}
|
||||
@@ -585,7 +567,7 @@ void Message::draw(
|
||||
paintText(p, trect, selection);
|
||||
if (mediaDisplayed) {
|
||||
auto mediaHeight = media->height();
|
||||
auto mediaLeft = g.left();
|
||||
auto mediaLeft = inner.left();
|
||||
auto mediaTop = (trect.y() + trect.height() - mediaHeight);
|
||||
|
||||
p.translate(mediaLeft, mediaTop);
|
||||
@@ -593,7 +575,7 @@ void Message::draw(
|
||||
p.translate(-mediaLeft, -mediaTop);
|
||||
}
|
||||
if (entry) {
|
||||
auto entryLeft = g.left();
|
||||
auto entryLeft = inner.left();
|
||||
auto entryTop = trect.y() + trect.height();
|
||||
p.translate(entryLeft, entryTop);
|
||||
auto entrySelection = skipTextSelection(selection);
|
||||
@@ -609,29 +591,29 @@ void Message::draw(
|
||||
? !media->customInfoLayout()
|
||||
: true);
|
||||
if (needDrawInfo) {
|
||||
drawInfo(p, g.left() + g.width(), g.top() + g.height(), 2 * g.left() + g.width(), selected, InfoDisplayType::Default);
|
||||
if (g != gBubble) {
|
||||
drawInfo(p, inner.left() + inner.width(), inner.top() + inner.height(), 2 * inner.left() + inner.width(), selected, InfoDisplayType::Default);
|
||||
if (g != inner) {
|
||||
const auto o = p.opacity();
|
||||
p.setOpacity(0.3);
|
||||
const auto color = selected
|
||||
? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected)
|
||||
: (outbg ? st::msgOutDateFg : st::msgInDateFg);
|
||||
p.fillRect(g.left(), g.top() + g.height() - st::lineWidth, g.width(), st::lineWidth, color);
|
||||
p.fillRect(inner.left(), inner.top() + inner.height() - st::lineWidth, inner.width(), st::lineWidth, color);
|
||||
p.setOpacity(o);
|
||||
}
|
||||
}
|
||||
if (const auto size = rightActionSize()) {
|
||||
const auto fastShareSkip = std::clamp(
|
||||
(gBubble.height() - size->height()) / 2,
|
||||
(g.height() - size->height()) / 2,
|
||||
0,
|
||||
st::historyFastShareBottom);
|
||||
const auto fastShareLeft = g.left() + g.width() + st::historyFastShareLeft;
|
||||
const auto fastShareTop = g.top() + gBubble.height() - fastShareSkip - size->height();
|
||||
const auto fastShareTop = g.top() + g.height() - fastShareSkip - size->height();
|
||||
drawRightAction(p, fastShareLeft, fastShareTop, width());
|
||||
}
|
||||
|
||||
if (media) {
|
||||
media->paintBubbleFireworks(p, gBubble, ms);
|
||||
media->paintBubbleFireworks(p, g, ms);
|
||||
}
|
||||
} else if (media && media->isDisplayed()) {
|
||||
p.translate(g.topLeft());
|
||||
@@ -752,8 +734,6 @@ void Message::paintCommentsButton(
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(st::historyCommentsUserpicStroke);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.setPen(pen);
|
||||
auto x = (count - 1) * (single - shift);
|
||||
for (auto i = count; i != 0;) {
|
||||
auto &entry = list[--i];
|
||||
@@ -761,6 +741,8 @@ void Message::paintCommentsButton(
|
||||
entry.peer->paintUserpic(q, entry.view, x, 0, single);
|
||||
entry.uniqueKey = entry.peer->userpicUniqueKey(entry.view);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(x, 0, single, single);
|
||||
x -= single - shift;
|
||||
}
|
||||
@@ -800,74 +782,77 @@ void Message::paintFromName(
|
||||
QRect &trect,
|
||||
bool selected) const {
|
||||
const auto item = message();
|
||||
if (displayFromName()) {
|
||||
const auto badgeWidth = _rightBadge.isEmpty() ? 0 : _rightBadge.maxWidth();
|
||||
const auto replyWidth = [&] {
|
||||
if (isUnderCursor() && displayFastReply()) {
|
||||
return st::msgFont->width(FastReplyText());
|
||||
}
|
||||
return 0;
|
||||
}();
|
||||
const auto rightWidth = replyWidth ? replyWidth : badgeWidth;
|
||||
auto availableLeft = trect.left();
|
||||
auto availableWidth = trect.width();
|
||||
if (rightWidth) {
|
||||
availableWidth -= st::msgPadding.right() + rightWidth;
|
||||
if (!displayFromName()) {
|
||||
return;
|
||||
}
|
||||
const auto badgeWidth = _rightBadge.isEmpty() ? 0 : _rightBadge.maxWidth();
|
||||
const auto replyWidth = [&] {
|
||||
if (isUnderCursor() && displayFastReply()) {
|
||||
return st::msgFont->width(FastReplyText());
|
||||
}
|
||||
return 0;
|
||||
}();
|
||||
const auto rightWidth = replyWidth ? replyWidth : badgeWidth;
|
||||
auto availableLeft = trect.left();
|
||||
auto availableWidth = trect.width();
|
||||
if (rightWidth) {
|
||||
availableWidth -= st::msgPadding.right() + rightWidth;
|
||||
}
|
||||
|
||||
p.setFont(st::msgNameFont);
|
||||
const auto nameText = [&]() -> const Ui::Text::String * {
|
||||
const auto from = item->displayFrom();
|
||||
if (hasOutLayout()) {
|
||||
p.setPen(selected ? st::msgOutServiceFgSelected : st::msgOutServiceFg);
|
||||
return &from->nameText();
|
||||
} else if (item->isPost()) {
|
||||
p.setPen(selected ? st::msgInServiceFgSelected : st::msgInServiceFg);
|
||||
return &from->nameText();
|
||||
} else if (from) {
|
||||
p.setPen(FromNameFg(from->id, selected));
|
||||
return &from->nameText();
|
||||
} else if (const auto info = item->hiddenForwardedInfo()) {
|
||||
p.setPen(FromNameFg(info->colorPeerId, selected));
|
||||
return &info->nameText;
|
||||
} else {
|
||||
Unexpected("Corrupt forwarded information in message.");
|
||||
}
|
||||
}();
|
||||
nameText->drawElided(p, availableLeft, trect.top(), availableWidth);
|
||||
const auto skipWidth = nameText->maxWidth() + st::msgServiceFont->spacew;
|
||||
p.setFont(st::msgNameFont);
|
||||
const auto outbg = hasOutLayout();
|
||||
const auto nameText = [&]() -> const Ui::Text::String * {
|
||||
const auto from = item->displayFrom();
|
||||
if (outbg) {
|
||||
p.setPen(selected ? st::msgOutServiceFgSelected : st::msgOutServiceFg);
|
||||
return &from->nameText();
|
||||
} else if (item->isPost()) {
|
||||
p.setPen(selected ? st::msgInServiceFgSelected : st::msgInServiceFg);
|
||||
return &from->nameText();
|
||||
} else if (from) {
|
||||
p.setPen(FromNameFg(from->id, selected));
|
||||
return &from->nameText();
|
||||
} else if (const auto info = item->hiddenForwardedInfo()) {
|
||||
p.setPen(FromNameFg(info->colorPeerId, selected));
|
||||
return &info->nameText;
|
||||
} else {
|
||||
Unexpected("Corrupt forwarded information in message.");
|
||||
}
|
||||
}();
|
||||
nameText->drawElided(p, availableLeft, trect.top(), availableWidth);
|
||||
const auto skipWidth = nameText->maxWidth() + st::msgServiceFont->spacew;
|
||||
availableLeft += skipWidth;
|
||||
availableWidth -= skipWidth;
|
||||
|
||||
auto via = item->Get<HistoryMessageVia>();
|
||||
if (via && !displayForwardedFrom() && availableWidth > 0) {
|
||||
p.setPen(selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg));
|
||||
p.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, via->text);
|
||||
auto skipWidth = via->width + st::msgServiceFont->spacew;
|
||||
availableLeft += skipWidth;
|
||||
availableWidth -= skipWidth;
|
||||
|
||||
auto via = item->Get<HistoryMessageVia>();
|
||||
if (via && !displayForwardedFrom() && availableWidth > 0) {
|
||||
const auto outbg = hasOutLayout();
|
||||
p.setPen(selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg));
|
||||
p.drawText(availableLeft, trect.top() + st::msgServiceFont->ascent, via->text);
|
||||
auto skipWidth = via->width + st::msgServiceFont->spacew;
|
||||
availableLeft += skipWidth;
|
||||
availableWidth -= skipWidth;
|
||||
}
|
||||
if (rightWidth) {
|
||||
p.setPen(selected ? st::msgInDateFgSelected : st::msgInDateFg);
|
||||
p.setFont(ClickHandler::showAsActive(_fastReplyLink)
|
||||
? st::msgFont->underline()
|
||||
: st::msgFont);
|
||||
if (replyWidth) {
|
||||
p.drawText(
|
||||
trect.left() + trect.width() - rightWidth,
|
||||
trect.top() + st::msgFont->ascent,
|
||||
FastReplyText());
|
||||
} else {
|
||||
_rightBadge.draw(
|
||||
p,
|
||||
trect.left() + trect.width() - rightWidth,
|
||||
trect.top(),
|
||||
rightWidth);
|
||||
}
|
||||
}
|
||||
trect.setY(trect.y() + st::msgNameFont->height);
|
||||
}
|
||||
if (rightWidth) {
|
||||
p.setPen(outbg
|
||||
? (selected ? st::msgOutDateFgSelected : st::msgOutDateFg)
|
||||
: (selected ? st::msgInDateFgSelected : st::msgInDateFg));
|
||||
p.setFont(ClickHandler::showAsActive(_fastReplyLink)
|
||||
? st::msgFont->underline()
|
||||
: st::msgFont);
|
||||
if (replyWidth) {
|
||||
p.drawText(
|
||||
trect.left() + trect.width() - rightWidth,
|
||||
trect.top() + st::msgFont->ascent,
|
||||
FastReplyText());
|
||||
} else {
|
||||
_rightBadge.draw(
|
||||
p,
|
||||
trect.left() + trect.width() - rightWidth,
|
||||
trect.top(),
|
||||
rightWidth);
|
||||
}
|
||||
}
|
||||
trect.setY(trect.y() + st::msgNameFont->height);
|
||||
}
|
||||
|
||||
void Message::paintForwardedInfo(Painter &p, QRect &trect, bool selected) const {
|
||||
@@ -1166,12 +1151,12 @@ TextState Message::textState(
|
||||
auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/);
|
||||
auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());
|
||||
|
||||
const auto gBubble = g;
|
||||
if (getStateCommentsButton(point, g, &result)) {
|
||||
auto bubble = g;
|
||||
if (getStateCommentsButton(point, bubble, &result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto trect = g.marginsRemoved(st::msgPadding);
|
||||
auto trect = bubble.marginsRemoved(st::msgPadding);
|
||||
if (mediaOnBottom) {
|
||||
trect.setHeight(trect.height() + st::msgPadding.bottom());
|
||||
}
|
||||
@@ -1194,7 +1179,7 @@ TextState Message::textState(
|
||||
if (entry) {
|
||||
auto entryHeight = entry->height();
|
||||
trect.setHeight(trect.height() - entryHeight);
|
||||
auto entryLeft = g.left();
|
||||
auto entryLeft = bubble.left();
|
||||
auto entryTop = trect.y() + trect.height();
|
||||
if (point.y() >= entryTop && point.y() < entryTop + entryHeight) {
|
||||
result = entry->textState(
|
||||
@@ -1209,8 +1194,8 @@ TextState Message::textState(
|
||||
return;
|
||||
}
|
||||
const auto inDate = pointInTime(
|
||||
g.left() + g.width(),
|
||||
g.top() + g.height(),
|
||||
bubble.left() + bubble.width(),
|
||||
bubble.top() + bubble.height(),
|
||||
point,
|
||||
InfoDisplayType::Default);
|
||||
if (inDate) {
|
||||
@@ -1242,11 +1227,11 @@ TextState Message::textState(
|
||||
checkForPointInTime();
|
||||
if (const auto size = rightActionSize()) {
|
||||
const auto fastShareSkip = snap(
|
||||
(gBubble.height() - size->height()) / 2,
|
||||
(g.height() - size->height()) / 2,
|
||||
0,
|
||||
st::historyFastShareBottom);
|
||||
const auto fastShareLeft = g.left() + g.width() + st::historyFastShareLeft;
|
||||
const auto fastShareTop = g.top() + gBubble.height() - fastShareSkip - size->height();
|
||||
const auto fastShareTop = g.top() + g.height() - fastShareSkip - size->height();
|
||||
if (QRect(
|
||||
fastShareLeft,
|
||||
fastShareTop,
|
||||
@@ -1304,7 +1289,15 @@ ClickHandlerPtr Message::createGoToCommentsLink() const {
|
||||
if (const auto window = App::wnd()) {
|
||||
if (const auto controller = window->sessionController()) {
|
||||
if (const auto item = controller->session().data().message(fullId)) {
|
||||
controller->showRepliesForMessage(item->history(), item->id);
|
||||
const auto history = item->history();
|
||||
if (const auto channel = history->peer->asChannel()) {
|
||||
if (channel->invitePeekExpires()) {
|
||||
Ui::Toast::Show(
|
||||
tr::lng_channel_invite_private(tr::now));
|
||||
return;
|
||||
}
|
||||
}
|
||||
controller->showRepliesForMessage(history, item->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1702,7 +1695,8 @@ void Message::drawInfo(
|
||||
}
|
||||
dateX += timeLeft();
|
||||
|
||||
if (const auto msgsigned = item->Get<HistoryMessageSigned>()) {
|
||||
if (const auto msgsigned = item->Get<HistoryMessageSigned>()
|
||||
; msgsigned && !msgsigned->isAnonymousRank) {
|
||||
msgsigned->signature.drawElided(p, dateX, dateY, item->_timeWidth);
|
||||
} else if (const auto edited = displayedEditBadge()) {
|
||||
edited->text.drawElided(p, dateX, dateY, item->_timeWidth);
|
||||
@@ -2014,6 +2008,36 @@ bool Message::hasBubble() const {
|
||||
return drawBubble();
|
||||
}
|
||||
|
||||
int Message::minWidthForMedia() const {
|
||||
auto result = infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x());
|
||||
const auto views = data()->Get<HistoryMessageViews>();
|
||||
if (data()->repliesAreComments() && !views->replies.text.isEmpty()) {
|
||||
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
|
||||
const auto single = st::historyCommentsUserpicSize;
|
||||
const auto shift = st::historyCommentsUserpicOverlap;
|
||||
const auto added = single
|
||||
+ (limit - 1) * (single - shift)
|
||||
+ st::historyCommentsSkipLeft
|
||||
+ st::historyCommentsSkipRight
|
||||
+ st::historyCommentsSkipText
|
||||
+ st::historyCommentsOpenOutSelected.width()
|
||||
+ st::historyCommentsSkipRight
|
||||
+ st::mediaUnreadSkip
|
||||
+ st::mediaUnreadSize;
|
||||
accumulate_max(result, added + views->replies.textWidth);
|
||||
} else if (data()->externalReply()) {
|
||||
const auto added = st::historyCommentsIn.width()
|
||||
+ st::historyCommentsSkipLeft
|
||||
+ st::historyCommentsSkipRight
|
||||
+ st::historyCommentsSkipText
|
||||
+ st::historyCommentsOpenOutSelected.width()
|
||||
+ st::historyCommentsSkipRight;
|
||||
accumulate_max(result, added + st::semiboldFont->width(
|
||||
tr::lng_replies_view_original(tr::now)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Message::hasFastReply() const {
|
||||
if (context() == Context::Replies) {
|
||||
if (data()->isDiscussionPost()) {
|
||||
@@ -2380,7 +2404,11 @@ int Message::resizeContentGetHeight(int newWidth) {
|
||||
const auto bubble = drawBubble();
|
||||
|
||||
// This code duplicates countGeometry() but also resizes media.
|
||||
auto contentWidth = newWidth - (st::msgMargin.left() + st::msgMargin.right());
|
||||
const auto commentsRoot = (context() == Context::Replies)
|
||||
&& data()->isDiscussionPost();
|
||||
auto contentWidth = newWidth
|
||||
- st::msgMargin.left()
|
||||
- (commentsRoot ? st::msgMargin.left() : st::msgMargin.right());
|
||||
if (hasFromPhoto()) {
|
||||
if (const auto size = rightActionSize()) {
|
||||
contentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize);
|
||||
@@ -2517,17 +2545,20 @@ void Message::refreshEditedBadge() {
|
||||
edited->refresh(dateText, editDate != 0);
|
||||
}
|
||||
if (const auto msgsigned = item->Get<HistoryMessageSigned>()) {
|
||||
const auto text = (!edited || !editDate)
|
||||
? dateText
|
||||
: edited->text.toString();
|
||||
msgsigned->refresh(text);
|
||||
if (!msgsigned->isAnonymousRank) {
|
||||
const auto text = (!edited || !editDate)
|
||||
? dateText
|
||||
: edited->text.toString();
|
||||
msgsigned->refresh(text);
|
||||
}
|
||||
}
|
||||
initTime();
|
||||
}
|
||||
|
||||
void Message::initTime() {
|
||||
const auto item = message();
|
||||
if (const auto msgsigned = item->Get<HistoryMessageSigned>()) {
|
||||
if (const auto msgsigned = item->Get<HistoryMessageSigned>()
|
||||
; msgsigned && !msgsigned->isAnonymousRank) {
|
||||
item->_timeWidth = msgsigned->maxWidth();
|
||||
} else if (const auto edited = displayedEditBadge()) {
|
||||
item->_timeWidth = edited->maxWidth();
|
||||
|
||||
@@ -91,6 +91,7 @@ public:
|
||||
bool hasOutLayout() const override;
|
||||
bool drawBubble() const override;
|
||||
bool hasBubble() const override;
|
||||
int minWidthForMedia() const override;
|
||||
bool hasFastReply() const override;
|
||||
bool displayFastReply() const override;
|
||||
bool displayRightActionComments() const;
|
||||
|
||||
@@ -531,7 +531,7 @@ void RepliesWidget::setupComposeControls() {
|
||||
//if (const auto item = messages.lastSentMessage(_history)) {
|
||||
// _inner->editMessageRequestNotify(item->fullId());
|
||||
//} else {
|
||||
// _scroll->keyPressEvent(e);
|
||||
_scroll->keyPressEvent(e);
|
||||
//}
|
||||
} else {
|
||||
_scroll->keyPressEvent(e);
|
||||
@@ -540,6 +540,12 @@ void RepliesWidget::setupComposeControls() {
|
||||
} else if (e->key() == Qt::Key_Down) {
|
||||
_scroll->keyPressEvent(e);
|
||||
e->accept();
|
||||
} else if (e->key() == Qt::Key_PageDown) {
|
||||
_scroll->keyPressEvent(e);
|
||||
e->accept();
|
||||
} else if (e->key() == Qt::Key_PageUp) {
|
||||
_scroll->keyPressEvent(e);
|
||||
e->accept();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
@@ -1444,11 +1450,6 @@ void RepliesWidget::saveState(not_null<RepliesMemento*> memento) {
|
||||
void RepliesWidget::restoreState(not_null<RepliesMemento*> memento) {
|
||||
const auto setReplies = [&](std::shared_ptr<Data::RepliesList> replies) {
|
||||
_replies = std::move(replies);
|
||||
_replies->fullCount(
|
||||
) | rpl::take(1) | rpl::start_with_next([=] {
|
||||
_loaded = true;
|
||||
updatePinnedVisibility();
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
rpl::single(0) | rpl::then(_replies->fullCount()),
|
||||
@@ -1518,7 +1519,7 @@ void RepliesWidget::updateControlsGeometry() {
|
||||
|
||||
const auto bottom = height();
|
||||
const auto controlsHeight = _composeControls->heightCurrent();
|
||||
const auto scrollY = _topBar->height() + _rootView->height();
|
||||
const auto scrollY = _topBar->height() + _rootViewHeight;
|
||||
const auto scrollHeight = bottom - scrollY - controlsHeight;
|
||||
const auto scrollSize = QSize(contentWidth, scrollHeight);
|
||||
if (_scroll->size() != scrollSize) {
|
||||
@@ -1584,13 +1585,28 @@ void RepliesWidget::updatePinnedVisibility() {
|
||||
return _root;
|
||||
}();
|
||||
const auto view = _inner->viewByPosition(item->position());
|
||||
setPinnedVisibility(!view
|
||||
|| (view->y() + view->height() <= _scroll->scrollTop()));
|
||||
const auto visible = !view
|
||||
|| (view->y() + view->height() <= _scroll->scrollTop());
|
||||
setPinnedVisibility(visible);
|
||||
}
|
||||
|
||||
void RepliesWidget::setPinnedVisibility(bool shown) {
|
||||
if (!animating()) {
|
||||
_rootView->toggle(shown, anim::type::normal);
|
||||
if (!_rootViewInited) {
|
||||
const auto height = shown ? st::historyReplyHeight : 0;
|
||||
if (const auto delta = height - _rootViewHeight) {
|
||||
_rootViewHeight = height;
|
||||
if (_scroll->scrollTop() == _scroll->scrollTopMax()) {
|
||||
setGeometryWithTopMoved(geometry(), delta);
|
||||
} else {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
}
|
||||
_rootView->toggle(shown, anim::type::instant);
|
||||
_rootViewInited = true;
|
||||
} else {
|
||||
_rootView->toggle(shown, anim::type::normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1657,7 +1673,18 @@ rpl::producer<Data::MessagesSlice> RepliesWidget::listSource(
|
||||
Data::MessagePosition aroundId,
|
||||
int limitBefore,
|
||||
int limitAfter) {
|
||||
return _replies->source(aroundId, limitBefore, limitAfter);
|
||||
return _replies->source(
|
||||
aroundId,
|
||||
limitBefore,
|
||||
limitAfter
|
||||
) | rpl::before_next([=] { // after_next makes a copy of value.
|
||||
if (!_loaded) {
|
||||
_loaded = true;
|
||||
crl::on_main(this, [=] {
|
||||
updatePinnedVisibility();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool RepliesWidget::listAllowsMultiSelect() {
|
||||
|
||||
@@ -255,6 +255,7 @@ private:
|
||||
object_ptr<Ui::SlideWrap<Ui::RpWidget>> _rootView;
|
||||
int _rootViewHeight = 0;
|
||||
object_ptr<Ui::PlainShadow> _rootShadow;
|
||||
bool _rootViewInited = false;
|
||||
|
||||
std::unique_ptr<Ui::ScrollArea> _scroll;
|
||||
|
||||
|
||||
@@ -137,7 +137,8 @@ QSize Contact::countOptimalSize() {
|
||||
auto minHeight = 0;
|
||||
if (_userId) {
|
||||
minHeight = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom();
|
||||
if (item->Has<HistoryMessageSigned>()
|
||||
const auto msgsigned = item->Get<HistoryMessageSigned>();
|
||||
if ((msgsigned && !msgsigned->isAnonymousRank)
|
||||
|| item->Has<HistoryMessageViews>()) {
|
||||
minHeight += st::msgDateFont->height - st::msgDateDelta.y();
|
||||
}
|
||||
|
||||
@@ -200,7 +200,8 @@ QSize Document::countOptimalSize() {
|
||||
} else {
|
||||
minHeight = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom();
|
||||
}
|
||||
if (!captioned && (item->Has<HistoryMessageSigned>()
|
||||
const auto msgsigned = item->Get<HistoryMessageSigned>();
|
||||
if (!captioned && ((msgsigned && !msgsigned->isAnonymousRank)
|
||||
|| item->Has<HistoryMessageViews>()
|
||||
|| _parent->displayEditedBadge())) {
|
||||
minHeight += st::msgDateFont->height - st::msgDateDelta.y();
|
||||
@@ -383,9 +384,7 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti
|
||||
_animation->radial.draw(p, rinner, st::msgFileRadialLine, fg);
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
drawCornerDownload(p, selected);
|
||||
}
|
||||
drawCornerDownload(p, selected);
|
||||
}
|
||||
auto namewidth = width() - nameleft - nameright;
|
||||
auto statuswidth = namewidth;
|
||||
@@ -529,7 +528,9 @@ bool Document::downloadInCorner() const {
|
||||
}
|
||||
|
||||
void Document::drawCornerDownload(Painter &p, bool selected) const {
|
||||
if (!downloadInCorner()) {
|
||||
if (dataLoaded()
|
||||
|| _data->loadedInMediaCache()
|
||||
|| !downloadInCorner()) {
|
||||
return;
|
||||
}
|
||||
auto outbg = _parent->hasOutLayout();
|
||||
@@ -569,7 +570,9 @@ TextState Document::cornerDownloadTextState(
|
||||
QPoint point,
|
||||
StateRequest request) const {
|
||||
auto result = TextState(_parent);
|
||||
if (!downloadInCorner()) {
|
||||
if (dataLoaded()
|
||||
|| _data->loadedInMediaCache()
|
||||
|| !downloadInCorner()) {
|
||||
return result;
|
||||
}
|
||||
auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
|
||||
@@ -626,10 +629,8 @@ TextState Document::textState(QPoint point, StateRequest request) const {
|
||||
nametop = st::msgFileNameTop - topMinus;
|
||||
bottom = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom() - topMinus;
|
||||
|
||||
if (!loaded) {
|
||||
if (const auto state = cornerDownloadTextState(point, request); state.link) {
|
||||
return state;
|
||||
}
|
||||
if (const auto state = cornerDownloadTextState(point, request); state.link) {
|
||||
return state;
|
||||
}
|
||||
QRect inner(style::rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top() - topMinus, st::msgFileSize, st::msgFileSize, width()));
|
||||
if ((_data->loading() || _data->uploading()) && inner.contains(point) && !downloadInCorner()) {
|
||||
|
||||
@@ -155,7 +155,7 @@ QSize Gif::countOptimalSize() {
|
||||
_thumbh = th;
|
||||
auto maxWidth = qMax(tw, st::minPhotoSize);
|
||||
auto minHeight = qMax(th, st::minPhotoSize);
|
||||
accumulate_max(maxWidth, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));
|
||||
accumulate_max(maxWidth, _parent->minWidthForMedia());
|
||||
if (!activeCurrentStreamed()) {
|
||||
accumulate_max(maxWidth, gifMaxStatusWidth(_data) + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));
|
||||
}
|
||||
@@ -212,7 +212,7 @@ QSize Gif::countCurrentSize(int newWidth) {
|
||||
|
||||
newWidth = qMax(tw, st::minPhotoSize);
|
||||
auto newHeight = qMax(th, st::minPhotoSize);
|
||||
accumulate_max(newWidth, _parent->infoWidth() + 2 * st::msgDateImgDelta + st::msgDateImgPadding.x());
|
||||
accumulate_max(newWidth, _parent->minWidthForMedia());
|
||||
if (!activeCurrentStreamed()) {
|
||||
accumulate_max(newWidth, gifMaxStatusWidth(_data) + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ QSize Location::countOptimalSize() {
|
||||
th = (st::maxMediaSize * th) / tw;
|
||||
tw = st::maxMediaSize;
|
||||
}
|
||||
auto minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));
|
||||
auto minWidth = qMax(st::minPhotoSize, _parent->minWidthForMedia());
|
||||
auto maxWidth = qMax(tw, minWidth);
|
||||
auto minHeight = qMax(th, st::minPhotoSize);
|
||||
|
||||
@@ -118,7 +118,7 @@ QSize Location::countCurrentSize(int newWidth) {
|
||||
} else {
|
||||
newWidth = tw;
|
||||
}
|
||||
auto minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));
|
||||
auto minWidth = qMax(st::minPhotoSize, _parent->minWidthForMedia());
|
||||
accumulate_max(newWidth, minWidth);
|
||||
accumulate_max(newHeight, st::minPhotoSize);
|
||||
if (_parent->hasBubble()) {
|
||||
|
||||
@@ -154,7 +154,9 @@ QSize Photo::countOptimalSize() {
|
||||
if (_serviceWidth > 0) {
|
||||
return { _serviceWidth, _serviceWidth };
|
||||
}
|
||||
const auto minWidth = qMax((_parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));
|
||||
const auto minWidth = qMax(
|
||||
(_parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize),
|
||||
_parent->minWidthForMedia());
|
||||
const auto maxActualWidth = qMax(tw, minWidth);
|
||||
maxWidth = qMax(maxActualWidth, th);
|
||||
minHeight = qMax(th, st::minPhotoSize);
|
||||
@@ -194,7 +196,9 @@ QSize Photo::countCurrentSize(int newWidth) {
|
||||
if (_pixw < 1) _pixw = 1;
|
||||
if (_pixh < 1) _pixh = 1;
|
||||
|
||||
auto minWidth = qMax((_parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()));
|
||||
auto minWidth = qMax(
|
||||
(_parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize),
|
||||
_parent->minWidthForMedia());
|
||||
newWidth = qMax(_pixw, minWidth);
|
||||
auto newHeight = qMax(_pixh, st::minPhotoSize);
|
||||
if (_parent->hasBubble() && !_caption.isEmpty()) {
|
||||
|
||||
@@ -649,36 +649,41 @@ void ListWidget::restart() {
|
||||
}
|
||||
|
||||
void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
if (isMyItem(item)) {
|
||||
auto id = GetUniversalId(item);
|
||||
|
||||
auto sectionIt = findSectionByItem(id);
|
||||
if (sectionIt != _sections.end()) {
|
||||
if (sectionIt->removeItem(id)) {
|
||||
auto top = sectionIt->top();
|
||||
if (sectionIt->empty()) {
|
||||
_sections.erase(sectionIt);
|
||||
}
|
||||
refreshHeight();
|
||||
}
|
||||
}
|
||||
|
||||
if (isItemLayout(item, _overLayout)) {
|
||||
_overLayout = nullptr;
|
||||
}
|
||||
|
||||
if (const auto i = _layouts.find(id); i != _layouts.end()) {
|
||||
_heavyLayouts.remove(i->second.item.get());
|
||||
_layouts.erase(i);
|
||||
}
|
||||
_dragSelected.remove(id);
|
||||
|
||||
if (const auto i = _selected.find(id); i != _selected.cend()) {
|
||||
removeItemSelection(i);
|
||||
}
|
||||
|
||||
mouseActionUpdate(_mousePosition);
|
||||
if (!isMyItem(item)) {
|
||||
return;
|
||||
}
|
||||
auto id = GetUniversalId(item);
|
||||
|
||||
auto needHeightRefresh = false;
|
||||
auto sectionIt = findSectionByItem(id);
|
||||
if (sectionIt != _sections.end()) {
|
||||
if (sectionIt->removeItem(id)) {
|
||||
auto top = sectionIt->top();
|
||||
if (sectionIt->empty()) {
|
||||
_sections.erase(sectionIt);
|
||||
}
|
||||
needHeightRefresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isItemLayout(item, _overLayout)) {
|
||||
_overLayout = nullptr;
|
||||
}
|
||||
|
||||
if (const auto i = _layouts.find(id); i != _layouts.end()) {
|
||||
_heavyLayouts.remove(i->second.item.get());
|
||||
_layouts.erase(i);
|
||||
}
|
||||
_dragSelected.remove(id);
|
||||
|
||||
if (const auto i = _selected.find(id); i != _selected.cend()) {
|
||||
removeItemSelection(i);
|
||||
}
|
||||
|
||||
if (needHeightRefresh) {
|
||||
refreshHeight();
|
||||
}
|
||||
mouseActionUpdate(_mousePosition);
|
||||
}
|
||||
|
||||
FullMsgId ListWidget::computeFullId(
|
||||
|
||||
@@ -2153,7 +2153,11 @@ void MainWidget::updateControlsGeometry() {
|
||||
const auto active = _controller->activeChatCurrent();
|
||||
if (const auto peer = active.peer()) {
|
||||
if (Core::App().settings().tabbedSelectorSectionEnabled()) {
|
||||
_history->pushTabbedSelectorToThirdSection(peer, params);
|
||||
if (_mainSection) {
|
||||
_mainSection->pushTabbedSelectorToThirdSection(peer, params);
|
||||
} else {
|
||||
_history->pushTabbedSelectorToThirdSection(peer, params);
|
||||
}
|
||||
} else if (Core::App().settings().thirdSectionInfoEnabled()) {
|
||||
_controller->showSection(
|
||||
Info::Memento::Default(peer),
|
||||
@@ -2412,7 +2416,9 @@ void MainWidget::updateThirdColumnToCurrentChat(
|
||||
};
|
||||
auto switchTabbedFast = [&](not_null<PeerData*> peer) {
|
||||
saveOldThirdSection();
|
||||
return _history->pushTabbedSelectorToThirdSection(peer, params);
|
||||
return _mainSection
|
||||
? _mainSection->pushTabbedSelectorToThirdSection(peer, params)
|
||||
: _history->pushTabbedSelectorToThirdSection(peer, params);
|
||||
};
|
||||
if (Adaptive::ThreeColumn()
|
||||
&& settings.tabbedSelectorSectionEnabled()
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "media/audio/media_audio_capture.h"
|
||||
|
||||
#include "media/audio/media_audio_ffmpeg_loader.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
#include <al.h>
|
||||
#include <alc.h>
|
||||
@@ -37,6 +38,36 @@ bool ErrorHappened(ALCdevice *device) {
|
||||
|
||||
} // namespace
|
||||
|
||||
class Instance::Inner final : public QObject {
|
||||
public:
|
||||
Inner(QThread *thread);
|
||||
~Inner();
|
||||
|
||||
void start(Fn<void(Update)> updated, Fn<void()> error);
|
||||
void stop(Fn<void(Result&&)> callback = nullptr);
|
||||
|
||||
void timeout();
|
||||
|
||||
private:
|
||||
void processFrame(int32 offset, int32 framesize);
|
||||
void fail();
|
||||
|
||||
void writeFrame(AVFrame *frame);
|
||||
|
||||
// Writes the packets till EAGAIN is got from av_receive_packet()
|
||||
// Returns number of packets written or -1 on error
|
||||
int writePackets();
|
||||
|
||||
Fn<void(Update)> _updated;
|
||||
Fn<void()> _error;
|
||||
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
base::Timer _timer;
|
||||
QByteArray _captured;
|
||||
|
||||
};
|
||||
|
||||
void Start() {
|
||||
Assert(CaptureInstance == nullptr);
|
||||
CaptureInstance = new Instance();
|
||||
@@ -47,18 +78,40 @@ void Finish() {
|
||||
delete base::take(CaptureInstance);
|
||||
}
|
||||
|
||||
Instance::Instance() : _inner(new Inner(&_thread)) {
|
||||
Instance::Instance() : _inner(std::make_unique<Inner>(&_thread)) {
|
||||
CaptureInstance = this;
|
||||
connect(this, SIGNAL(start()), _inner, SLOT(onStart()));
|
||||
connect(this, SIGNAL(stop(bool)), _inner, SLOT(onStop(bool)));
|
||||
connect(_inner, SIGNAL(done(QByteArray, VoiceWaveform, qint32)), this, SIGNAL(done(QByteArray, VoiceWaveform, qint32)));
|
||||
connect(_inner, SIGNAL(updated(quint16, qint32)), this, SIGNAL(updated(quint16, qint32)));
|
||||
connect(_inner, SIGNAL(error()), this, SIGNAL(error()));
|
||||
connect(&_thread, SIGNAL(started()), _inner, SLOT(onInit()));
|
||||
connect(&_thread, SIGNAL(finished()), _inner, SLOT(deleteLater()));
|
||||
_thread.start();
|
||||
}
|
||||
|
||||
void Instance::start() {
|
||||
_updates.fire_done();
|
||||
InvokeQueued(_inner.get(), [=] {
|
||||
_inner->start([=](Update update) {
|
||||
crl::on_main(this, [=] {
|
||||
_updates.fire_copy(update);
|
||||
});
|
||||
}, [=] {
|
||||
crl::on_main(this, [=] {
|
||||
_updates.fire_error({});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::stop(Fn<void(Result&&)> callback) {
|
||||
InvokeQueued(_inner.get(), [=] {
|
||||
if (!callback) {
|
||||
_inner->stop();
|
||||
return;
|
||||
}
|
||||
_inner->stop([=](Result &&result) {
|
||||
crl::on_main([=, result = std::move(result)]() mutable {
|
||||
callback(std::move(result));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::check() {
|
||||
_available = false;
|
||||
if (auto device = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) {
|
||||
@@ -71,7 +124,8 @@ void Instance::check() {
|
||||
}
|
||||
|
||||
Instance::~Instance() {
|
||||
_inner = nullptr;
|
||||
InvokeQueued(_inner.get(), [copy = base::take(_inner)] {
|
||||
});
|
||||
_thread.quit();
|
||||
_thread.wait();
|
||||
}
|
||||
@@ -155,34 +209,39 @@ struct Instance::Inner::Private {
|
||||
}
|
||||
};
|
||||
|
||||
Instance::Inner::Inner(QThread *thread) : d(new Private()) {
|
||||
Instance::Inner::Inner(QThread *thread)
|
||||
: d(std::make_unique<Private>())
|
||||
, _timer(thread, [=] { timeout(); }) {
|
||||
moveToThread(thread);
|
||||
_timer.moveToThread(thread);
|
||||
connect(&_timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
|
||||
}
|
||||
|
||||
Instance::Inner::~Inner() {
|
||||
onStop(false);
|
||||
delete d;
|
||||
stop();
|
||||
}
|
||||
|
||||
void Instance::Inner::onInit() {
|
||||
void Instance::Inner::fail() {
|
||||
Expects(_error != nullptr);
|
||||
|
||||
stop();
|
||||
_error();
|
||||
}
|
||||
|
||||
void Instance::Inner::onStart() {
|
||||
void Instance::Inner::start(Fn<void(Update)> updated, Fn<void()> error) {
|
||||
_updated = std::move(updated);
|
||||
_error = std::move(error);
|
||||
|
||||
// Start OpenAL Capture
|
||||
d->device = alcCaptureOpenDevice(nullptr, kCaptureFrequency, AL_FORMAT_MONO16, kCaptureFrequency / 5);
|
||||
if (!d->device) {
|
||||
LOG(("Audio Error: capture device not present!"));
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
alcCaptureStart(d->device);
|
||||
if (ErrorHappened(d->device)) {
|
||||
alcCaptureCloseDevice(d->device);
|
||||
d->device = nullptr;
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,7 +249,7 @@ void Instance::Inner::onStart() {
|
||||
|
||||
d->ioBuffer = (uchar*)av_malloc(AVBlockSize);
|
||||
|
||||
d->ioContext = avio_alloc_context(d->ioBuffer, AVBlockSize, 1, static_cast<void*>(d), &Private::_read_data, &Private::_write_data, &Private::_seek_data);
|
||||
d->ioContext = avio_alloc_context(d->ioBuffer, AVBlockSize, 1, static_cast<void*>(d.get()), &Private::_read_data, &Private::_write_data, &Private::_seek_data);
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
AVOutputFormat *fmt = 0;
|
||||
@@ -201,15 +260,13 @@ void Instance::Inner::onStart() {
|
||||
}
|
||||
if (!fmt) {
|
||||
LOG(("Audio Error: Unable to find opus AVOutputFormat for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((res = avformat_alloc_output_context2(&d->fmtContext, fmt, 0, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to avformat_alloc_output_context2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
d->fmtContext->pb = d->ioContext;
|
||||
@@ -220,23 +277,20 @@ void Instance::Inner::onStart() {
|
||||
d->codec = avcodec_find_encoder(fmt->audio_codec);
|
||||
if (!d->codec) {
|
||||
LOG(("Audio Error: Unable to avcodec_find_encoder for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
d->stream = avformat_new_stream(d->fmtContext, d->codec);
|
||||
if (!d->stream) {
|
||||
LOG(("Audio Error: Unable to avformat_new_stream for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
d->stream->id = d->fmtContext->nb_streams - 1;
|
||||
d->codecContext = avcodec_alloc_context3(d->codec);
|
||||
if (!d->codecContext) {
|
||||
LOG(("Audio Error: Unable to avcodec_alloc_context3 for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -255,8 +309,7 @@ void Instance::Inner::onStart() {
|
||||
// Open audio stream
|
||||
if ((res = avcodec_open2(d->codecContext, d->codec, nullptr)) < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_open2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,48 +340,46 @@ void Instance::Inner::onStart() {
|
||||
|
||||
if ((res = swr_init(d->swrContext)) < 0) {
|
||||
LOG(("Audio Error: Unable to swr_init for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
d->maxDstSamples = d->srcSamples;
|
||||
if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
d->dstSamplesSize = av_samples_get_buffer_size(0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);
|
||||
|
||||
if ((res = avcodec_parameters_from_context(d->stream->codecpar, d->codecContext)) < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_parameters_from_context for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
// Write file header
|
||||
if ((res = avformat_write_header(d->fmtContext, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to avformat_write_header for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
_timer.start(50);
|
||||
_timer.callEach(50);
|
||||
_captured.clear();
|
||||
_captured.reserve(kCaptureBufferSlice);
|
||||
DEBUG_LOG(("Audio Capture: started!"));
|
||||
}
|
||||
|
||||
void Instance::Inner::onStop(bool needResult) {
|
||||
if (!_timer.isActive()) return; // in onStop() already
|
||||
_timer.stop();
|
||||
void Instance::Inner::stop(Fn<void(Result&&)> callback) {
|
||||
if (!_timer.isActive()) {
|
||||
return; // in stop() already
|
||||
}
|
||||
_timer.cancel();
|
||||
|
||||
if (d->device) {
|
||||
alcCaptureStop(d->device);
|
||||
onTimeout(); // get last data
|
||||
timeout(); // get last data
|
||||
}
|
||||
|
||||
// Write what is left
|
||||
@@ -370,7 +421,11 @@ void Instance::Inner::onStop(bool needResult) {
|
||||
}
|
||||
}
|
||||
}
|
||||
DEBUG_LOG(("Audio Capture: stopping (need result: %1), size: %2, samples: %3").arg(Logs::b(needResult)).arg(d->data.size()).arg(d->fullSamples));
|
||||
DEBUG_LOG(("Audio Capture: "
|
||||
"stopping (need result: %1), size: %2, samples: %3"
|
||||
).arg(Logs::b(callback != nullptr)
|
||||
).arg(d->data.size()
|
||||
).arg(d->fullSamples));
|
||||
_captured = QByteArray();
|
||||
|
||||
// Finish stream
|
||||
@@ -465,19 +520,21 @@ void Instance::Inner::onStop(bool needResult) {
|
||||
d->waveformPeak = 0;
|
||||
d->waveform.clear();
|
||||
}
|
||||
if (needResult) emit done(result, waveform, samples);
|
||||
|
||||
if (callback) {
|
||||
callback({ result, waveform, samples });
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Inner::onTimeout() {
|
||||
void Instance::Inner::timeout() {
|
||||
if (!d->device) {
|
||||
_timer.stop();
|
||||
_timer.cancel();
|
||||
return;
|
||||
}
|
||||
ALint samples;
|
||||
alcGetIntegerv(d->device, ALC_CAPTURE_SAMPLES, sizeof(samples), &samples);
|
||||
alcGetIntegerv(d->device, ALC_CAPTURE_SAMPLES, 1, &samples);
|
||||
if (ErrorHappened(d->device)) {
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
if (samples > 0) {
|
||||
@@ -490,8 +547,7 @@ void Instance::Inner::onTimeout() {
|
||||
_captured.resize(news);
|
||||
alcCaptureSamples(d->device, (ALCvoid *)(_captured.data() + s), samples);
|
||||
if (ErrorHappened(d->device)) {
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -512,7 +568,7 @@ void Instance::Inner::onTimeout() {
|
||||
}
|
||||
qint32 samplesFull = d->fullSamples + _captured.size() / sizeof(short), samplesSinceUpdate = samplesFull - d->lastUpdate;
|
||||
if (samplesSinceUpdate > kCaptureUpdateDelta * kCaptureFrequency / 1000) {
|
||||
emit updated(d->levelMax, samplesFull);
|
||||
_updated(Update{ .samples = samplesFull, .level = d->levelMax });
|
||||
d->lastUpdate = samplesFull;
|
||||
d->levelMax = 0;
|
||||
}
|
||||
@@ -539,8 +595,7 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) {
|
||||
|
||||
if (framesize % sizeof(short)) { // in the middle of a sample
|
||||
LOG(("Audio Error: Bad framesize in writeFrame() for capture, framesize %1, %2").arg(framesize));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
auto samplesCnt = static_cast<int>(framesize / sizeof(short));
|
||||
@@ -587,8 +642,7 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) {
|
||||
av_freep(&d->dstSamplesData[0]);
|
||||
if ((res = av_samples_alloc(d->dstSamplesData, 0, d->codecContext->channels, d->dstSamples, d->codecContext->sample_fmt, 1)) < 0) {
|
||||
LOG(("Audio Error: Unable to av_samples_alloc for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
d->dstSamplesSize = av_samples_get_buffer_size(0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);
|
||||
@@ -596,8 +650,7 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) {
|
||||
|
||||
if ((res = swr_convert(d->swrContext, d->dstSamplesData, d->dstSamples, (const uint8_t **)srcSamplesData, d->srcSamples)) < 0) {
|
||||
LOG(("Audio Error: Unable to swr_convert for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -627,30 +680,26 @@ void Instance::Inner::writeFrame(AVFrame *frame) {
|
||||
if (packetsWritten < 0) {
|
||||
if (frame && packetsWritten == AVERROR_EOF) {
|
||||
LOG(("Audio Error: EOF in packets received when EAGAIN was got in avcodec_send_frame()"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
}
|
||||
return;
|
||||
} else if (!packetsWritten) {
|
||||
LOG(("Audio Error: No packets received when EAGAIN was got in avcodec_send_frame()"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
res = avcodec_send_frame(d->codecContext, frame);
|
||||
}
|
||||
if (res < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_send_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame) { // drain
|
||||
if ((res = writePackets()) != AVERROR_EOF) {
|
||||
LOG(("Audio Error: not EOF in packets received when draining the codec, result %1").arg(res));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -672,8 +721,7 @@ int Instance::Inner::writePackets() {
|
||||
return res;
|
||||
}
|
||||
LOG(("Audio Error: Unable to avcodec_receive_packet for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -681,8 +729,7 @@ int Instance::Inner::writePackets() {
|
||||
pkt.stream_index = d->stream->index;
|
||||
if ((res = av_interleaved_write_frame(d->fmtContext, &pkt)) < 0) {
|
||||
LOG(("Audio Error: Unable to av_interleaved_write_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
fail();
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,76 +14,49 @@ struct AVFrame;
|
||||
namespace Media {
|
||||
namespace Capture {
|
||||
|
||||
struct Update {
|
||||
int samples = 0;
|
||||
ushort level = 0;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
QByteArray bytes;
|
||||
VoiceWaveform waveform;
|
||||
int samples = 0;
|
||||
};
|
||||
|
||||
void Start();
|
||||
void Finish();
|
||||
|
||||
class Instance : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
class Instance final : public QObject {
|
||||
public:
|
||||
Instance();
|
||||
~Instance();
|
||||
|
||||
void check();
|
||||
bool available() const {
|
||||
[[nodiscard]] bool available() const {
|
||||
return _available;
|
||||
}
|
||||
|
||||
~Instance();
|
||||
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const {
|
||||
return _updates.events();
|
||||
}
|
||||
|
||||
signals:
|
||||
void start();
|
||||
void stop(bool needResult);
|
||||
|
||||
void done(QByteArray data, VoiceWaveform waveform, qint32 samples);
|
||||
void updated(quint16 level, qint32 samples);
|
||||
void error();
|
||||
void stop(Fn<void(Result&&)> callback = nullptr);
|
||||
|
||||
private:
|
||||
class Inner;
|
||||
friend class Inner;
|
||||
|
||||
bool _available = false;
|
||||
rpl::event_stream<Update, rpl::empty_error> _updates;
|
||||
QThread _thread;
|
||||
Inner *_inner;
|
||||
std::unique_ptr<Inner> _inner;
|
||||
|
||||
};
|
||||
|
||||
Instance *instance();
|
||||
|
||||
class Instance::Inner : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Inner(QThread *thread);
|
||||
~Inner();
|
||||
|
||||
signals:
|
||||
void error();
|
||||
void updated(quint16 level, qint32 samples);
|
||||
void done(QByteArray data, VoiceWaveform waveform, qint32 samples);
|
||||
|
||||
public slots:
|
||||
void onInit();
|
||||
void onStart();
|
||||
void onStop(bool needResult);
|
||||
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
void processFrame(int32 offset, int32 framesize);
|
||||
|
||||
void writeFrame(AVFrame *frame);
|
||||
|
||||
// Writes the packets till EAGAIN is got from av_receive_packet()
|
||||
// Returns number of packets written or -1 on error
|
||||
int writePackets();
|
||||
|
||||
struct Private;
|
||||
Private *d;
|
||||
QTimer _timer;
|
||||
QByteArray _captured;
|
||||
|
||||
};
|
||||
[[nodiscard]] Instance *instance();
|
||||
|
||||
} // namespace Capture
|
||||
} // namespace Media
|
||||
|
||||
@@ -1489,32 +1489,33 @@ void OverlayWidget::onForward() {
|
||||
}
|
||||
|
||||
void OverlayWidget::onDelete() {
|
||||
const auto session = _session;
|
||||
if (!session) {
|
||||
if (!_session) {
|
||||
return;
|
||||
}
|
||||
|
||||
close();
|
||||
const auto deletingPeerPhoto = [this] {
|
||||
const auto session = _session;
|
||||
const auto photo = _photo;
|
||||
const auto msgid = _msgid;
|
||||
const auto deletingPeerPhoto = [&] {
|
||||
if (!_msgid) {
|
||||
return true;
|
||||
}
|
||||
if (_photo && _history) {
|
||||
} else if (_photo && _history) {
|
||||
if (_history->peer->userpicPhotoId() == _photo->id) {
|
||||
return _firstOpenedPeerPhoto;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}();
|
||||
close();
|
||||
|
||||
Core::App().domain().activate(&_session->account());
|
||||
const auto &active = _session->windows();
|
||||
Core::App().domain().activate(&session->account());
|
||||
const auto &active = session->windows();
|
||||
if (active.empty()) {
|
||||
return;
|
||||
}
|
||||
if (deletingPeerPhoto()) {
|
||||
active.front()->content()->deletePhotoLayer(_photo);
|
||||
} else if (const auto item = session->data().message(_msgid)) {
|
||||
if (deletingPeerPhoto) {
|
||||
active.front()->content()->deletePhotoLayer(photo);
|
||||
} else if (const auto item = session->data().message(msgid)) {
|
||||
const auto suggestModerateActions = true;
|
||||
Ui::show(Box<DeleteMessagesBox>(item, suggestModerateActions));
|
||||
}
|
||||
@@ -1544,15 +1545,15 @@ void OverlayWidget::onCopy() {
|
||||
|
||||
void OverlayWidget::onAttachedStickers() {
|
||||
const auto session = _session;
|
||||
if (!session) {
|
||||
if (!session || !_photo) {
|
||||
return;
|
||||
}
|
||||
const auto &active = _session->windows();
|
||||
if (active.empty()) {
|
||||
return;
|
||||
}
|
||||
close();
|
||||
active.front()->requestAttachedStickerSets(_photo);
|
||||
close();
|
||||
}
|
||||
|
||||
auto OverlayWidget::sharedMediaType() const
|
||||
|
||||
@@ -39,6 +39,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtDBus/QDBusReply>
|
||||
#include <QtDBus/QDBusError>
|
||||
#include <QtDBus/QDBusMetaType>
|
||||
|
||||
extern "C" {
|
||||
#undef signals
|
||||
#include <gio/gio.h>
|
||||
#define signals public
|
||||
} // extern "C"
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
|
||||
#include <glib.h>
|
||||
@@ -51,10 +57,13 @@ constexpr auto kForcePanelIcon = "TDESKTOP_FORCE_PANEL_ICON"_cs;
|
||||
constexpr auto kPanelTrayIconName = "telegram-panel"_cs;
|
||||
constexpr auto kMutePanelTrayIconName = "telegram-mute-panel"_cs;
|
||||
constexpr auto kAttentionPanelTrayIconName = "telegram-attention-panel"_cs;
|
||||
constexpr auto kSNIWatcherService = "org.kde.StatusNotifierWatcher"_cs;
|
||||
constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs;
|
||||
constexpr auto kTrayIconFilename = "tdesktop-trayicon-XXXXXX.png"_cs;
|
||||
|
||||
constexpr auto kSNIWatcherService = "org.kde.StatusNotifierWatcher"_cs;
|
||||
constexpr auto kSNIWatcherObjectPath = "/StatusNotifierWatcher"_cs;
|
||||
constexpr auto kSNIWatcherInterface = kSNIWatcherService;
|
||||
|
||||
constexpr auto kAppMenuService = "com.canonical.AppMenu.Registrar"_cs;
|
||||
constexpr auto kAppMenuObjectPath = "/com/canonical/AppMenu/Registrar"_cs;
|
||||
constexpr auto kAppMenuInterface = kAppMenuService;
|
||||
@@ -65,9 +74,6 @@ base::flat_map<int, QImage> TrayIconImageBack;
|
||||
QIcon TrayIcon;
|
||||
QString TrayIconThemeName, TrayIconName;
|
||||
|
||||
bool SNIAvailable = false;
|
||||
bool AppMenuSupported = false;
|
||||
|
||||
QString GetPanelIconName(int counter, bool muted) {
|
||||
return (counter > 0)
|
||||
? (muted
|
||||
@@ -222,7 +228,13 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
iconImage.height() - layer.height() - 1,
|
||||
layer);
|
||||
} else {
|
||||
App::wnd()->placeSmallCounter(iconImage, 16, counter, bg, QPoint(), fg);
|
||||
App::wnd()->placeSmallCounter(
|
||||
iconImage,
|
||||
16,
|
||||
counter,
|
||||
bg,
|
||||
QPoint(),
|
||||
fg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +324,7 @@ bool IsSNIAvailable() {
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
auto message = QDBusMessage::createMethodCall(
|
||||
kSNIWatcherService.utf16(),
|
||||
qsl("/StatusNotifierWatcher"),
|
||||
kSNIWatcherObjectPath.utf16(),
|
||||
kPropertiesInterface.utf16(),
|
||||
qsl("Get"));
|
||||
|
||||
@@ -416,15 +428,26 @@ MainWindow::MainWindow(not_null<Window::Controller*> controller)
|
||||
}
|
||||
|
||||
void MainWindow::initHook() {
|
||||
SNIAvailable = IsSNIAvailable();
|
||||
|
||||
const auto trayAvailable = SNIAvailable
|
||||
|| QSystemTrayIcon::isSystemTrayAvailable();
|
||||
|
||||
LOG(("System tray available: %1").arg(Logs::b(trayAvailable)));
|
||||
Platform::SetTrayIconSupported(trayAvailable);
|
||||
_sniAvailable = IsSNIAvailable();
|
||||
LOG(("System tray available: %1").arg(Logs::b(trayAvailable())));
|
||||
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
_sniDBusProxy = g_dbus_proxy_new_for_bus_sync(
|
||||
G_BUS_TYPE_SESSION,
|
||||
G_DBUS_PROXY_FLAGS_NONE,
|
||||
nullptr,
|
||||
kSNIWatcherService.utf8(),
|
||||
kSNIWatcherObjectPath.utf8(),
|
||||
kSNIWatcherInterface.utf8(),
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
g_signal_connect(
|
||||
_sniDBusProxy,
|
||||
"g-signal",
|
||||
G_CALLBACK(sniSignalEmitted),
|
||||
this);
|
||||
|
||||
auto sniWatcher = new QDBusServiceWatcher(
|
||||
kSNIWatcherService.utf16(),
|
||||
QDBusConnection::sessionBus(),
|
||||
@@ -442,7 +465,7 @@ void MainWindow::initHook() {
|
||||
handleSNIOwnerChanged(service, oldOwner, newOwner);
|
||||
});
|
||||
|
||||
AppMenuSupported = IsAppMenuSupported();
|
||||
_appMenuSupported = IsAppMenuSupported();
|
||||
|
||||
auto appMenuWatcher = new QDBusServiceWatcher(
|
||||
kAppMenuService.utf16(),
|
||||
@@ -461,7 +484,7 @@ void MainWindow::initHook() {
|
||||
handleAppMenuOwnerChanged(service, oldOwner, newOwner);
|
||||
});
|
||||
|
||||
if (AppMenuSupported) {
|
||||
if (_appMenuSupported) {
|
||||
LOG(("Using D-Bus global menu."));
|
||||
} else {
|
||||
LOG(("Not using D-Bus global menu."));
|
||||
@@ -484,7 +507,7 @@ void MainWindow::initHook() {
|
||||
|
||||
bool MainWindow::hasTrayIcon() const {
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
return trayIcon || (SNIAvailable && _sniTrayIcon);
|
||||
return trayIcon || (_sniAvailable && _sniTrayIcon);
|
||||
#else
|
||||
return trayIcon;
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
@@ -564,15 +587,50 @@ void MainWindow::attachToSNITrayIcon() {
|
||||
updateTrayMenu();
|
||||
}
|
||||
|
||||
void MainWindow::handleSNIOwnerChanged(
|
||||
const QString &service,
|
||||
const QString &oldOwner,
|
||||
const QString &newOwner) {
|
||||
void MainWindow::sniSignalEmitted(
|
||||
GDBusProxy *proxy,
|
||||
gchar *sender_name,
|
||||
gchar *signal_name,
|
||||
GVariant *parameters,
|
||||
MainWindow *window) {
|
||||
if(signal_name == qstr("StatusNotifierHostRegistered")) {
|
||||
window->handleSNIHostRegistered();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::handleSNIHostRegistered() {
|
||||
if (_sniAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
_sniAvailable = true;
|
||||
|
||||
if (Global::WorkMode().value() == dbiwmWindowOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldOwner.isEmpty() && !newOwner.isEmpty()) {
|
||||
LOG(("Switching to SNI tray icon..."));
|
||||
|
||||
if (trayIcon) {
|
||||
trayIcon->setContextMenu(0);
|
||||
trayIcon->deleteLater();
|
||||
}
|
||||
trayIcon = nullptr;
|
||||
|
||||
psSetupTrayIcon();
|
||||
}
|
||||
|
||||
void MainWindow::handleSNIOwnerChanged(
|
||||
const QString &service,
|
||||
const QString &oldOwner,
|
||||
const QString &newOwner) {
|
||||
_sniAvailable = IsSNIAvailable();
|
||||
|
||||
if (Global::WorkMode().value() == dbiwmWindowOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldOwner.isEmpty() && !newOwner.isEmpty() && _sniAvailable) {
|
||||
LOG(("Switching to SNI tray icon..."));
|
||||
} else if (!oldOwner.isEmpty() && newOwner.isEmpty()) {
|
||||
LOG(("Switching to Qt tray icon..."));
|
||||
@@ -586,14 +644,7 @@ void MainWindow::handleSNIOwnerChanged(
|
||||
}
|
||||
trayIcon = nullptr;
|
||||
|
||||
SNIAvailable = !newOwner.isEmpty();
|
||||
|
||||
const auto trayAvailable = SNIAvailable
|
||||
|| QSystemTrayIcon::isSystemTrayAvailable();
|
||||
|
||||
Platform::SetTrayIconSupported(trayAvailable);
|
||||
|
||||
if (trayAvailable) {
|
||||
if (trayAvailable()) {
|
||||
psSetupTrayIcon();
|
||||
} else {
|
||||
LOG(("System tray is not available."));
|
||||
@@ -605,14 +656,14 @@ void MainWindow::handleAppMenuOwnerChanged(
|
||||
const QString &oldOwner,
|
||||
const QString &newOwner) {
|
||||
if (oldOwner.isEmpty() && !newOwner.isEmpty()) {
|
||||
AppMenuSupported = true;
|
||||
_appMenuSupported = true;
|
||||
LOG(("Using D-Bus global menu."));
|
||||
} else if (!oldOwner.isEmpty() && newOwner.isEmpty()) {
|
||||
AppMenuSupported = false;
|
||||
_appMenuSupported = false;
|
||||
LOG(("Not using D-Bus global menu."));
|
||||
}
|
||||
|
||||
if (AppMenuSupported && !_mainMenuPath.path().isEmpty()) {
|
||||
if (_appMenuSupported && !_mainMenuPath.path().isEmpty()) {
|
||||
RegisterAppMenu(winId(), _mainMenuPath);
|
||||
} else {
|
||||
UnregisterAppMenu(winId());
|
||||
@@ -624,7 +675,7 @@ void MainWindow::psSetupTrayIcon() {
|
||||
const auto counter = Core::App().unreadBadge();
|
||||
const auto muted = Core::App().unreadBadgeMuted();
|
||||
|
||||
if (SNIAvailable) {
|
||||
if (_sniAvailable) {
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
LOG(("Using SNI tray icon."));
|
||||
if (!_sniTrayIcon) {
|
||||
@@ -654,7 +705,7 @@ void MainWindow::psSetupTrayIcon() {
|
||||
}
|
||||
|
||||
void MainWindow::workmodeUpdated(DBIWorkMode mode) {
|
||||
if (!Platform::TrayIconSupported()) {
|
||||
if (!trayAvailable()) {
|
||||
return;
|
||||
} else if (mode == dbiwmWindowOnly) {
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
@@ -953,7 +1004,7 @@ void MainWindow::createGlobalMenu() {
|
||||
_mainMenuPath.path(),
|
||||
psMainMenu);
|
||||
|
||||
if (AppMenuSupported) {
|
||||
if (_appMenuSupported) {
|
||||
RegisterAppMenu(winId(), _mainMenuPath);
|
||||
}
|
||||
|
||||
@@ -1074,7 +1125,7 @@ void MainWindow::updateGlobalMenuHook() {
|
||||
}
|
||||
|
||||
void MainWindow::handleVisibleChangedHook(bool visible) {
|
||||
if (AppMenuSupported && !_mainMenuPath.path().isEmpty()) {
|
||||
if (_appMenuSupported && !_mainMenuPath.path().isEmpty()) {
|
||||
if (visible) {
|
||||
RegisterAppMenu(winId(), _mainMenuPath);
|
||||
} else {
|
||||
@@ -1089,12 +1140,14 @@ MainWindow::~MainWindow() {
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
delete _sniTrayIcon;
|
||||
|
||||
if (AppMenuSupported) {
|
||||
if (_appMenuSupported) {
|
||||
UnregisterAppMenu(winId());
|
||||
}
|
||||
|
||||
delete _mainMenuExporter;
|
||||
delete psMainMenu;
|
||||
|
||||
g_object_unref(_sniDBusProxy);
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
|
||||
delete _trayIconMenuXEmbed;
|
||||
|
||||
@@ -16,7 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QTemporaryFile>
|
||||
#include <QtDBus/QDBusObjectPath>
|
||||
#include <dbusmenuexporter.h>
|
||||
#endif
|
||||
|
||||
typedef char gchar;
|
||||
typedef struct _GVariant GVariant;
|
||||
typedef struct _GDBusProxy GDBusProxy;
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
|
||||
namespace Platform {
|
||||
|
||||
@@ -33,6 +37,14 @@ public:
|
||||
|
||||
void psShowTrayMenu();
|
||||
|
||||
bool trayAvailable() {
|
||||
return _sniAvailable || QSystemTrayIcon::isSystemTrayAvailable();
|
||||
}
|
||||
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
void handleSNIHostRegistered();
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
|
||||
static void LibsLoaded();
|
||||
|
||||
~MainWindow();
|
||||
@@ -64,6 +76,7 @@ protected:
|
||||
style::color color) = 0;
|
||||
|
||||
private:
|
||||
bool _sniAvailable = false;
|
||||
Ui::PopupMenu *_trayIconMenuXEmbed = nullptr;
|
||||
|
||||
void updateIconCounters();
|
||||
@@ -71,8 +84,10 @@ private:
|
||||
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
StatusNotifierItem *_sniTrayIcon = nullptr;
|
||||
GDBusProxy *_sniDBusProxy = nullptr;
|
||||
std::unique_ptr<QTemporaryFile> _trayIconFile = nullptr;
|
||||
|
||||
bool _appMenuSupported = false;
|
||||
DBusMenuExporter *_mainMenuExporter = nullptr;
|
||||
QDBusObjectPath _mainMenuPath;
|
||||
|
||||
@@ -124,6 +139,13 @@ private:
|
||||
void psLinuxStrikeOut();
|
||||
void psLinuxMonospace();
|
||||
void psLinuxClearFormat();
|
||||
|
||||
static void sniSignalEmitted(
|
||||
GDBusProxy *proxy,
|
||||
gchar *sender_name,
|
||||
gchar *signal_name,
|
||||
GVariant *parameters,
|
||||
MainWindow *window);
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
|
||||
};
|
||||
|
||||
@@ -85,8 +85,6 @@ constexpr auto kXCBFrameExtentsAtomName = "_GTK_FRAME_EXTENTS"_cs;
|
||||
|
||||
QStringList PlatformThemes;
|
||||
|
||||
bool IsTrayIconSupported = true;
|
||||
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
void PortalAutostart(bool autostart, bool silent = false) {
|
||||
if (cExeName().isEmpty()) {
|
||||
@@ -628,6 +626,8 @@ bool StartXCBMoveResize(QWindow *window, int edges) {
|
||||
}
|
||||
|
||||
bool StartWaylandMove(QWindow *window) {
|
||||
// There are startSystemMove on Qt 5.15
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) && !defined DESKTOP_APP_QT_PATCHED
|
||||
if (const auto waylandWindow = static_cast<QWaylandWindow*>(
|
||||
window->handle())) {
|
||||
if (const auto seat = waylandWindow->display()->lastInputDevice()) {
|
||||
@@ -636,27 +636,29 @@ bool StartWaylandMove(QWindow *window) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // Qt < 5.15 && !DESKTOP_APP_QT_PATCHED
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool StartWaylandResize(QWindow *window, Qt::Edges edges) {
|
||||
// There are startSystemResize on Qt 5.15
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) && !defined DESKTOP_APP_QT_PATCHED
|
||||
if (const auto waylandWindow = static_cast<QWaylandWindow*>(
|
||||
window->handle())) {
|
||||
if (const auto seat = waylandWindow->display()->lastInputDevice()) {
|
||||
if (const auto shellSurface = waylandWindow->shellSurface()) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) || defined DESKTOP_APP_QT_PATCHED
|
||||
return shellSurface->resize(seat, edges);
|
||||
#elif QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) || defined DESKTOP_APP_QT_PATCHED // Qt >= 5.15 || DESKTOP_APP_QT_PATCHED
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
|
||||
shellSurface->resize(seat, edges);
|
||||
return true;
|
||||
#else // Qt >= 5.13 || DESKTOP_APP_QT_PATCHED
|
||||
#else // Qt >= 5.13
|
||||
shellSurface->resize(seat, WlResizeFromEdges(edges));
|
||||
return true;
|
||||
#endif // Qt < 5.13 && !DESKTOP_APP_QT_PATCHED
|
||||
#endif // Qt < 5.13
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // Qt < 5.15 && !DESKTOP_APP_QT_PATCHED
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -770,22 +772,6 @@ bool UnsetXCBFrameExtents(QWindow *window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetWaylandWindowGeometry(QWindow *window, const QRect &geometry) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) || defined DESKTOP_APP_QT_PATCHED
|
||||
if (const auto waylandWindow = static_cast<QWaylandWindow*>(
|
||||
window->handle())) {
|
||||
if (const auto seat = waylandWindow->display()->lastInputDevice()) {
|
||||
if (const auto shellSurface = waylandWindow->shellSurface()) {
|
||||
shellSurface->setWindowGeometry(geometry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // Qt >= 5.13 || DESKTOP_APP_QT_PATCHED
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Window::Control GtkKeywordToWindowControl(const QString &keyword) {
|
||||
if (keyword == qstr("minimize")) {
|
||||
return Window::Control::Minimize;
|
||||
@@ -949,17 +935,6 @@ QString SingleInstanceLocalServerName(const QString &hash) {
|
||||
|
||||
QString GetLauncherBasename() {
|
||||
static const auto Result = [&] {
|
||||
if (InSnap() && !cExeName().isEmpty()) {
|
||||
const auto snapNameKey =
|
||||
qEnvironmentVariableIsSet("SNAP_INSTANCE_NAME")
|
||||
? "SNAP_INSTANCE_NAME"
|
||||
: "SNAP_NAME";
|
||||
|
||||
return qsl("%1_%2")
|
||||
.arg(QString::fromLatin1(qgetenv(snapNameKey)))
|
||||
.arg(cExeName());
|
||||
}
|
||||
|
||||
if ((IsStaticBinary() || InAppImage()) && !cExeName().isEmpty()) {
|
||||
const auto appimagePath = qsl("file://%1%2")
|
||||
.arg(cExeDir())
|
||||
@@ -1036,7 +1011,10 @@ QImage GetImageFromClipboard() {
|
||||
|
||||
std::optional<crl::time> LastUserInputTime() {
|
||||
if (!IsWayland()) {
|
||||
return XCBLastUserInputTime();
|
||||
const auto xcbResult = XCBLastUserInputTime();
|
||||
if (xcbResult.has_value()) {
|
||||
return xcbResult;
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
@@ -1086,11 +1064,9 @@ bool AutostartSupported() {
|
||||
}
|
||||
|
||||
bool TrayIconSupported() {
|
||||
return IsTrayIconSupported;
|
||||
}
|
||||
|
||||
void SetTrayIconSupported(bool supported) {
|
||||
IsTrayIconSupported = supported;
|
||||
return App::wnd()
|
||||
? App::wnd()->trayAvailable()
|
||||
: false;
|
||||
}
|
||||
|
||||
bool StartSystemMove(QWindow *window) {
|
||||
@@ -1118,32 +1094,22 @@ bool ShowWindowMenu(QWindow *window) {
|
||||
}
|
||||
|
||||
bool SetWindowExtents(QWindow *window, const QMargins &extents) {
|
||||
if (IsWayland()) {
|
||||
const auto geometry = QRect(QPoint(), window->size())
|
||||
.marginsRemoved(extents);
|
||||
|
||||
return SetWaylandWindowGeometry(window, geometry);
|
||||
} else {
|
||||
if (!IsWayland()) {
|
||||
return SetXCBFrameExtents(window, extents);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UnsetWindowExtents(QWindow *window) {
|
||||
if (IsWayland()) {
|
||||
const auto geometry = QRect(QPoint(), window->size());
|
||||
return SetWaylandWindowGeometry(window, geometry);
|
||||
} else {
|
||||
if (!IsWayland()) {
|
||||
return UnsetXCBFrameExtents(window);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WindowsNeedShadow() {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) || defined DESKTOP_APP_QT_PATCHED
|
||||
if (IsWayland()) {
|
||||
return true;
|
||||
}
|
||||
#endif // Qt >= 5.13 || DESKTOP_APP_QT_PATCHED
|
||||
|
||||
if (!IsWayland() && XCBFrameExtentsSupported()) {
|
||||
return true;
|
||||
}
|
||||
@@ -1379,15 +1345,11 @@ void start() {
|
||||
"this may lead to font issues.");
|
||||
#endif // DESKTOP_APP_USE_PACKAGED_FONTS
|
||||
|
||||
if(IsStaticBinary()
|
||||
|| InAppImage()
|
||||
|| InFlatpak()
|
||||
|| InSnap()
|
||||
|| IsQtPluginsBundled()) {
|
||||
if (IsQtPluginsBundled()) {
|
||||
qputenv("QT_WAYLAND_DECORATION", "material");
|
||||
}
|
||||
|
||||
if((IsStaticBinary()
|
||||
if ((IsStaticBinary()
|
||||
|| InAppImage()
|
||||
|| IsQtPluginsBundled())
|
||||
// it is handled by Qt for flatpak and snap
|
||||
@@ -1457,13 +1419,13 @@ void RegisterCustomScheme(bool force) {
|
||||
|
||||
GError *error = nullptr;
|
||||
|
||||
const auto actualCommandlineBuilder = qsl("%1 --")
|
||||
const auto neededCommandlineBuilder = qsl("%1 --")
|
||||
.arg((IsStaticBinary() || InAppImage())
|
||||
? cExeDir() + cExeName()
|
||||
: cExeName());
|
||||
|
||||
const auto actualCommandline = qsl("%1 %u")
|
||||
.arg(actualCommandlineBuilder);
|
||||
const auto neededCommandline = qsl("%1 %u")
|
||||
.arg(neededCommandlineBuilder);
|
||||
|
||||
auto currentAppInfo = g_app_info_get_default_for_type(
|
||||
kHandlerTypeName.utf8(),
|
||||
@@ -1475,13 +1437,36 @@ void RegisterCustomScheme(bool force) {
|
||||
|
||||
g_object_unref(currentAppInfo);
|
||||
|
||||
if (currentCommandline == actualCommandline) {
|
||||
if (currentCommandline == neededCommandline) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto registeredAppInfoList = g_app_info_get_recommended_for_type(
|
||||
kHandlerTypeName.utf8());
|
||||
|
||||
for (auto l = registeredAppInfoList; l != nullptr; l = l->next) {
|
||||
const auto currentRegisteredAppInfo = reinterpret_cast<GAppInfo*>(
|
||||
l->data);
|
||||
|
||||
const auto currentAppInfoId = QString(
|
||||
g_app_info_get_id(currentRegisteredAppInfo));
|
||||
|
||||
const auto currentCommandline = QString(
|
||||
g_app_info_get_commandline(currentRegisteredAppInfo));
|
||||
|
||||
if (currentCommandline == neededCommandline
|
||||
&& currentAppInfoId.startsWith(qsl("userapp-"))) {
|
||||
g_app_info_delete(currentRegisteredAppInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (registeredAppInfoList) {
|
||||
g_list_free_full(registeredAppInfoList, g_object_unref);
|
||||
}
|
||||
|
||||
auto newAppInfo = g_app_info_create_from_commandline(
|
||||
actualCommandlineBuilder.toUtf8(),
|
||||
neededCommandlineBuilder.toUtf8(),
|
||||
AppName.utf8(),
|
||||
G_APP_INFO_CREATE_SUPPORTS_URIS,
|
||||
&error);
|
||||
|
||||
@@ -42,7 +42,6 @@ QString GetIconName();
|
||||
inline void IgnoreApplicationActivationRightNow() {
|
||||
}
|
||||
|
||||
void SetTrayIconSupported(bool supported);
|
||||
void InstallMainDesktopFile();
|
||||
|
||||
} // namespace Platform
|
||||
|
||||
@@ -35,8 +35,29 @@ bool IsCompositionEnabled() {
|
||||
return success && result;
|
||||
}
|
||||
|
||||
bool IsTaskbarAutoHidden(PUINT pEdge = nullptr) {
|
||||
HWND hTaskbar = FindWindowW(L"Shell_TrayWnd", nullptr);
|
||||
HWND FindTaskbarWindow(LPRECT rcMon = nullptr) {
|
||||
HWND hTaskbar = nullptr;
|
||||
RECT rcTaskbar, rcMatch;
|
||||
|
||||
while ((hTaskbar = FindWindowEx(
|
||||
nullptr,
|
||||
hTaskbar,
|
||||
L"Shell_TrayWnd",
|
||||
nullptr)) != nullptr) {
|
||||
if (!rcMon) {
|
||||
break; // OK, return first found
|
||||
}
|
||||
if (GetWindowRect(hTaskbar, &rcTaskbar)
|
||||
&& IntersectRect(&rcMatch, &rcTaskbar, rcMon)) {
|
||||
break; // OK, taskbar match monitor
|
||||
}
|
||||
}
|
||||
|
||||
return hTaskbar;
|
||||
}
|
||||
|
||||
bool IsTaskbarAutoHidden(LPRECT rcMon = nullptr, PUINT pEdge = nullptr) {
|
||||
HWND hTaskbar = FindTaskbarWindow(rcMon);
|
||||
if (!hTaskbar) {
|
||||
if (pEdge) {
|
||||
*pEdge = (UINT)-1;
|
||||
@@ -55,7 +76,7 @@ bool IsTaskbarAutoHidden(PUINT pEdge = nullptr) {
|
||||
*pEdge = pos.uEdge;
|
||||
}
|
||||
} else {
|
||||
Unexpected("Failed to get taskbar pos");
|
||||
LOG(("Failed to get taskbar pos"));
|
||||
if (pEdge) {
|
||||
*pEdge = ABE_BOTTOM;
|
||||
}
|
||||
@@ -134,7 +155,7 @@ bool EventFilter::customWindowFrameEvent(
|
||||
if (GetMonitorInfo(hMonitor, &mi)) {
|
||||
*r = mi.rcWork;
|
||||
UINT uEdge = (UINT)-1;
|
||||
if (IsTaskbarAutoHidden(&uEdge)) {
|
||||
if (IsTaskbarAutoHidden(&mi.rcMonitor, &uEdge)) {
|
||||
switch (uEdge) {
|
||||
case ABE_LEFT: r->left += 1; break;
|
||||
case ABE_RIGHT: r->right -= 1; break;
|
||||
|
||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/platform/ui_platform_utility.h"
|
||||
#include "history/history.h"
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/window_title_qt.h" // kShowAfterWindowFlagChangeDelay
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_lock_widgets.h"
|
||||
#include "window/window_outdated_bar.h"
|
||||
@@ -363,6 +364,21 @@ void MainWindow::refreshTitleWidget() {
|
||||
_title->init();
|
||||
_titleShadow.destroy();
|
||||
}
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// setWindowFlag calls setParent(parentWidget(), newFlags), which
|
||||
// always calls hide() explicitly, we have to show() the window back.
|
||||
const auto hidden = isHidden();
|
||||
const auto withShadow = hasShadow();
|
||||
setWindowFlag(Qt::NoDropShadowWindowHint, withShadow);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent, !withShadow);
|
||||
if (!hidden) {
|
||||
base::call_delayed(
|
||||
kShowAfterWindowFlagChangeDelay,
|
||||
this,
|
||||
[=] { show(); });
|
||||
}
|
||||
#endif // Q_OS_LINUX
|
||||
}
|
||||
|
||||
void MainWindow::updateMinimumSize() {
|
||||
@@ -377,12 +393,6 @@ void MainWindow::updateShadowSize() {
|
||||
}
|
||||
|
||||
void MainWindow::recountGeometryConstraints() {
|
||||
#ifdef Q_OS_LINUX
|
||||
const auto hasShadow = this->hasShadow();
|
||||
setWindowFlag(Qt::NoDropShadowWindowHint, hasShadow);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent, !hasShadow);
|
||||
#endif // Q_OS_LINUX
|
||||
|
||||
updateShadowSize();
|
||||
updateMinimumSize();
|
||||
updateControlsGeometry();
|
||||
@@ -705,6 +715,17 @@ void MainWindow::launchDrag(std::unique_ptr<QMimeData> data) {
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
_title.destroy();
|
||||
|
||||
// Otherwise:
|
||||
// ~QWidget
|
||||
// QWidgetPrivate::close_helper
|
||||
// QWidgetPrivate::setVisible
|
||||
// QWidgetPrivate::hide_helper
|
||||
// QWidgetPrivate::hide_sys
|
||||
// QWindowPrivate::setVisible
|
||||
// QMetaObject::activate
|
||||
// Window::MainWindow::handleVisibleChanged on a destroyed MainWindow.
|
||||
hide();
|
||||
}
|
||||
|
||||
} // namespace Window
|
||||
|
||||
@@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/delayed_activation.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "boxes/calendar_box.h"
|
||||
#include "boxes/sticker_set_box.h" // requestAttachedStickerSets.
|
||||
#include "boxes/confirm_box.h" // requestAttachedStickerSets.
|
||||
@@ -321,6 +322,10 @@ void SessionNavigation::showRepliesForMessage(
|
||||
});
|
||||
}).fail([=](const RPCError &error) {
|
||||
_showingRepliesRequestId = 0;
|
||||
if (error.type() == u"CHANNEL_PRIVATE"_q
|
||||
|| error.type() == u"USER_BANNED_IN_CHANNEL"_q) {
|
||||
Ui::Toast::Show(tr::lng_group_not_accessible(tr::now));
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Window {
|
||||
namespace {
|
||||
|
||||
// If we toggle frameless window hint in maximized window, and
|
||||
// show it back too quickly, the mouse position inside the window
|
||||
// won't be correct (from Qt-s point of view) until we Alt-Tab from
|
||||
// that window. If we show the window back with this delay it works.
|
||||
constexpr auto kShowAfterFramelessToggleDelay = crl::time(1000);
|
||||
|
||||
style::margins ShadowExtents() {
|
||||
[[nodiscard]] style::margins ShadowExtents() {
|
||||
return st::callShadow.extend;
|
||||
}
|
||||
|
||||
@@ -103,7 +97,7 @@ void TitleWidgetQt::toggleFramelessWindow(bool enabled) {
|
||||
top->setWindowFlag(Qt::FramelessWindowHint, enabled);
|
||||
if (!hidden) {
|
||||
base::call_delayed(
|
||||
kShowAfterFramelessToggleDelay,
|
||||
kShowAfterWindowFlagChangeDelay,
|
||||
top,
|
||||
[=] { top->show(); });
|
||||
}
|
||||
@@ -252,14 +246,6 @@ void TitleWidgetQt::mouseDoubleClickEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
bool TitleWidgetQt::eventFilter(QObject *obj, QEvent *e) {
|
||||
// I tried to listen only QEvent::Move and QEvent::Resize
|
||||
// but that doesn't work on Wayland
|
||||
if (obj->isWidgetType()
|
||||
&& window() == static_cast<QWidget*>(obj)
|
||||
&& Platform::IsWayland()) {
|
||||
updateWindowExtents();
|
||||
}
|
||||
|
||||
if (e->type() == QEvent::MouseMove
|
||||
|| e->type() == QEvent::MouseButtonPress) {
|
||||
if (window()->isAncestorOf(static_cast<QWidget*>(obj))) {
|
||||
|
||||
@@ -22,6 +22,12 @@ class PlainShadow;
|
||||
|
||||
namespace Window {
|
||||
|
||||
// If we toggle frameless window hint in maximized window, and
|
||||
// show it back too quickly, the mouse position inside the window
|
||||
// won't be correct (from Qt-s point of view) until we Alt-Tab from
|
||||
// that window. If we show the window back with this delay it works.
|
||||
inline constexpr auto kShowAfterWindowFlagChangeDelay = crl::time(1000);
|
||||
|
||||
class TitleWidgetQt : public TitleWidget {
|
||||
public:
|
||||
TitleWidgetQt(QWidget *parent);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
AppVersion 2004000
|
||||
AppVersion 2004003
|
||||
AppVersionStrMajor 2.4
|
||||
AppVersionStrSmall 2.4
|
||||
AppVersionStr 2.4.0
|
||||
AppVersionStrSmall 2.4.3
|
||||
AppVersionStr 2.4.3
|
||||
BetaChannel 0
|
||||
AlphaVersion 0
|
||||
AppVersionOriginal 2.4
|
||||
AppVersionOriginal 2.4.3
|
||||
|
||||
@@ -827,7 +827,7 @@ if (NOT TGVOIP_FOUND)
|
||||
|
||||
if (LINUX)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(ALSA REQUIRED alsa)
|
||||
find_package(ALSA REQUIRED)
|
||||
pkg_check_modules(PULSE REQUIRED libpulse)
|
||||
|
||||
target_include_directories(lib_tgvoip_bundled
|
||||
|
||||
@@ -31,8 +31,6 @@ PRIVATE
|
||||
export/output/export_output_result.h
|
||||
export/output/export_output_stats.cpp
|
||||
export/output/export_output_stats.h
|
||||
export/output/export_output_text.cpp
|
||||
export/output/export_output_text.h
|
||||
)
|
||||
|
||||
target_include_directories(td_export
|
||||
|
||||
Submodule Telegram/lib_rpl updated: 86b9b80fae...e1b96399d9
Submodule Telegram/lib_ui updated: 86e141272a...ae340a0b76
@@ -1,3 +1,21 @@
|
||||
2.4.3 (07.10.20)
|
||||
|
||||
- Fix sending voice messages in scheduled messages section.
|
||||
- Fix deleting profile / group / channel photos.
|
||||
- Several crash fixes.
|
||||
|
||||
2.4.2 (02.10.20)
|
||||
|
||||
- Allow block, report and delete all message from user from "user joined" service message context menu.
|
||||
- Fix admin badge display in groups.
|
||||
- Fix loading and opening of comments in channels.
|
||||
|
||||
2.4.1 (01.10.20)
|
||||
|
||||
- Move by PageUp and PageDown in channel comments.
|
||||
- Several layout bugfixes.
|
||||
- Several crashfixes.
|
||||
|
||||
2.4 (30.09.20)
|
||||
|
||||
- Turn on "Remain Anonymous" in an admin's Permissions to let them post on behalf of the group and become invisible in the list of members.
|
||||
|
||||
@@ -16,7 +16,7 @@ apps:
|
||||
telegram-desktop:
|
||||
command: bin/desktop-launch telegram-desktop
|
||||
common-id: org.telegram.desktop
|
||||
desktop: usr/share/applications/telegramdesktop.desktop
|
||||
desktop: usr/share/applications/telegram-desktop_telegram-desktop.desktop
|
||||
environment:
|
||||
# Use GTK3 cursor theme, icon theme and open/save file dialogs.
|
||||
QT_QPA_PLATFORMTHEME: gtk3
|
||||
@@ -29,7 +29,7 @@ apps:
|
||||
- desktop-legacy
|
||||
- home
|
||||
- network
|
||||
- network-manager
|
||||
- network-manager-observe
|
||||
- opengl
|
||||
- pulseaudio
|
||||
- removable-media
|
||||
@@ -65,7 +65,7 @@ parts:
|
||||
plugin: cmake
|
||||
source: .
|
||||
source-type: git
|
||||
parse-info: [usr/share/metainfo/telegramdesktop.appdata.xml]
|
||||
parse-info: [usr/share/metainfo/telegram-desktop_telegram-desktop.appdata.xml]
|
||||
build-environment:
|
||||
- LD_LIBRARY_PATH: $SNAPCRAFT_STAGE/usr/lib
|
||||
- tg_owt_DIR: $SNAPCRAFT_STAGE/tg_owt
|
||||
@@ -105,6 +105,7 @@ parts:
|
||||
- -DTDESKTOP_API_HASH=d524b414d21f4d37f08684c1df41ac9c
|
||||
- -DDESKTOP_APP_USE_PACKAGED_LAZY=ON
|
||||
- -DDESKTOP_APP_QTWAYLANDCLIENT_PRIVATE_HEADERS=$SNAPCRAFT_STAGE/usr/include/$SNAPCRAFT_ARCH_TRIPLET/qt5/QtWaylandClient/5.12.8
|
||||
- -DTDESKTOP_LAUNCHER_BASENAME=telegram-desktop_telegram-desktop
|
||||
override-pull: |
|
||||
snapcraftctl pull
|
||||
|
||||
|
||||
Reference in New Issue
Block a user