Compare commits

...

20 Commits

Author SHA1 Message Date
detiam 6a2896e730
Merge 9409fb344f into c0db5ee98a 2024-04-27 01:50:49 +06:00
John Preston c0db5ee98a Beta version 4.16.10: Fix GCC build. 2024-04-26 23:41:28 +04:00
John Preston 372b3da09c Beta version 4.16.10.
- Group admins can mass-moderate many messages.
- Premium users can use animated emoji in polls.
- Revert the default "Open Sans" font to 1.10.
- Several crash fixes and small improvements.
2024-04-26 20:55:06 +04:00
John Preston 79532954dc Allow a bit more font size adjusting. 2024-04-26 20:18:30 +04:00
23rd aff2be605e Removed item for poll creation from menu when it is impossible. 2024-04-26 19:15:03 +03:00
John Preston 363c191a6e Skip media bottom skip in IV. 2024-04-26 20:12:30 +04:00
John Preston 2949cdab61 Don't request IV two times in a row. 2024-04-26 20:12:29 +04:00
John Preston 7addcf2d25 Add IV footer. 2024-04-26 20:12:29 +04:00
John Preston a272807a99 Remove "Create poll" button in Replies chat.
Fixes #27817.
2024-04-26 20:12:29 +04:00
23rd c803603de4 Added ability to insert custom emoji to polls. 2024-04-26 20:12:29 +04:00
23rd e6c22ec1ca Added api support for custom emoji in polls. 2024-04-26 20:12:29 +04:00
John Preston b3ae843f0e Update API scheme to layer 179. 2024-04-26 20:12:29 +04:00
John Preston 12a78c1f45 Allow pasting text to the unfocused search. 2024-04-26 20:12:29 +04:00
23rd 612b81ee87 Fixed allowed reactions for channel posts in linked chats. 2024-04-26 20:12:29 +04:00
23rd e5b91d2f3d Added entry points for moderation box. 2024-04-26 20:12:29 +04:00
23rd 82293c98eb Added arrow icon to divider link in moderation box. 2024-04-26 20:12:29 +04:00
23rd 629da68cfc Added api support to moderation box. 2024-04-26 20:12:29 +04:00
23rd 643ecd2c2c Implemented initial ui of moderation box. 2024-04-26 20:12:29 +04:00
Ilya Fedin dd1cb00c62 Update snap to core24 2024-04-26 18:39:56 +04:00
detiam 9409fb344f
Fix scroll issue in media view on some logitech hi-res wheel mouse 2023-12-21 13:54:13 +08:00
48 changed files with 1361 additions and 259 deletions

View File

@ -273,6 +273,8 @@ PRIVATE
boxes/local_storage_box.h
boxes/max_invite_box.cpp
boxes/max_invite_box.h
boxes/moderate_messages_box.cpp
boxes/moderate_messages_box.h
boxes/peer_list_box.cpp
boxes/peer_list_box.h
boxes/peer_list_controllers.cpp

View File

@ -134,9 +134,32 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
.page-slide {
position: relative;
width: 100%;
min-height: 100%;
margin-left: 0%;
transition: margin 240ms ease-in-out;
}
.page-footer {
height: 32px;
margin-top: -32px;
background: var(--td-window-bg-over);
}
.page-footer .content {
padding: 3px 18px;
font-size: 15px;
color: var(--td-window-sub-text-fg);
text-align: center;
}
.page-footer .wrong {
position: relative;
padding: 5px;
margin: -5px;
color: var(--td-window-sub-text-fg);
text-decoration: none;
cursor: pointer;
}
.page-footer .wrong:hover {
text-decoration: underline;
}
.hidden-left,
.hidden-right {
pointer-events: none;
@ -148,7 +171,7 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
margin-left: 100%;
}
article {
padding-bottom: 12px;
padding-bottom: 40px;
overflow-y: hidden;
overflow-x: auto;
white-space: pre-wrap;
@ -893,6 +916,9 @@ section.related a.related-link:after {
right: 0;
bottom: 0;
}
section.related a.related-link:last-child:after {
border-bottom: 0px;
}
section.related .related-link-url {
display: block;
font-size: 15px;
@ -1027,6 +1053,9 @@ section.channel > a > h4 {
display: block;
margin: 0 auto;
}
.media-outer {
margin-bottom: 16px;
}
.photo-wrap,
.video-wrap {
width: 100%;

View File

@ -26,7 +26,7 @@ var IV = {
}
target = target.parentNode;
}
if (!target || !target.hasAttribute('href')) {
if (!target || (context === '' && !target.hasAttribute('href'))) {
return;
}
var base = document.createElement('A');
@ -413,9 +413,12 @@ var IV = {
var article = function (el) {
return el.getElementsByTagName('article')[0];
};
var from = article(IV.findPageScroll());
var to = article(IV.makeScrolledContent(data.html));
morphdom(from, to, {
var footer = function (el) {
return el.getElementsByClassName('page-footer')[0];
};
var from = IV.findPageScroll();
var to = IV.makeScrolledContent(data.html);
morphdom(article(from), article(to), {
onBeforeElUpdated: function (fromEl, toEl) {
if (fromEl.classList.contains('video')
&& toEl.classList.contains('video')
@ -439,6 +442,7 @@ var IV = {
return !fromEl.isEqualNode(toEl);
}
});
morphdom(footer(from), footer(to));
IV.initMedia();
eval(data.js);
},
@ -477,9 +481,7 @@ var IV = {
var result = document.createElement('div');
result.className = 'page-scroll';
result.tabIndex = '-1';
result.innerHTML = '<div class="page-slide"><article>'
+ html
+ '</article></div>';
result.innerHTML = html.trim();
result.onscroll = IV.frameScrolled;
return result;
},

View File

@ -2854,7 +2854,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_in_dlg_audio_count#other" = "{count} audio";
"lng_ban_user" = "Ban User";
"lng_ban_users" = "Ban users";
"lng_restrict_users" = "Restrict users";
"lng_delete_all_from_user" = "Delete all from {user}";
"lng_delete_all_from_users" = "Delete all from users";
"lng_restrict_user_part" = "Partially restrict this user {emoji}";
"lng_restrict_users_part" = "Partially restrict users {emoji}";
"lng_restrict_user_full" = "Fully ban this user {emoji}";
"lng_restrict_users_full" = "Fully ban users {emoji}";
"lng_restrict_users_part_single_header" = "What can this user do?";
"lng_restrict_users_part_header#one" = "What can {count} selected user do?";
"lng_restrict_users_part_header#other" = "What can {count} selected users do?";
"lng_report_spam" = "Report Spam";
"lng_report_spam_and_leave" = "Report spam and leave";
"lng_report_spam_done" = "Thank you for your report.";
@ -5101,6 +5111,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_iv_share" = "Share";
"lng_iv_join_channel" = "Join";
"lng_iv_window_title" = "Instant View";
"lng_iv_wrong_layout" = "Wrong layout?";
"lng_limit_download_title" = "Download speed limited";
"lng_limit_download_subscribe" = "Subscribe to {link} and increase download speed {increase}.";

View File

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="4.16.9.0" />
Version="4.16.10.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View File

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,16,9,0
PRODUCTVERSION 4,16,9,0
FILEVERSION 4,16,10,0
PRODUCTVERSION 4,16,10,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "4.16.9.0"
VALUE "FileVersion", "4.16.10.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "4.16.9.0"
VALUE "ProductVersion", "4.16.10.0"
END
END
BLOCK "VarFileInfo"

View File

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,16,9,0
PRODUCTVERSION 4,16,9,0
FILEVERSION 4,16,10,0
PRODUCTVERSION 4,16,10,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", "4.16.9.0"
VALUE "FileVersion", "4.16.10.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "4.16.9.0"
VALUE "ProductVersion", "4.16.10.0"
END
END
BLOCK "VarFileInfo"

View File

@ -592,6 +592,33 @@ ChatParticipants::Parsed ChatParticipants::ParseRecent(
return result;
}
void ChatParticipants::Restrict(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail) {
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel,
participant->input,
MTP_chatBannedRights(
MTP_flags(MTPDchatBannedRights::Flags::from_raw(
uint32(newRights.flags))),
MTP_int(newRights.until))
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
channel->applyEditBanned(participant, oldRights, newRights);
if (onDone) {
onDone();
}
}).fail([=] {
if (onFail) {
onFail();
}
}).send();
}
void ChatParticipants::requestSelf(not_null<ChannelData*> channel) {
if (_selfParticipantRequests.contains(channel)) {
return;

View File

@ -100,6 +100,13 @@ public:
static Parsed ParseRecent(
not_null<ChannelData*> channel,
const TLMembers &data);
static void Restrict(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail);
void add(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,

View File

@ -62,6 +62,10 @@ void ConfirmPhone::resolve(
return bad("FirebaseSms");
}, [&](const MTPDauth_sentCodeTypeEmailCode &) {
return bad("EmailCode");
}, [&](const MTPDauth_sentCodeTypeSmsWord &) {
return bad("SmsWord");
}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {
return bad("SmsPhrase");
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
return bad("SetUpEmailRequired");
});

View File

@ -691,6 +691,10 @@ createPollOptionField: InputField(createPollField) {
placeholderMargins: margins(2px, 0px, 2px, 0px);
heightMax: 68px;
}
createPollOptionFieldPremium: InputField(createPollOptionField) {
textMargins: margins(22px, 11px, 68px, 11px);
}
createPollOptionFieldPremiumEmojiPosition: point(15px, -1px);
createPollSolutionField: InputField(createPollField) {
textMargins: margins(0px, 4px, 0px, 4px);
border: 1px;
@ -704,7 +708,7 @@ createPollOptionRemove: CrossButton {
cross: CrossAnimation {
size: 22px;
skip: 6px;
stroke: 1.;
stroke: 1.5;
minScale: 0.3;
}
crossFg: boxTitleCloseFg;
@ -718,6 +722,7 @@ createPollOptionRemove: CrossButton {
}
}
createPollOptionRemovePosition: point(11px, 9px);
createPollOptionEmojiPositionSkip: 4px;
createPollWarning: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
palette: TextPalette(defaultTextPalette) {
@ -1074,3 +1079,23 @@ collectibleBox: Box(defaultBox) {
buttonHeight: 36px;
button: collectibleCopy;
}
moderateBoxUserpic: UserpicButton(defaultUserpicButton) {
size: size(34px, 42px);
photoSize: 34px;
photoPosition: point(0px, 4px);
}
moderateBoxExpand: icon {{ "chat/reply_type_group", boxTextFg }};
moderateBoxExpandHeight: 20px;
moderateBoxExpandRight: 10px;
moderateBoxExpandInnerSkip: 2px;
moderateBoxExpandFont: font(11px);
moderateBoxExpandToggleSize: 4px;
moderateBoxExpandToggleFourStrokes: 3px;
moderateBoxExpandIcon: icon{{ "info/edit/expand_arrow_small-flip_vertical", windowActiveTextFg }};
moderateBoxExpandIconDown: icon{{ "info/edit/expand_arrow_small", windowActiveTextFg }};
moderateBoxDividerLabel: FlatLabel(boxDividerLabel) {
palette: TextPalette(defaultTextPalette) {
selectLinkFg: windowActiveTextFg;
}
}

View File

@ -7,33 +7,41 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/create_poll_box.h"
#include "lang/lang_keys.h"
#include "data/data_poll.h"
#include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "main/main_session.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "base/call_delayed.h"
#include "base/event_filter.h"
#include "base/random.h"
#include "base/unique_qptr.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "menu/menu_send.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_poll.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/view/history_view_schedule_box.h"
#include "base/unique_qptr.h"
#include "base/event_filter.h"
#include "base/call_delayed.h"
#include "base/random.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "menu/menu_send.h"
#include "ui/controls/emoji_button.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h" // defaultComposeFiles.
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace {
@ -46,12 +54,107 @@ constexpr auto kSolutionLimit = 200;
constexpr auto kWarnSolutionLimit = 60;
constexpr auto kErrorLimit = 99;
[[nodiscard]] not_null<Ui::EmojiButton*> AddEmojiToggleToField(
not_null<Ui::InputField*> field,
not_null<Ui::BoxContent*> box,
not_null<Window::SessionController*> controller,
not_null<ChatHelpers::TabbedPanel*> emojiPanel,
QPoint shift) {
const auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(
field->parentWidget(),
st::defaultComposeFiles.emoji);
const auto fade = Ui::CreateChild<Ui::FadeAnimation>(
emojiToggle,
emojiToggle,
0.5);
{
const auto fadeTarget = Ui::CreateChild<Ui::RpWidget>(emojiToggle);
fadeTarget->resize(emojiToggle->size());
fadeTarget->paintRequest(
) | rpl::start_with_next([=](const QRect &rect) {
auto p = QPainter(fadeTarget);
if (fade->animating()) {
p.fillRect(fadeTarget->rect(), st::boxBg);
}
fade->paint(p);
}, fadeTarget->lifetime());
rpl::single(false) | rpl::then(
field->focusedChanges()
) | rpl::start_with_next([=](bool shown) {
if (shown) {
fade->fadeIn(st::universalDuration);
} else {
fade->fadeOut(st::universalDuration);
}
}, emojiToggle->lifetime());
fade->fadeOut(1);
fade->finish();
}
const auto outer = box->getDelegate()->outerContainer();
const auto allow = [](not_null<DocumentData*>) { return true; };
InitMessageFieldHandlers(
controller,
field,
Window::GifPauseReason::Layer,
allow);
Ui::Emoji::SuggestionsController::Init(
outer,
field,
&controller->session(),
Ui::Emoji::SuggestionsController::Options{
.suggestCustomEmoji = true,
.allowCustomWithoutPremium = allow,
});
const auto updateEmojiPanelGeometry = [=] {
const auto parent = emojiPanel->parentWidget();
const auto global = emojiToggle->mapToGlobal({ 0, 0 });
const auto local = parent->mapFromGlobal(global);
const auto right = local.x() + emojiToggle->width() * 3;
const auto isDropDown = local.y() < parent->height() / 2;
emojiPanel->setDropDown(isDropDown);
if (isDropDown) {
emojiPanel->moveTopRight(
local.y() + emojiToggle->height(),
right);
} else {
emojiPanel->moveBottomRight(local.y(), right);
}
};
rpl::combine(
box->sizeValue(),
field->geometryValue()
) | rpl::start_with_next([=](QSize outer, QRect inner) {
emojiToggle->moveToLeft(
rect::right(inner) + shift.x(),
inner.y() + shift.y());
emojiToggle->update();
}, emojiToggle->lifetime());
emojiToggle->installEventFilter(emojiPanel);
emojiToggle->addClickHandler([=] {
updateEmojiPanelGeometry();
emojiPanel->toggleAnimated();
});
const auto filterCallback = [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Enter) {
updateEmojiPanelGeometry();
}
return base::EventFilterResult::Continue;
};
base::install_event_filter(emojiToggle, filterCallback);
return emojiToggle;
}
class Options {
public:
Options(
not_null<QWidget*> outer,
not_null<Ui::BoxContent*> box,
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
not_null<Window::SessionController*> controller,
ChatHelpers::TabbedPanel *emojiPanel,
bool chooseCorrectEnabled);
[[nodiscard]] bool hasOptions() const;
@ -140,9 +243,10 @@ private:
[[nodiscard]] auto createChooseCorrectGroup()
-> std::shared_ptr<Ui::RadiobuttonGroup>;
not_null<QWidget*> _outer;
not_null<Ui::BoxContent*> _box;
not_null<Ui::VerticalLayout*> _container;
const not_null<Main::Session*> _session;
const not_null<Window::SessionController*> _controller;
ChatHelpers::TabbedPanel * const _emojiPanel;
std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
int _position = 0;
std::vector<std::unique_ptr<Option>> _list;
@ -154,6 +258,7 @@ private:
rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
rpl::event_stream<> _backspaceInFront;
rpl::event_stream<> _tabbed;
rpl::lifetime _emojiPanelLifetime;
};
@ -193,8 +298,9 @@ not_null<Ui::FlatLabel*> CreateWarningLabel(
if (value >= 0) {
result->setText(QString::number(value));
} else {
constexpr auto kMinus = QChar(0x2212);
result->setMarkedText(Ui::Text::Colorized(
QString::number(value)));
kMinus + QString::number(std::abs(value))));
}
result->setVisible(shown);
}));
@ -223,7 +329,9 @@ Options::Option::Option(
, _field(
Ui::CreateChild<Ui::InputField>(
_content.get(),
st::createPollOptionField,
session->user()->isPremium()
? st::createPollOptionFieldPremium
: st::createPollOptionField,
Ui::InputField::Mode::NoNewlines,
tr::lng_polls_create_option_add())) {
InitField(outer, _field, session);
@ -299,7 +407,7 @@ void Options::Option::createRemove() {
const auto remove = Ui::CreateChild<Ui::CrossButton>(
field.get(),
st::createPollOptionRemove);
remove->hide(anim::type::instant);
remove->show(anim::type::instant);
const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
_removeAlways = lifetime.make_state<rpl::variable<bool>>(false);
@ -309,6 +417,7 @@ void Options::Option::createRemove() {
// Don't capture 'this'! Because Option is a value type.
*toggle = !field->getLastText().isEmpty();
}, field->lifetime());
#if 0
rpl::combine(
toggle->value(),
_removeAlways->value(),
@ -316,6 +425,7 @@ void Options::Option::createRemove() {
) | rpl::start_with_next([=](bool shown) {
remove->toggle(shown, anim::type::normal);
}, remove->lifetime());
#endif
field->widthValue(
) | rpl::start_with_next([=](int width) {
@ -456,10 +566,16 @@ void Options::Option::removePlaceholder() const {
PollAnswer Options::Option::toPollAnswer(int index) const {
Expects(index >= 0 && index < kMaxOptionsCount);
const auto text = field()->getTextWithTags();
auto result = PollAnswer{
field()->getLastText().trimmed(),
QByteArray(1, ('0' + index))
TextWithEntities{
.text = text.text,
.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
},
QByteArray(1, ('0' + index)),
};
TextUtilities::Trim(result.text);
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
return result;
}
@ -469,13 +585,15 @@ rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
}
Options::Options(
not_null<QWidget*> outer,
not_null<Ui::BoxContent*> box,
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
not_null<Window::SessionController*> controller,
ChatHelpers::TabbedPanel *emojiPanel,
bool chooseCorrectEnabled)
: _outer(outer)
: _box(box)
, _container(container)
, _session(session)
, _controller(controller)
, _emojiPanel(emojiPanel)
, _chooseCorrectGroup(chooseCorrectEnabled
? createChooseCorrectGroup()
: nullptr)
@ -645,12 +763,40 @@ void Options::addEmptyOption() {
(*(_list.end() - 2))->toggleRemoveAlways(true);
}
_list.push_back(std::make_unique<Option>(
_outer,
_box,
_container,
_session,
&_controller->session(),
_position + _list.size() + _destroyed.size(),
_chooseCorrectGroup));
const auto field = _list.back()->field();
if (const auto emojiPanel = _emojiPanel) {
const auto emojiToggle = AddEmojiToggleToField(
field,
_box,
_controller,
emojiPanel,
QPoint(
-st::createPollOptionFieldPremium.textMargins.right(),
st::createPollOptionEmojiPositionSkip));
emojiToggle->shownValue() | rpl::start_with_next([=](bool shown) {
if (!shown) {
return;
}
_emojiPanelLifetime.destroy();
emojiPanel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
if (field->hasFocus()) {
Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
}
}, _emojiPanelLifetime);
emojiPanel->selector()->customEmojiChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
if (field->hasFocus()) {
Data::InsertCustomEmoji(field, data.document);
}
}, _emojiPanelLifetime);
}, emojiToggle->lifetime());
}
field->submits(
) | rpl::start_with_next([=] {
const auto index = findField(field);
@ -697,7 +843,7 @@ void Options::addEmptyOption() {
});
_list.back()->removeClicks(
) | rpl::take(1) | rpl::start_with_next([=] {
) | rpl::start_with_next([=] {
Ui::PostponeCall(crl::guard(field, [=] {
Expects(!_list.empty());
@ -789,19 +935,63 @@ not_null<Ui::InputField*> CreatePollBox::setupQuestion(
using namespace Settings;
const auto session = &_controller->session();
const auto isPremium = session->user()->isPremium();
Ui::AddSubsectionTitle(container, tr::lng_polls_create_question());
const auto question = container->add(
object_ptr<Ui::InputField>(
container,
st::createPollField,
Ui::InputField::Mode::MultiLine,
tr::lng_polls_create_question_placeholder()),
st::createPollFieldPadding);
st::createPollFieldPadding
+ (isPremium
? QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0)
: QMargins()));
InitField(getDelegate()->outerContainer(), question, session);
question->setMaxLength(kQuestionLimit + kErrorLimit);
question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
question->customTab(true);
if (isPremium) {
using Selector = ChatHelpers::TabbedSelector;
const auto outer = getDelegate()->outerContainer();
_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
outer,
_controller,
object_ptr<Selector>(
nullptr,
_controller->uiShow(),
Window::GifPauseReason::Layer,
Selector::Mode::EmojiOnly));
const auto emojiPanel = _emojiPanel.get();
emojiPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
emojiPanel->hide();
emojiPanel->selector()->setCurrentPeer(session->user());
const auto emojiToggle = AddEmojiToggleToField(
question,
this,
_controller,
emojiPanel,
st::createPollOptionFieldPremiumEmojiPosition);
emojiPanel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
if (question->hasFocus()) {
Ui::InsertEmojiAtCursor(question->textCursor(), data.emoji);
}
}, emojiToggle->lifetime());
emojiPanel->selector()->customEmojiChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
if (question->hasFocus()) {
Data::InsertCustomEmoji(question, data.document);
}
}, emojiToggle->lifetime());
}
const auto warning = CreateWarningLabel(
container,
question,
@ -910,9 +1100,10 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
st::defaultSubsectionTitle),
st::createPollFieldTitlePadding);
const auto options = lifetime().make_state<Options>(
getDelegate()->outerContainer(),
this,
container,
&_controller->session(),
_controller,
_emojiPanel ? _emojiPanel.get() : nullptr,
(_chosen & PollData::Flag::Quiz));
auto limit = options->usedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count);
@ -1029,9 +1220,13 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
};
const auto collectResult = [=] {
const auto textWithTags = question->getTextWithTags();
using Flag = PollData::Flag;
auto result = PollData(&_controller->session().data(), id);
result.question = question->getLastText().trimmed();
result.question.text = textWithTags.text;
result.question.entities = TextUtilities::ConvertTextTagsToEntities(
textWithTags.tags);
TextUtilities::Trim(result.question);
result.answers = options->toPollAnswers();
const auto solutionWithTags = quiz->checked()
? solution->getTextWithAppliedMarkdown()

View File

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
struct PollData;
namespace ChatHelpers {
class TabbedPanel;
} // namespace ChatHelpers
namespace Ui {
class VerticalLayout;
} // namespace Ui
@ -72,6 +76,7 @@ private:
const PollData::Flags _disabled = PollData::Flags();
const Api::SendType _sendType = Api::SendType();
const SendMenu::Type _sendMenuType;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus;
Fn<rpl::producer<bool>()> _dataIsValidValue;
rpl::event_stream<Result> _submitRequests;

View File

@ -0,0 +1,649 @@
/*
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 "boxes/moderate_messages_box.h"
#include "api/api_chat_participants.h"
#include "apiwrap.h"
#include "base/timer.h"
#include "boxes/delete_messages_box.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "core/ui_integration.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_chat_participant_status.h"
#include "data/data_histories.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/toggle_arrow.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rect_part.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
namespace {
enum class ModerateOption {
Ban = (1 << 0),
DeleteAll = (1 << 1),
};
inline constexpr bool is_flag_type(ModerateOption) { return true; }
using ModerateOptions = base::flags<ModerateOption>;
ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {
Expects(!items.empty());
const auto peer = items.front()->history()->peer;
auto allCanBan = true;
auto allCanDelete = true;
for (const auto &item : items) {
if (!allCanBan && !allCanDelete) {
return ModerateOptions(0);
}
if (peer != item->history()->peer) {
return ModerateOptions(0);
}
if (!item->suggestBanReport()) {
allCanBan = false;
}
if (!item->suggestDeleteAllReport()) {
allCanDelete = false;
}
}
return ModerateOptions(0)
| (allCanBan ? ModerateOption::Ban : ModerateOptions(0))
| (allCanDelete ? ModerateOption::DeleteAll : ModerateOptions(0));
}
class Button final : public Ui::RippleButton {
public:
Button(not_null<QWidget*> parent, int count);
void setChecked(bool checked);
[[nodiscard]] bool checked() const;
[[nodiscard]] static QSize ComputeSize(int);
private:
void paintEvent(QPaintEvent *event) override;
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
const int _count;
const QString _text;
bool _checked = false;
Ui::Animations::Simple _animation;
};
Button::Button(not_null<QWidget*> parent, int count)
: RippleButton(parent, st::defaultRippleAnimation)
, _count(count)
, _text(QString::number(std::abs(_count))) {
}
QSize Button::ComputeSize(int count) {
return QSize(
st::moderateBoxExpandHeight
+ st::moderateBoxExpand.width()
+ st::moderateBoxExpandInnerSkip * 4
+ st::moderateBoxExpandFont->width(
QString::number(std::abs(count)))
+ st::moderateBoxExpandToggleSize,
st::moderateBoxExpandHeight);
}
void Button::setChecked(bool checked) {
if (_checked == checked) {
return;
}
_checked = checked;
_animation.stop();
_animation.start(
[=] { update(); },
checked ? 0 : 1,
checked ? 1 : 0,
st::slideWrapDuration);
}
bool Button::checked() const {
return _checked;
}
void Button::paintEvent(QPaintEvent *event) {
auto p = Painter(this);
auto hq = PainterHighQualityEnabler(p);
Ui::RippleButton::paintRipple(p, QPoint());
const auto radius = height() / 2;
p.setPen(Qt::NoPen);
st::moderateBoxExpand.paint(
p,
radius,
(height() - st::moderateBoxExpand.height()) / 2,
width());
const auto innerSkip = st::moderateBoxExpandInnerSkip;
p.setBrush(Qt::NoBrush);
p.setPen(st::boxTextFg);
p.setFont(st::moderateBoxExpandFont);
p.drawText(
QRect(
innerSkip + radius + st::moderateBoxExpand.width(),
0,
width(),
height()),
_text,
style::al_left);
const auto path = Ui::ToggleUpDownArrowPath(
width() - st::moderateBoxExpandToggleSize - radius,
height() / 2,
st::moderateBoxExpandToggleSize,
st::moderateBoxExpandToggleFourStrokes,
_animation.value(_checked ? 1. : 0.));
p.fillPath(path, st::boxTextFg);
}
QImage Button::prepareRippleMask() const {
return Ui::RippleAnimation::RoundRectMask(size(), size().height() / 2);
}
QPoint Button::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
} // namespace
void CreateModerateMessagesBox(
not_null<Ui::GenericBox*> box,
const HistoryItemsList &items,
Fn<void()> confirmed) {
using Users = std::vector<not_null<UserData*>>;
struct Controller final {
rpl::event_stream<bool> toggleRequestsFromTop;
rpl::event_stream<bool> toggleRequestsFromInner;
rpl::event_stream<bool> checkAllRequests;
Fn<Users()> collectRequests;
};
constexpr auto kSmallDelayMs = 5;
const auto options = CalculateModerateOptions(items);
const auto inner = box->verticalLayout();
const auto users = [&] {
auto result = Users();
for (const auto &item : items) {
if (const auto user = item->from()->asUser()) {
if (!ranges::contains(result, not_null{ user })) {
result.push_back(user);
}
}
}
return result;
}();
Assert(!users.empty());
const auto confirms = inner->lifetime().make_state<rpl::event_stream<>>();
const auto isSingle = users.size() == 1;
const auto buttonPadding = isSingle
? QMargins()
: QMargins(0, 0, Button::ComputeSize(users.size()).width(), 0);
using Request = Fn<void(not_null<UserData*>, not_null<ChannelData*>)>;
const auto sequentiallyRequest = [=](Request request, Users users) {
const auto session = &items.front()->history()->session();
const auto history = items.front()->history();
const auto peerId = history->peer->id;
const auto userIds = ranges::views::all(
users
) | ranges::views::transform([](not_null<UserData*> user) {
return user->id;
}) | ranges::to_vector;
const auto lifetime = std::make_shared<rpl::lifetime>();
const auto counter = lifetime->make_state<int>(0);
const auto timer = lifetime->make_state<base::Timer>();
timer->setCallback(crl::guard(session, [=] {
if ((*counter) < userIds.size()) {
const auto peer = session->data().peer(peerId);
const auto channel = peer ? peer->asChannel() : nullptr;
const auto from = session->data().peer(userIds[*counter]);
if (const auto user = from->asUser(); channel && user) {
request(user, channel);
}
(*counter)++;
} else {
lifetime->destroy();
}
}));
timer->callEach(kSmallDelayMs);
};
const auto handleConfirmation = [=](
not_null<Ui::Checkbox*> checkbox,
not_null<Controller*> controller,
Request request) {
confirms->events() | rpl::start_with_next([=] {
if (checkbox->checked()) {
if (isSingle) {
const auto item = items.front();
const auto channel = item->history()->peer->asChannel();
request(users.front(), channel);
} else if (const auto collect = controller->collectRequests) {
sequentiallyRequest(request, collect());
}
}
}, checkbox->lifetime());
};
const auto createUsersList = [&](not_null<Controller*> controller) {
const auto wrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
wrap->toggle(false, anim::type::instant);
controller->toggleRequestsFromTop.events(
) | rpl::start_with_next([=](bool toggled) {
wrap->toggle(toggled, anim::type::normal);
}, wrap->lifetime());
const auto container = wrap->entity();
Ui::AddSkip(container);
auto &lifetime = wrap->lifetime();
const auto clicks = lifetime.make_state<rpl::event_stream<>>();
const auto checkboxes = ranges::views::all(
users
) | ranges::views::transform([&](not_null<UserData*> user) {
const auto line = container->add(
object_ptr<Ui::AbstractButton>(container));
const auto &st = st::moderateBoxUserpic;
line->resize(line->width(), st.size.height());
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
line,
user,
st);
const auto checkbox = Ui::CreateChild<Ui::Checkbox>(
line,
user->name(),
false,
st::defaultBoxCheckbox);
line->widthValue(
) | rpl::start_with_next([=](int width) {
userpic->moveToLeft(
st::boxRowPadding.left()
+ checkbox->checkRect().width()
+ st::defaultBoxCheckbox.textPosition.x(),
0);
const auto skip = st::defaultBoxCheckbox.textPosition.x();
checkbox->resizeToWidth(width
- rect::right(userpic)
- skip
- st::boxRowPadding.right());
checkbox->moveToLeft(
rect::right(userpic) + skip,
((userpic->height() - checkbox->height()) / 2)
+ st::defaultBoxCheckbox.margin.top());
}, checkbox->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
checkbox->setAttribute(Qt::WA_TransparentForMouseEvents);
line->setClickedCallback([=] {
checkbox->setChecked(!checkbox->checked());
clicks->fire({});
});
return checkbox;
}) | ranges::to_vector;
clicks->events(
) | rpl::start_with_next([=] {
controller->toggleRequestsFromInner.fire_copy(
ranges::any_of(checkboxes, &Ui::Checkbox::checked));
}, container->lifetime());
controller->checkAllRequests.events(
) | rpl::start_with_next([=](bool checked) {
for (const auto &c : checkboxes) {
c->setChecked(checked);
}
}, container->lifetime());
controller->collectRequests = [=] {
auto result = Users();
for (auto i = 0; i < checkboxes.size(); i++) {
if (checkboxes[i]->checked()) {
result.push_back(users[i]);
}
}
return result;
};
};
const auto appendList = [&](
not_null<Ui::Checkbox*> checkbox,
not_null<Controller*> controller) {
const auto button = Ui::CreateChild<Button>(inner, users.size());
button->resize(Button::ComputeSize(users.size()));
const auto overlay = Ui::CreateChild<Ui::AbstractButton>(inner);
checkbox->geometryValue(
) | rpl::start_with_next([=](const QRect &rect) {
overlay->setGeometry(rect);
overlay->raise();
button->moveToRight(
st::moderateBoxExpandRight,
rect.top() + (rect.height() - button->height()) / 2,
box->width());
button->raise();
}, button->lifetime());
controller->toggleRequestsFromInner.events(
) | rpl::start_with_next([=](bool toggled) {
checkbox->setChecked(toggled);
}, checkbox->lifetime());
button->setClickedCallback([=] {
button->setChecked(!button->checked());
controller->toggleRequestsFromTop.fire_copy(button->checked());
});
overlay->setClickedCallback([=] {
checkbox->setChecked(!checkbox->checked());
controller->checkAllRequests.fire_copy(checkbox->checked());
});
createUsersList(controller);
};
Ui::AddSkip(inner);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
(items.size() == 1)
? tr::lng_selected_delete_sure_this()
: tr::lng_selected_delete_sure(
lt_count,
rpl::single(items.size()) | tr::to_count()),
st::boxLabel));
Ui::AddSkip(inner);
Ui::AddSkip(inner);
Ui::AddSkip(inner);
{
const auto report = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::lng_report_spam(tr::now),
false,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
const auto controller = box->lifetime().make_state<Controller>();
if (!isSingle) {
appendList(report, controller);
}
const auto ids = items.front()->from()->owner().itemsToIds(items);
handleConfirmation(report, controller, [=](
not_null<UserData*> u,
not_null<ChannelData*> c) {
auto filtered = QVector<MTPint>();
for (const auto &id : ids) {
if (const auto item = u->session().data().message(id)) {
if (item->from()->asUser() == u) {
filtered.push_back(MTP_int(item->fullId().msg));
}
}
}
u->session().api().request(
MTPchannels_ReportSpam(
c->inputChannel,
u->input,
MTP_vector<MTPint>(std::move(filtered)))
).send();
});
}
if (options & ModerateOption::DeleteAll) {
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto deleteAll = inner->add(
object_ptr<Ui::Checkbox>(
inner,
!(isSingle)
? tr::lng_delete_all_from_users(
tr::now,
Ui::Text::WithEntities)
: tr::lng_delete_all_from_user(
tr::now,
lt_user,
Ui::Text::Bold(items.front()->from()->name()),
Ui::Text::WithEntities),
false,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
const auto controller = box->lifetime().make_state<Controller>();
if (!isSingle) {
appendList(deleteAll, controller);
}
handleConfirmation(deleteAll, controller, [=](
not_null<UserData*> u,
not_null<ChannelData*> c) {
u->session().api().deleteAllFromParticipant(c, u);
});
}
if (options & ModerateOption::Ban) {
auto ownedWrap = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner));
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto ban = inner->add(
object_ptr<Ui::Checkbox>(
box,
rpl::conditional(
ownedWrap->toggledValue(),
tr::lng_context_restrict_user(),
rpl::conditional(
rpl::single(isSingle),
tr::lng_ban_user(),
tr::lng_ban_users())),
false,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
const auto controller = box->lifetime().make_state<Controller>();
if (!isSingle) {
appendList(ban, controller);
}
Ui::AddSkip(inner);
Ui::AddSkip(inner);
const auto wrap = inner->add(std::move(ownedWrap));
const auto container = wrap->entity();
wrap->toggle(false, anim::type::instant);
const auto session = &users.front()->session();
const auto emojiMargin = QMargins(
-st::moderateBoxExpandInnerSkip,
-st::moderateBoxExpandInnerSkip / 2,
0,
0);
const auto emojiUp = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIcon,
emojiMargin,
false));
const auto emojiDown = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::moderateBoxExpandIconDown,
emojiMargin,
false));
auto label = object_ptr<Ui::FlatLabel>(
inner,
QString(),
st::moderateBoxDividerLabel);
const auto raw = label.data();
auto &lifetime = wrap->lifetime();
const auto scrollLifetime = lifetime.make_state<rpl::lifetime>();
label->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return false;
}
wrap->toggle(!wrap->toggled(), anim::type::normal);
{
inner->heightValue() | rpl::start_with_next([=] {
if (!wrap->animating()) {
scrollLifetime->destroy();
Ui::PostponeCall(crl::guard(box, [=] {
box->scrollToY(std::numeric_limits<int>::max());
}));
} else {
box->scrollToY(std::numeric_limits<int>::max());
}
}, *scrollLifetime);
}
return true;
});
wrap->toggledValue(
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
return ((toggled && isSingle)
? tr::lng_restrict_user_part
: (toggled && !isSingle)
? tr::lng_restrict_users_part
: isSingle
? tr::lng_restrict_user_full
: tr::lng_restrict_users_full)(
lt_emoji,
rpl::single(toggled ? emojiUp : emojiDown),
Ui::Text::WithEntities);
}) | rpl::flatten_latest(
) | rpl::start_with_next([=](const TextWithEntities &text) {
raw->setMarkedText(
Ui::Text::Link(text, u"internal:"_q),
Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { raw->update(); },
});
}, label->lifetime());
Ui::AddSkip(inner);
inner->add(object_ptr<Ui::DividerLabel>(
inner,
std::move(label),
st::defaultBoxDividerLabelPadding,
RectPart::Top | RectPart::Bottom));
using Flag = ChatRestriction;
using Flags = ChatRestrictions;
const auto peer = items.front()->history()->peer;
const auto chat = peer->asChat();
const auto channel = peer->asChannel();
const auto defaultRestrictions = chat
? chat->defaultRestrictions()
: channel->defaultRestrictions();
const auto prepareFlags = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
const auto disabledMessages = [&] {
auto result = base::flat_map<Flags, QString>();
{
const auto disabled = FixDependentRestrictions(
defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::ChangeInfo | Flag::PinMessages)
: Flags(0)));
result.emplace(
disabled,
tr::lng_rights_restriction_for_all(tr::now));
}
return result;
}();
auto [checkboxes, getRestrictions, changes] = CreateEditRestrictions(
box,
rpl::conditional(
rpl::single(isSingle),
tr::lng_restrict_users_part_single_header(),
tr::lng_restrict_users_part_header(
lt_count,
rpl::single(users.size()) | tr::to_count())),
prepareFlags,
disabledMessages,
{ .isForum = peer->isForum() });
std::move(changes) | rpl::start_with_next([=] {
ban->setChecked(true);
}, ban->lifetime());
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
container->add(std::move(checkboxes));
handleConfirmation(ban, controller, [=](
not_null<UserData*> user,
not_null<ChannelData*> channel) {
if (wrap->toggled()) {
Api::ChatParticipants::Restrict(
channel,
user,
ChatRestrictionsInfo(), // Unused.
ChatRestrictionsInfo(getRestrictions(), 0),
nullptr,
nullptr);
} else {
channel->session().api().chatParticipants().kick(
channel,
user,
{ channel->restrictions(), 0 });
}
});
}
const auto close = crl::guard(box, [=] { box->closeBox(); });
box->addButton(tr::lng_box_delete(), [=] {
confirms->fire({});
box->closeBox();
const auto data = &users.front()->session().data();
const auto ids = data->itemsToIds(items);
if (confirmed) {
confirmed();
}
data->histories().deleteMessages(ids, true);
data->sendHistoryChangeNotifications();
close();
});
box->addButton(tr::lng_cancel(), close);
}
bool CanCreateModerateMessagesBox(const HistoryItemsList &items) {
const auto options = CalculateModerateOptions(items);
return (options & ModerateOption::Ban)
|| (options & ModerateOption::DeleteAll);
}

View File

@ -0,0 +1,20 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Ui {
class GenericBox;
} // namespace Ui
void CreateModerateMessagesBox(
not_null<Ui::GenericBox*> box,
const HistoryItemsList &items,
Fn<void()> confirmed);
[[nodiscard]] bool CanCreateModerateMessagesBox(const HistoryItemsList &);

View File

@ -166,33 +166,6 @@ void SaveChannelAdmin(
}).send();
}
void SaveChannelRestriction(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail) {
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel,
participant->input,
MTP_chatBannedRights(
MTP_flags(MTPDchatBannedRights::Flags::from_raw(
uint32(newRights.flags))),
MTP_int(newRights.until))
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
channel->applyEditBanned(participant, oldRights, newRights);
if (onDone) {
onDone();
}
}).fail([=] {
if (onFail) {
onFail();
}
}).send();
}
void SaveChatParticipantKick(
not_null<ChatData*> chat,
not_null<UserData*> user,
@ -275,7 +248,7 @@ Fn<void(
ChatRestrictionsInfo newRights) {
const auto done = [=] { if (onDone) onDone(newRights); };
const auto saveForChannel = [=](not_null<ChannelData*> channel) {
SaveChannelRestriction(
Api::ChatParticipants::Restrict(
channel,
participant,
oldRights,

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 = 4016009;
constexpr auto AppVersionStr = "4.16.9";
constexpr auto AppVersion = 4016010;
constexpr auto AppVersionStr = "4.16.10";
constexpr auto AppBetaVersion = true;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@ -1857,23 +1857,21 @@ TextWithEntities MediaPoll::notificationText() const {
}
QString MediaPoll::pinnedTextSubstring() const {
return QChar(171) + _poll->question + QChar(187);
return QChar(171) + _poll->question.text + QChar(187);
}
TextForMimeData MediaPoll::clipboardText() const {
const auto text = u"[ "_q
+ tr::lng_in_dlg_poll(tr::now)
+ u" : "_q
+ _poll->question
+ u" ]"_q
+ ranges::accumulate(
ranges::views::all(
_poll->answers
) | ranges::views::transform([](const PollAnswer &answer) {
return "\n- " + answer.text;
}),
QString());
return TextForMimeData::Simple(text);
auto result = TextWithEntities();
result
.append(u"[ "_q)
.append(tr::lng_in_dlg_poll(tr::now))
.append(u" : "_q)
.append(_poll->question)
.append(u" ]"_q);
for (const auto &answer : _poll->answers) {
result.append(u"\n- "_q).append(answer.text);
}
return TextForMimeData::Rich(std::move(result));
}
bool MediaPoll::updateInlineResultMedia(const MTPMessageMedia &media) {

View File

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "main/main_session.h"
#include "main/main_app_config.h"
#include "main/session/send_as_peers.h"
@ -137,7 +138,14 @@ PossibleItemReactionsRef LookupPossibleReactions(
return {};
}
auto result = PossibleItemReactionsRef();
const auto peer = item->history()->peer;
auto peer = item->history()->peer;
if (item->isDiscussionPost()) {
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
if (forwarded->savedFromPeer) {
peer = forwarded->savedFromPeer;
}
}
}
const auto session = &peer->session();
const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Reactions::Type::Active);

View File

@ -533,7 +533,9 @@ bool PeerData::canPinMessages() const {
bool PeerData::canCreatePolls() const {
if (const auto user = asUser()) {
return user->isBot() && !user->isSupport();
return user->isBot()
&& !user->isSupport()
&& !user->isRepliesChat();
}
return Data::CanSend(this, ChatRestriction::SendPolls);
}

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_poll.h"
#include "api/api_text_entities.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "base/call_delayed.h"
@ -69,7 +70,12 @@ bool PollData::closeByTimer() {
bool PollData::applyChanges(const MTPDpoll &poll) {
Expects(poll.vid().v == id);
const auto newQuestion = qs(poll.vquestion());
const auto newQuestion = TextWithEntities{
.text = qs(poll.vquestion().data().vtext()),
.entities = Api::EntitiesFromMTP(
&session(),
poll.vquestion().data().ventities().v),
};
const auto newFlags = (poll.is_closed() ? Flag::Closed : Flag(0))
| (poll.is_public_voters() ? Flag::PublicVotes : Flag(0))
| (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0))
@ -78,11 +84,16 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
const auto newClosePeriod = poll.vclose_period().value_or_empty();
auto newAnswers = ranges::views::all(
poll.vanswers().v
) | ranges::views::transform([](const MTPPollAnswer &data) {
return data.match([](const MTPDpollAnswer &answer) {
) | ranges::views::transform([&](const MTPPollAnswer &data) {
return data.match([&](const MTPDpollAnswer &answer) {
auto result = PollAnswer();
result.option = answer.voption().v;
result.text = qs(answer.vtext());
result.text = TextWithEntities{
.text = qs(answer.vtext().data().vtext()),
.entities = Api::EntitiesFromMTP(
&session(),
answer.vtext().data().ventities().v),
};
return result;
});
}) | ranges::views::take(
@ -251,9 +262,11 @@ bool PollData::quiz() const {
}
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
const auto convert = [](const PollAnswer &answer) {
const auto convert = [&](const PollAnswer &answer) {
return MTP_pollAnswer(
MTP_string(answer.text),
MTP_textWithEntities(
MTP_string(answer.text.text),
Api::EntitiesToMTP(&poll->session(), answer.text.entities)),
MTP_bytes(answer.option));
};
auto answers = QVector<MTPPollAnswer>();
@ -272,7 +285,9 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
return MTP_poll(
MTP_long(poll->id),
MTP_flags(flags),
MTP_string(poll->question),
MTP_textWithEntities(
MTP_string(poll->question.text),
Api::EntitiesToMTP(&poll->session(), poll->question.entities)),
MTP_vector<MTPPollAnswer>(answers),
MTP_int(poll->closePeriod),
MTP_int(poll->closeDate));

View File

@ -16,7 +16,7 @@ class Session;
} // namespace Main
struct PollAnswer {
QString text;
TextWithEntities text;
QByteArray option;
int votes = 0;
bool chosen = false;
@ -65,7 +65,7 @@ struct PollData {
[[nodiscard]] bool quiz() const;
PollId id = 0;
QString question;
TextWithEntities question;
std::vector<PollAnswer> answers;
std::vector<not_null<PeerData*>> recentVoters;
std::vector<QByteArray> sendingVotes;

View File

@ -3264,13 +3264,7 @@ void Widget::keyPressEvent(QKeyEvent *e) {
} else {
_inner->selectSkipPage(_scroll->height(), -1);
}
} else if (!(e->modifiers() & ~Qt::ShiftModifier)
&& e->key() != Qt::Key_Shift
&& !_openedFolder
&& !_openedForum
&& _search->isVisible()
&& !_search->hasFocus()
&& !e->text().isEmpty()) {
} else if (redirectKeyToSearch(e)) {
// This delay in search focus processing allows us not to create
// _suggestions in case the event inserts some non-whitespace search
// query while still show _suggestions animated, if it is a space.
@ -3284,6 +3278,31 @@ void Widget::keyPressEvent(QKeyEvent *e) {
}
}
bool Widget::redirectKeyToSearch(QKeyEvent *e) const {
if (_openedFolder
|| _openedForum
|| !_search->isVisible()
|| _search->hasFocus()) {
return false;
}
const auto character = !(e->modifiers() & ~Qt::ShiftModifier)
&& (e->key() != Qt::Key_Shift)
&& !e->text().isEmpty();
if (character) {
return true;
} else if (e != QKeySequence::Paste) {
return false;
}
const auto useSelectionMode = (e->key() == Qt::Key_Insert)
&& (e->modifiers() == (Qt::CTRL | Qt::SHIFT))
&& QGuiApplication::clipboard()->supportsSelection();
const auto pasteMode = useSelectionMode
? QClipboard::Selection
: QClipboard::Clipboard;
const auto data = QGuiApplication::clipboard()->mimeData(pasteMode);
return data && data->hasText();
}
void Widget::paintEvent(QPaintEvent *e) {
if (controller()->contentOverlapped(this, e)) {
return;

View File

@ -250,6 +250,8 @@ private:
void updateSuggestions(anim::type animated);
void processSearchFocusChange();
[[nodiscard]] bool redirectKeyToSearch(QKeyEvent *e) const;
MTP::Sender _api;
bool _dragInScroll = false;

View File

@ -668,14 +668,14 @@ Poll ParsePoll(const MTPDmessageMediaPoll &data) {
auto result = Poll();
data.vpoll().match([&](const MTPDpoll &poll) {
result.id = poll.vid().v;
result.question = ParseString(poll.vquestion());
result.question = ParseString(poll.vquestion().data().vtext());
result.closed = poll.is_closed();
result.answers = ranges::views::all(
poll.vanswers().v
) | ranges::views::transform([](const MTPPollAnswer &answer) {
return answer.match([](const MTPDpollAnswer &answer) {
auto result = Poll::Answer();
result.text = ParseString(answer.vtext());
result.text = ParseString(answer.vtext().data().vtext());
result.option = answer.voption().v;
return result;
});

View File

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_helpers.h"
#include "history/view/controls/history_view_forward_panel.h"
#include "history/view/controls/history_view_draft_options.h"
#include "boxes/moderate_messages_box.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/reactions/history_view_reactions_button.h"
@ -4269,8 +4270,13 @@ void HistoryInner::deleteItem(not_null<HistoryItem*> item) {
_controller->cancelUploadLayer(item);
return;
}
const auto suggestModerateActions = true;
_controller->show(Box<DeleteMessagesBox>(item, suggestModerateActions));
const auto list = HistoryItemsList{ item };
if (CanCreateModerateMessagesBox(list)) {
_controller->show(Box(CreateModerateMessagesBox, list, nullptr));
} else {
const auto suggestModerate = false;
_controller->show(Box<DeleteMessagesBox>(item, suggestModerate));
}
}
bool HistoryInner::hasPendingResizedItems() const {

View File

@ -3427,6 +3427,14 @@ void HistoryItem::setupForwardedComponent(const CreateConfig &config) {
forwarded->savedFromMsgId = config.savedFromMsgId;
forwarded->savedFromSender = _history->owner().peerLoaded(
config.savedFromSenderId);
if (forwarded->savedFromPeer
&& !forwarded->savedFromPeer->isFullLoaded()
&& forwarded->savedFromPeer->isChannel()) {
_history->session().api().requestFullPeer(forwarded->savedFromPeer);
} else if (config.savedFromPeer) {
_history->session().api().requestFullPeer(
_history->owner().peer(config.savedFromPeer));
}
forwarded->savedFromOutgoing = config.savedFromOutgoing;
if (!forwarded->savedFromSender
&& !config.savedFromSenderName.isEmpty()) {

View File

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/send_files_box.h"
#include "boxes/share_box.h"
#include "boxes/edit_caption_box.h"
#include "boxes/moderate_messages_box.h"
#include "boxes/premium_limits_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/peers/edit_peer_permissions_box.h" // ShowAboutGigagroup.
@ -7953,15 +7954,23 @@ void HistoryWidget::forwardSelected() {
void HistoryWidget::confirmDeleteSelected() {
if (!_list) return;
auto items = _list->getSelectedItems();
if (items.empty()) {
auto ids = _list->getSelectedItems();
if (ids.empty()) {
return;
}
auto box = Box<DeleteMessagesBox>(&session(), std::move(items));
box->setDeleteConfirmedCallback(crl::guard(this, [=] {
clearSelected();
}));
controller()->show(std::move(box));
const auto items = session().data().idsToItems(ids);
if (CanCreateModerateMessagesBox(items)) {
controller()->show(Box(
CreateModerateMessagesBox,
items,
crl::guard(this, [=] { clearSelected(); })));
} else {
auto box = Box<DeleteMessagesBox>(&session(), std::move(ids));
box->setDeleteConfirmedCallback(crl::guard(this, [=] {
clearSelected();
}));
controller()->show(std::move(box));
}
}
void HistoryWidget::escape() {

View File

@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/fields/input_field.h"
#include "ui/power_saving.h"
#include "boxes/delete_messages_box.h"
#include "boxes/moderate_messages_box.h"
#include "boxes/report_messages_box.h"
#include "boxes/sticker_set_box.h"
#include "boxes/stickers_box.h"
@ -828,9 +829,15 @@ bool AddDeleteMessageAction(
controller->cancelUploadLayer(item);
return;
}
const auto suggestModerateActions = true;
controller->show(
Box<DeleteMessagesBox>(item, suggestModerateActions));
const auto list = HistoryItemsList{ item };
if (CanCreateModerateMessagesBox(list)) {
controller->show(
Box(CreateModerateMessagesBox, list, nullptr));
} else {
const auto suggestModerateActions = false;
controller->show(
Box<DeleteMessagesBox>(item, suggestModerateActions));
}
}
});
if (item->isUploading()) {
@ -1300,15 +1307,15 @@ void AddPollActions(
const auto radio = QString::fromUtf8(kRadio);
auto text = poll->question;
for (const auto &answer : poll->answers) {
text += '\n' + radio + answer.text;
text.append('\n').append(radio).append(answer.text);
}
if (!Ui::SkipTranslate({ text })) {
if (!Ui::SkipTranslate(text)) {
menu->addAction(tr::lng_context_translate(tr::now), [=] {
controller->show(Box(
Ui::TranslateBox,
item->history()->peer,
MsgId(),
TextWithEntities{ .text = text },
std::move(text),
item->forbidsForward()));
}, &st::menuIconTranslate);
}

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/media/history_view_poll.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "lang/lang_keys.h"
#include "history/history.h"
#include "history/history_item.h"
@ -154,7 +155,10 @@ struct Poll::SendingAnimation {
struct Poll::Answer {
Answer();
void fillData(not_null<PollData*> poll, const PollAnswer &original);
void fillData(
not_null<PollData*> poll,
const PollAnswer &original,
Core::MarkedTextContext context);
Ui::Text::String text;
QByteArray option;
@ -201,16 +205,18 @@ Poll::Answer::Answer() : text(st::msgMinWidth / 2) {
void Poll::Answer::fillData(
not_null<PollData*> poll,
const PollAnswer &original) {
const PollAnswer &original,
Core::MarkedTextContext context) {
chosen = original.chosen;
correct = poll->quiz() ? original.correct : chosen;
if (!text.isEmpty() && text.toString() == original.text) {
if (!text.isEmpty() && text.toTextWithEntities() == original.text) {
return;
}
text.setText(
text.setMarkedText(
st::historyPollAnswerStyle,
original.text,
Ui::WebpageTextTitleOptions());
Ui::WebpageTextTitleOptions(),
context);
}
Poll::CloseInformation::CloseInformation(
@ -383,13 +389,18 @@ void Poll::updateTexts() {
const auto willStartAnimation = checkAnimationStart();
const auto voted = _voted;
if (_question.toString() != _poll->question) {
if (_question.toTextWithEntities() != _poll->question) {
auto options = Ui::WebpageTextTitleOptions();
options.maxw = options.maxh = 0;
_question.setText(
_question.setMarkedText(
st::historyPollQuestionStyle,
_poll->question,
options);
options,
Core::MarkedTextContext{
.session = &_poll->session(),
.customEmojiRepaint = [=] { repaint(); },
.customEmojiLoopLimit = 2,
});
}
if (_flags != _poll->flags() || _subtitle.isEmpty()) {
using Flag = PollData::Flag;
@ -514,6 +525,11 @@ void Poll::updateRecentVoters() {
}
void Poll::updateAnswers() {
const auto context = Core::MarkedTextContext{
.session = &_poll->session(),
.customEmojiRepaint = [=] { repaint(); },
.customEmojiLoopLimit = 2,
};
const auto changed = !ranges::equal(
_answers,
_poll->answers,
@ -523,7 +539,7 @@ void Poll::updateAnswers() {
if (!changed) {
auto &&answers = ranges::views::zip(_answers, _poll->answers);
for (auto &&[answer, original] : answers) {
answer.fillData(_poll, original);
answer.fillData(_poll, original, context);
}
return;
}
@ -532,7 +548,7 @@ void Poll::updateAnswers() {
) | ranges::views::transform([&](const PollAnswer &answer) {
auto result = Answer();
result.option = answer.option;
result.fillData(_poll, answer);
result.fillData(_poll, answer, context);
return result;
}) | ranges::to_vector;

View File

@ -8,17 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/polls/info_polls_results_inner_widget.h"
#include "info/polls/info_polls_results_widget.h"
#include "info/info_controller.h"
#include "lang/lang_keys.h"
#include "data/data_poll.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "ui/controls/peer_list_dummy.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/text/text_utilities.h"
#include "boxes/peer_list_box.h"
@ -26,7 +22,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
namespace Info {
@ -461,10 +456,11 @@ ListController *CreateAnswerRows(
container.get(),
object_ptr<Ui::FlatLabel>(
container,
(answer.text
+ QString::fromUtf8(" \xe2\x80\x94 ")
+ QString::number(percent)
+ "%"),
rpl::single(
TextWithEntities(answer.text)
.append(QString::fromUtf8(" \xe2\x80\x94 "))
.append(QString::number(percent))
.append('%')),
st::boxDividerLabel),
style::margins(
st::pollResultsHeaderPadding.left(),
@ -613,7 +609,7 @@ void InnerWidget::setupContent() {
_content->add(
object_ptr<Ui::FlatLabel>(
_content,
_poll->question,
rpl::single(_poll->question),
st::pollResultsQuestion),
style::margins{
st::boxRowPadding.left(),

View File

@ -369,6 +369,10 @@ void Step::fillSentCodeData(const MTPDauth_sentCode &data) {
bad("FirebaseSms");
}, [&](const MTPDauth_sentCodeTypeEmailCode &) {
bad("EmailCode");
}, [&](const MTPDauth_sentCodeTypeSmsWord &) {
bad("SmsWord");
}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {
bad("SmsPhrase");
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
bad("SetUpEmailRequired");
});

View File

@ -155,10 +155,6 @@ namespace {
+ "IV.init();"
+ page.script;
const auto contentAttributes = page.rtl
? " dir=\"rtl\" class=\"rtl\""_q
: QByteArray();
return R"(<!DOCTYPE html>
<html)"_q
+ classAttribute
@ -179,9 +175,7 @@ namespace {
<path d="M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6" transform="translate(11.997236, 12.000000) scale(-1, -1) rotate(-90.000000) translate(-11.997236, -12.000000) "></path>
</svg>
</button>
<div class="page-scroll" tabindex="-1"><div class="page-slide">
<article)"_q + contentAttributes + ">"_q + page.content + R"(</article>
</div></div>
<div class="page-scroll" tabindex="-1">)"_q + page.content.trimmed() + R"(</div>
<script>)"_q + js + R"(</script>
</body>
</html>
@ -646,7 +640,12 @@ void Controller::processLink(const QString &url, const QString &context) {
const auto joinPrefix = u"join_link"_q;
const auto webpagePrefix = u"webpage"_q;
const auto viewerPrefix = u"viewer"_q;
if (context.startsWith(channelPrefix)) {
if (context == u"report-iv") {
_events.fire({
.type = Event::Type::Report,
.context = QString::number(compuseCurrentPageId()),
});
} else if (context.startsWith(channelPrefix)) {
_events.fire({
.type = Event::Type::OpenChannel,
.context = context.mid(channelPrefix.size()),
@ -701,6 +700,13 @@ QString Controller::composeCurrentUrl() const {
+ (_hash.isEmpty() ? u""_q : ('#' + _hash));
}
uint64 Controller::compuseCurrentPageId() const {
const auto index = _index.current();
Assert(index >= 0 && index < _pages.size());
return _pages[index].pageId;
}
void Controller::showMenu() {
const auto index = _index.current();
if (_menu || index < 0 || index > _pages.size()) {

View File

@ -63,6 +63,7 @@ public:
OpenLink,
OpenLinkExternal,
OpenMedia,
Report,
};
Type type = Type::Close;
QString url;
@ -116,6 +117,7 @@ private:
void quit();
[[nodiscard]] QString composeCurrentUrl() const;
[[nodiscard]] uint64 compuseCurrentPageId() const;
void showShareMenu();
void destroyShareMenu();

View File

@ -15,6 +15,7 @@ struct Options {
};
struct Prepared {
uint64 pageId = 0;
QString name;
QByteArray content;
QByteArray script;

View File

@ -742,9 +742,7 @@ void Instance::show(
not_null<Data*> data,
QString hash) {
const auto guard = gsl::finally([&] {
if (data->partial()) {
requestFull(session, data->id());
}
requestFull(session, data->id());
});
if (_shown && _shownSession == session) {
_shown->moveTo(data, hash);
@ -815,6 +813,7 @@ void Instance::show(
if (!urlChecked) {
break;
}
_fullRequested[_shownSession].emplace(event.url);
_shownSession->api().request(MTPmessages_GetWebPage(
MTP_string(event.url),
MTP_int(0)
@ -834,6 +833,17 @@ void Instance::show(
UrlClickHandler::Open(event.url);
}).send();
break;
case Type::Report:
if (const auto controller = _shownSession->tryResolveWindow()) {
controller->window().activate();
controller->showPeerByLink(Window::PeerByLinkInfo{
.usernameOrId = "previews",
.resolveType = Window::ResolveType::BotStart,
.startToken = ("webpage"
+ QString::number(event.context.toULongLong())),
});
}
break;
}
}, _shown->lifetime());
@ -938,6 +948,7 @@ void Instance::openWithIvPreferred(
};
_ivRequestSession = session;
_ivRequestUri = uri;
_fullRequested[session].emplace(url);
_ivRequestId = session->api().request(MTPmessages_GetWebPage(
MTP_string(url),
MTP_int(0)

View File

@ -142,6 +142,8 @@ private:
[[nodiscard]] QByteArray block(
const MTPDpageListOrderedItemBlocks &data);
[[nodiscard]] QByteArray wrap(const QByteArray &content, int views);
[[nodiscard]] QByteArray tag(
const QByteArray &name,
const QByteArray &body = {});
@ -223,9 +225,13 @@ Parser::Parser(const Source &source, const Options &options)
: /*_options(options)
, */_fileOriginPostfix('/' + Number(source.pageId)) {
process(source);
_result.pageId = source.pageId;
_result.name = source.name;
_result.rtl = source.page.data().is_rtl();
_result.content = list(source.page.data().vblocks());
const auto views = source.page.data().vviews().value_or_empty();
const auto content = list(source.page.data().vblocks());
_result.content = wrap(content, views);
}
Prepared Parser::result() {
@ -514,6 +520,9 @@ QByteArray Parser::block(
}, result);
if (!slideshow) {
result += caption(data.vcaption());
if (!collage) {
result = tag("div", { { "class", "media-outer" } }, result);
}
}
return result;
}
@ -579,6 +588,9 @@ QByteArray Parser::block(
}
if (!slideshow) {
result += caption(data.vcaption());
if (!collage) {
result = tag("div", { { "class", "media-outer" } }, result);
}
}
return result;
}
@ -925,6 +937,26 @@ QByteArray Parser::utf(const tl::conditional<MTPstring> &text) {
return text ? utf(*text) : QByteArray();
}
QByteArray Parser::wrap(const QByteArray &content, int views) {
const auto sep = " \xE2\x80\xA2 ";
const auto viewsText = views
? (tr::lng_stories_views(tr::now, lt_count_decimal, views) + sep)
: QString();
return R"(
<div class="page-slide">
<article>)"_q + content + R"(</article>
</div>
<div class="page-footer">
<div class="content">
)"_q
+ viewsText.toUtf8()
+ R"(<a class="wrong" data-context="report-iv">)"_q
+ tr::lng_iv_wrong_layout(tr::now).toUtf8()
+ R"(</a>
</div>
</div>)"_q;
}
QByteArray Parser::tag(
const QByteArray &name,
const QByteArray &body) {

View File

@ -5190,27 +5190,43 @@ void OverlayWidget::handleKeyPress(not_null<QKeyEvent*> e) {
void OverlayWidget::handleWheelEvent(not_null<QWheelEvent*> e) {
constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);
const auto _thisWheelDelta = e->angleDelta().y();
const auto acceptForJump = !_stories
&& ((e->source() == Qt::MouseEventNotSynthesized)
|| (e->source() == Qt::MouseEventSynthesizedBySystem));
_verticalWheelDelta += e->angleDelta().y();
while (qAbs(_verticalWheelDelta) >= step) {
if (_verticalWheelDelta < 0) {
_verticalWheelDelta += step;
const bool directionChanges =
std::signbit(_lastWheelDelta) != std::signbit(_thisWheelDelta);
if ((qAbs(_thisWheelDelta) != step) && directionChanges) {
// linux: first scroll after direction changes on hi-res wheel in
// libinput is unreliable. offen lost first half of it's value,
// or even only remain one with 15 delta, so we just hardcode it
// to same as step here, other system should be fine too.
_absWheelDelta = _thisWheelDelta > 0 ? step : step * -1;
} else {
_absWheelDelta += _thisWheelDelta;
}
while (qAbs(_absWheelDelta) >= step) {
if (_absWheelDelta < 0) {
// _absWheelDelta += step;
if (e->modifiers().testFlag(Qt::ControlModifier)) {
zoomOut();
} else if (acceptForJump) {
moveToNext(1);
}
} else {
_verticalWheelDelta -= step;
// _absWheelDelta -= step;
if (e->modifiers().testFlag(Qt::ControlModifier)) {
zoomIn();
} else if (acceptForJump) {
moveToNext(-1);
}
}
_absWheelDelta = 0; // reset to 0 to reduce pic move jump or skip.
}
_lastWheelDelta = _thisWheelDelta; // record last wheel delta
}
void OverlayWidget::setZoomLevel(int newZoom, bool force) {

View File

@ -725,7 +725,8 @@ private:
rpl::event_stream<TouchBarItemType> _touchbarDisplay;
rpl::event_stream<bool> _touchbarFullscreenToggled;
int _verticalWheelDelta = 0;
int _absWheelDelta = 0;
int _lastWheelDelta = 0;
bool _themePreviewShown = false;
uint64 _themePreviewId = 0;

View File

@ -758,6 +758,8 @@ auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true go
auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType;
auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType;
auth.sentCodeTypeFirebaseSms#e57b1432 flags:# nonce:flags.0?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType;
auth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType;
auth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType;
messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer;
@ -1149,9 +1151,9 @@ help.supportName#8c05f1c9 name:string = help.SupportName;
help.userInfoEmpty#f3ae2eed = help.UserInfo;
help.userInfo#1eb3758 message:string entities:Vector<MessageEntity> author:string date:int = help.UserInfo;
pollAnswer#6ca9c2e9 text:string option:bytes = PollAnswer;
pollAnswer#ff16e2ca text:TextWithEntities option:bytes = PollAnswer;
poll#86e18161 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector<PollAnswer> close_period:flags.4?int close_date:flags.5?int = Poll;
poll#58747131 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:TextWithEntities answers:Vector<PollAnswer> close_period:flags.4?int close_date:flags.5?int = Poll;
pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters;
@ -1808,6 +1810,7 @@ auth.checkRecoveryPassword#d36bf79 code:string = Bool;
auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization;
auth.requestFirebaseSms#89464b50 flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string ios_push_secret:flags.1?string = Bool;
auth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode;
auth.reportMissingCode#cb9deff6 phone_number:string phone_code_hash:string mnc:string = Bool;
account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector<long> = Bool;
account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector<long> = Bool;
@ -2424,4 +2427,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool;
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;
// LAYER 178
// LAYER 179

View File

@ -2217,6 +2217,10 @@ void FormController::startPhoneVerification(not_null<Value*> value) {
bad("FirebaseSms");
}, [&](const MTPDauth_sentCodeTypeEmailCode &) {
bad("EmailCode");
}, [&](const MTPDauth_sentCodeTypeSmsWord &) {
bad("SmsWord");
}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {
bad("SmsPhrase");
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
bad("SetUpEmailRequired");
});

View File

@ -994,7 +994,7 @@ TextWithEntities Manager::ComposeReactionNotification(
lt_reaction,
reactionWithEntities,
lt_title,
Ui::Text::WithEntities(poll->question),
poll->question,
Ui::Text::WithEntities);
} else if (media->game()) {
return simple(tr::lng_reaction_game);

View File

@ -1085,6 +1085,34 @@ void Filler::addViewStatistics() {
}
void Filler::addCreatePoll() {
const auto isJoinChannel = [&] {
if (_request.section != Section::Replies) {
if (const auto c = _peer->asChannel(); c && !c->amIn()) {
return true;
}
}
return false;
}();
const auto isBotStart = [&] {
const auto user = _peer ? _peer->asUser() : nullptr;
if (!user || !user->isBot()) {
return false;
} else if (!user->botInfo->startToken.isEmpty()) {
return true;
}
const auto history = _peer->owner().history(_peer);
if (history && history->isEmpty() && !history->lastMessage()) {
return true;
}
return false;
}();
const auto isBlocked = [&] {
return _peer && _peer->isUser() && _peer->asUser()->isBlocked();
}();
if (isBlocked || isJoinChannel || isBotStart) {
return;
}
const auto can = _topic
? Data::CanSend(_topic, ChatRestriction::SendPolls)
: _peer->canCreatePolls();

View File

@ -1,7 +1,7 @@
AppVersion 4016009
AppVersion 4016010
AppVersionStrMajor 4.16
AppVersionStrSmall 4.16.9
AppVersionStr 4.16.9
AppVersionStrSmall 4.16.10
AppVersionStr 4.16.10
BetaChannel 1
AlphaVersion 0
AppVersionOriginal 4.16.9.beta
AppVersionOriginal 4.16.10.beta

@ -1 +1 @@
Subproject commit 69e474ea775f115afb3e4afeb80d3227325dfcc4
Subproject commit cb57bef3f01b7ec60eb0eae0ee68cd56cb3a9b1f

@ -1 +1 @@
Subproject commit 9f9bcaaec922644406faadda4d37014c9dec2dd9
Subproject commit 2ccbfa5f3443274e40deb761674b536e2e8eedae

View File

@ -1,3 +1,10 @@
4.16.10 beta (26.04.24)
- Group admins can mass-moderate many messages.
- Premium users can use animated emoji in polls.
- Revert the default "Open Sans" font to 1.10.
- Several crash fixes and small improvements.
4.16.9 beta (23.04.24)
- Show "Frequent contacts" when you focus the search field.

View File

@ -2,7 +2,7 @@ name: telegram-desktop
adopt-info: telegram
icon: Telegram/Resources/art/icon512@2x.png
base: core22
base: core24
grade: stable
confinement: strict
compression: lzo
@ -70,12 +70,14 @@ layout:
bind: $SNAP/usr/share/X11
/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gtk-3.0:
bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gtk-3.0
/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gtk-4.0:
bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/gtk-4.0
/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/pipewire-0.3:
bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/pipewire-0.3
/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/spa-0.2:
bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/spa-0.2
/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkit2gtk-4.1:
bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkit2gtk-4.1
/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkitgtk-6.0:
bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkitgtk-6.0
package-repositories:
- type: apt
@ -94,14 +96,17 @@ parts:
- clang
- libtool-bin
- python3
- protobuf-compiler
- libasound2-dev
- libavif-dev
- libboost-regex1.74-dev
- libboost-regex-dev
- libfmt-dev
- libgirepository1.0-dev
- libglib2.0-dev
- libheif-dev
- libopenal-dev
- libopus-dev
- libprotobuf-dev
- libpulse-dev
- libssl-dev
- libwayland-dev
@ -112,15 +117,17 @@ parts:
- zlib1g-dev
stage-packages:
- libasound2
- libavif13
- libboost-regex1.74.0
- libavif16
- libboost-regex1.83.0
- libglib2.0-0
- libheif1
- libopenal1
- libopus0
- libprotobuf-lite32
- libpulse0
- libssl3
- libwayland-client0
- libwebkit2gtk-4.1-0
- libwebkitgtk-6.0-4
- libxcb1
- libxcb-keysyms1
- libxcb-record0
@ -157,8 +164,6 @@ parts:
after:
- ffmpeg
- libjxl
- openal
- protobuf
- qt
- rnnoise
- webrtc
@ -220,12 +225,12 @@ parts:
- libswresample-dev
- libswscale-dev
stage-packages:
- libavcodec58
- libavfilter7
- libavformat58
- libavutil56
- libswresample3
- libswscale5
- libavcodec60
- libavfilter9
- libavformat60
- libavutil58
- libswresample4
- libswscale7
- va-driver-all
- vdpau-driver-all
override-build: |
@ -273,60 +278,6 @@ parts:
after:
- patches
openal:
source: https://github.com/kcat/openal-soft.git
source-depth: 1
source-tag: 1.23.1
plugin: cmake
build-environment:
- LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s
build-packages:
- libasound2-dev
- libdbus-1-dev
- libpipewire-0.3-dev
- libpulse-dev
stage-packages:
- libasound2
- libdbus-1-3
- libpulse0
- pipewire
cmake-generator: Ninja
cmake-parameters:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INSTALL_PREFIX=/usr
- -DALSOFT_EXAMPLES=OFF
- -DALSOFT_UTILS=OFF
- -DALSOFT_INSTALL_CONFIG=OFF
prime:
- -./usr/include
- -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/cmake
- -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/pkgconfig
- -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/*.so
protobuf:
source: https://github.com/protocolbuffers/protobuf.git
source-depth: 1
source-tag: v24.3
plugin: cmake
build-environment:
- LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s
build-packages:
- zlib1g-dev
stage-packages:
- zlib1g
cmake-generator: Ninja
cmake-parameters:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INSTALL_PREFIX=/usr
- -DBUILD_SHARED_LIBS=ON
- -Dprotobuf_BUILD_TESTS=OFF
prime:
- -./usr/bin
- -./usr/include
- -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/cmake
- -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/pkgconfig
- -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/*.so
qt:
plugin: nil
build-environment:
@ -380,7 +331,7 @@ parts:
- libgtk-3-0
- libharfbuzz0b
- libice6
- libicu70
- libicu74
- liblcms2-2
- libopengl0
- libpcre2-16-0
@ -413,6 +364,7 @@ parts:
- libxkbcommon-x11-0
- zlib1g
- mesa-vulkan-drivers
- xkb-data
override-pull: |
QT=6.7.0
@ -518,10 +470,10 @@ parts:
- libgbm1
- libgl1
- libglib2.0-0
- libopenh264-6
- libopenh264-7
- libopus0
- libssl3
- libvpx7
- libvpx8
- libx11-6
- libxcomposite1
- libxdamage1