Compare commits
20 Commits
7236ecc034
...
6a2896e730
Author | SHA1 | Date |
---|---|---|
detiam | 6a2896e730 | |
John Preston | c0db5ee98a | |
John Preston | 372b3da09c | |
John Preston | 79532954dc | |
23rd | aff2be605e | |
John Preston | 363c191a6e | |
John Preston | 2949cdab61 | |
John Preston | 7addcf2d25 | |
John Preston | a272807a99 | |
23rd | c803603de4 | |
23rd | e6c22ec1ca | |
John Preston | b3ae843f0e | |
John Preston | 12a78c1f45 | |
23rd | 612b81ee87 | |
23rd | e5b91d2f3d | |
23rd | 82293c98eb | |
23rd | 629da68cfc | |
23rd | 643ecd2c2c | |
Ilya Fedin | dd1cb00c62 | |
detiam | 9409fb344f |
|
@ -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
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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}.";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 &);
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -250,6 +250,8 @@ private:
|
|||
void updateSuggestions(anim::type animated);
|
||||
void processSearchFocusChange();
|
||||
|
||||
[[nodiscard]] bool redirectKeyToSearch(QKeyEvent *e) const;
|
||||
|
||||
MTP::Sender _api;
|
||||
|
||||
bool _dragInScroll = false;
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ struct Options {
|
|||
};
|
||||
|
||||
struct Prepared {
|
||||
uint64 pageId = 0;
|
||||
QString name;
|
||||
QByteArray content;
|
||||
QByteArray script;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue