Compare commits

..

32 Commits

Author SHA1 Message Date
John Preston
f3a2460a54 Version 2.4.1: Fix build for Linux. 2020-10-01 20:06:00 +03:00
John Preston
04212140cc Version 2.4.1: Fix capture error reporting. 2020-10-01 18:46:22 +03:00
John Preston
4e1904b137 Version 2.4.1.
- Move by PageUp and PageDown in channel comments.
- Several layout bugfixes.
- Several crashfixes.
2020-10-01 18:24:29 +03:00
John Preston
0299ba4873 Allow in groups to delete service messages for everyone.
Fixes #8690.
2020-10-01 18:21:39 +03:00
23rd
46ce0df832 Fixed crash in SessionsBox when list of sessions is empty. 2020-10-01 18:14:09 +03:00
John Preston
d66debd802 Fix crash on bad message in Replies section. 2020-10-01 18:08:27 +03:00
John Preston
454fe8cdf7 Fix crash in calls box. 2020-10-01 17:52:02 +03:00
John Preston
c4dfc634d0 Fix crash in main window destruction. 2020-10-01 17:45:25 +03:00
John Preston
b08fa069b4 Fix assertion violation in case of bad messages. 2020-10-01 17:38:49 +03:00
John Preston
3d20958bb4 Remove assertion about taskbar position. 2020-10-01 17:33:59 +03:00
John Preston
c693fcb2b0 Reopen third column in Replies section.
Fixes #8674, fixes #8687.
2020-10-01 17:20:08 +03:00
John Preston
ddad42d80e Add report button to comments context menu.
Fixes #8679.
2020-10-01 17:06:04 +03:00
John Preston
c6f66e83ee Fix restriction label display.
Fixes #8680.
2020-10-01 16:53:39 +03:00
John Preston
8bb3b7fada Handle some errors on comments open.
Fixes #8682.
2020-10-01 16:42:31 +03:00
Ilya Fedin
1d24d29afa Little cleanup for Linux platform code & build
CheckCXXSourceCompiles is not needed anymore

Material wayland decorations could be checked just with IsQtPluginsBundled
2020-10-01 16:34:01 +03:00
Ilya Fedin
0536a479f9 Use startSystemMove/startSystemResize instead of platform code on Wayland with Qt 5.15 2020-10-01 16:30:53 +03:00
Ilya Fedin
7fef7e6315 Don't add shadow on Wayland
It was implemented like a hack and worked like a hack... Looks like it is better to wait until Qt give a way to create shadows.
2020-10-01 16:30:53 +03:00
John Preston
c6ef2b057e Fix couple of visual glitches.
Fixes #8676.
2020-10-01 16:29:09 +03:00
John Preston
784f10678c Fix root comments post layout. 2020-10-01 15:12:57 +03:00
John Preston
92dbd7089b Fix comments layout bug for narrow photos. 2020-10-01 14:05:26 +03:00
John Preston
415990c913 Show View thread button only in discussions for now. 2020-10-01 13:38:36 +03:00
John Preston
e42af74dd2 Don't try to open comments in invite peek channel. 2020-10-01 13:15:10 +03:00
John Preston
bd1a46252d Show admin rank for anonymous posts. 2020-10-01 12:57:03 +03:00
John Preston
874e5e0a61 Fix export of discussion messages. 2020-10-01 12:42:35 +03:00
John Preston
81457693f1 Don't add comments button for inline markup messages.
Fixes #8664.
2020-10-01 11:44:08 +03:00
John Preston
3a700650be Remove comments info if no info in server data. 2020-10-01 11:34:59 +03:00
John Preston
d642c3f3b5 Hide bot about header for Replies chat. 2020-10-01 11:19:14 +03:00
John Preston
4be03ffc25 Fix PageUp/PageDown scrolling in Replies section.
Fixes #8666.
2020-10-01 11:00:29 +03:00
Ilya Fedin
dcac3146c7 Fix Linux GitHub action 2020-10-01 10:54:42 +03:00
Ilya Fedin
10012d6b31 Handle launcher basename compile-time for snap
Just like for flatpak
2020-10-01 10:54:42 +03:00
John Preston
3aa1b1e9ae Fix recent commenters userpics border. 2020-10-01 10:52:53 +03:00
John Preston
4e8a1f8d29 Fix voice messages sending. 2020-10-01 10:47:03 +03:00
53 changed files with 520 additions and 1461 deletions

View File

@@ -443,6 +443,7 @@ jobs:
-qt-harfbuzz \
-qt-pcre \
-qt-xcb \
-no-icu \
-no-gtk \
-static \
-dbus-runtime \

View File

@@ -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)

View File

@@ -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.1.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>

View File

@@ -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,1,0
PRODUCTVERSION 2,4,1,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.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.4.0.0"
VALUE "ProductVersion", "2.4.1.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -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,1,0
PRODUCTVERSION 2,4,1,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.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.4.0.0"
VALUE "ProductVersion", "2.4.1.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -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);

View File

@@ -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());

View File

@@ -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 = 2004001;
constexpr auto AppVersionStr = "2.4.1";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -521,6 +521,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) {
@@ -530,6 +531,8 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
_list.push_back(item->id);
}
}
} else {
++skipped;
}
}
if (toFront) {
@@ -545,7 +548,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 +566,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 +580,7 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
}
}
return false;
return (list.size() > skipped);
}
} // namespace Data

View File

@@ -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()) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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.");

View File

@@ -35,8 +35,6 @@ class Stats;
enum class Format {
Html,
Json,
Text,
Yaml,
};
class AbstractWriter {

View File

@@ -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())

View File

@@ -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 = [&](

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -599,9 +599,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;
}
@@ -717,7 +715,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();
}

View File

@@ -287,6 +287,8 @@ public:
}
virtual void setReplies(const MTPMessageReplies &data) {
}
virtual void clearReplies() {
}
virtual void changeRepliesCount(int delta, PeerId replier) {
}
virtual void setReplyToTop(MsgId replyToTop) {

View File

@@ -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);

View File

@@ -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> {

View File

@@ -1028,7 +1028,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(
@@ -1049,6 +1049,7 @@ void HistoryMessage::createComponents(const CreateConfig &config) {
}
if (const auto msgsigned = Get<HistoryMessageSigned>()) {
msgsigned->author = config.author;
msgsigned->isAnonymousRank = author()->isMegagroup();
}
setupForwardedComponent(config);
if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
@@ -1631,6 +1632,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) {

View File

@@ -126,6 +126,7 @@ 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 setRealId(MsgId newId) override;

View File

@@ -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;
}
@@ -3361,6 +3355,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 +3365,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 +3405,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 +3673,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 {

View File

@@ -312,10 +312,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 +408,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 +705,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.

View File

@@ -732,6 +732,7 @@ void ComposeControls::showStarted() {
_tabbedPanel->hideFast();
}
_wrap->hide();
_writeRestricted->hide();
}
void ComposeControls::showFinished() {
@@ -741,7 +742,7 @@ void ComposeControls::showFinished() {
if (_tabbedPanel) {
_tabbedPanel->hideFast();
}
_wrap->show();
updateWrappingVisibility();
}
void ComposeControls::showForGrab() {
@@ -786,25 +787,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 +837,6 @@ void ComposeControls::init() {
}
}
void ComposeControls::recordError() {
stopRecording(false);
}
void ComposeControls::recordDone(
QByteArray result,
VoiceWaveform waveform,
@@ -889,20 +867,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 +906,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 +1079,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 = [&] {

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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();
@@ -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();
@@ -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;
}
@@ -1304,7 +1286,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 +1692,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 +2005,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 +2401,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 +2542,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();

View File

@@ -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;

View File

@@ -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()),
@@ -1657,7 +1658,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() {

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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()));
}

View File

@@ -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()) {

View File

@@ -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()) {

View File

@@ -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()

View File

@@ -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,32 @@ 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(), [=] {
_inner->stop(callback);
});
}
void Instance::check() {
_available = false;
if (auto device = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) {
@@ -71,7 +116,8 @@ void Instance::check() {
}
Instance::~Instance() {
_inner = nullptr;
InvokeQueued(_inner.get(), [copy = base::take(_inner)] {
});
_thread.quit();
_thread.wait();
}
@@ -155,34 +201,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 +241,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 +252,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 +269,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 +301,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 +332,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 +413,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 +512,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);
if (ErrorHappened(d->device)) {
onStop(false);
emit error();
fail();
return;
}
if (samples > 0) {
@@ -490,8 +539,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 +560,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 +587,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 +634,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 +642,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 +672,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 +713,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 +721,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;
}

View File

@@ -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

View File

@@ -628,6 +628,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 +638,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 +774,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 +937,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())
@@ -1118,32 +1095,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 +1346,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

View File

@@ -55,7 +55,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;
}

View File

@@ -705,6 +705,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

View File

@@ -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();
}

View File

@@ -252,14 +252,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))) {

View File

@@ -1,7 +1,7 @@
AppVersion 2004000
AppVersion 2004001
AppVersionStrMajor 2.4
AppVersionStrSmall 2.4
AppVersionStr 2.4.0
AppVersionStrSmall 2.4.1
AppVersionStr 2.4.1
BetaChannel 0
AlphaVersion 0
AppVersionOriginal 2.4
AppVersionOriginal 2.4.1

View File

@@ -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

View File

@@ -1,3 +1,9 @@
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.

View File

@@ -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
@@ -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