Compare commits

..

53 Commits

Author SHA1 Message Date
John Preston
672aacd528 wip linux webview 2021-03-31 22:20:30 +04:00
John Preston
1a5e807fa9 Fix build for macOS / Linux. 2021-03-31 21:38:32 +04:00
John Preston
f98f4f0d14 Simple receipt viewing. 2021-03-30 10:03:54 +04:00
John Preston
78def16ced Fix showing comments from the beginning. 2021-03-29 19:53:55 +04:00
John Preston
cd7b3419de Add phone format and validation in payments. 2021-03-29 18:56:26 +04:00
John Preston
dc4048f1c1 Add local validation for card information. 2021-03-29 16:16:54 +04:00
John Preston
bef5320163 Add nice country choosing in payments. 2021-03-26 21:09:09 +04:00
John Preston
9a722ea8d4 Improve checkout information / card page design. 2021-03-26 19:23:12 +04:00
John Preston
1aefada45d Improve checkout main page design. 2021-03-26 17:05:31 +04:00
John Preston
f5a8bf0e74 Handle native / non-native payment methods (same way). 2021-03-25 23:22:45 +04:00
John Preston
738439c334 Support entering card details natively. 2021-03-25 20:56:20 +04:00
John Preston
8949c9969b Fix jumping of Media Viewer in some DEs. 2021-03-25 19:10:01 +04:00
John Preston
cdf6fb1512 Port required parts of Stripe SDK to lib_stripe. 2021-03-24 22:00:49 +04:00
John Preston
fb0ea59ff3 Validate saved information on payment form open. 2021-03-24 16:41:46 +04:00
John Preston
36f5be60f4 Show some payment errors, focus fields. 2021-03-24 15:30:01 +04:00
John Preston
46508f7e5e First full-featured version of payments, no design. 2021-03-23 22:30:00 +04:00
John Preston
28137dfb60 Start proper payments processing. 2021-03-23 16:34:34 +04:00
John Preston
e7784620d3 Use navigation cancel in Webview. 2021-03-22 22:56:29 +04:00
John Preston
462986e9c3 Update lib_webview. 2021-03-22 22:56:29 +04:00
John Preston
c11de2380e Start Linux support. 2021-03-22 22:56:27 +04:00
John Preston
a432e826a6 Use lib_webview implementation on macOS. 2021-03-22 22:54:54 +04:00
John Preston
ea9e85e70f Use lib_webview implementation on Windows. 2021-03-22 22:54:54 +04:00
John Preston
d3829c52ec Fix webview on macOS. 2021-03-22 22:54:54 +04:00
John Preston
9f04570335 3DSecure in Proof-Of-Concept payments. 2021-03-22 22:54:53 +04:00
John Preston
3c486522a7 Proof-Of-Concept simplest invoice payment. 2021-03-22 22:54:53 +04:00
John Preston
07cd8c4e83 Link Telegram with lib_webview. 2021-03-22 22:54:53 +04:00
John Preston
7447c6ea75 Add webview / lib_webview submodules. 2021-03-22 22:54:53 +04:00
John Preston
4d579f873c Remove tested 110% scale. 2021-03-22 19:52:51 +04:00
John Preston
56c8327746 Fix emoji picker button in boxes in non-default scale. 2021-03-22 19:41:13 +04:00
John Preston
0e6d4291a2 Fix close window button paint in theme preview. 2021-03-22 19:40:12 +04:00
John Preston
8ca622d077 Remove 100% from voice chat 'speaking' status. 2021-03-22 18:55:07 +04:00
John Preston
4d24f28fd0 By voicechat link open the channel as well. 2021-03-22 17:24:53 +04:00
John Preston
2b3469ef22 Remove CAPS in calls / voice chats top bar. 2021-03-22 16:44:00 +04:00
John Preston
03a868a6e3 Allow skipping stuck files in data export.
Fixes #6423.
2021-03-22 16:32:40 +04:00
Ilya Fedin
12db51fe75 Avoid removing portal platformtheme plugin in snap 2021-03-22 07:57:18 +03:00
Ilya Fedin
ce5579e8f9 Unify gtk/xdp file dialog getters 2021-03-22 07:57:18 +03:00
Ilya Fedin
a16b7fbb83 Fix path to executable in ComputeExternalUpdater 2021-03-21 09:04:43 +03:00
Ilya Fedin
9f6f7f7c9b Fix build without dbus 2021-03-21 08:59:19 +03:00
John Preston
a82d1e863e Version 2.7.1: Fix channels ban in admin log. 2021-03-20 18:19:31 +04:00
John Preston
26d97a3636 Version 2.7.1.
- Fix editing 'Manage Voice Chats' rights for channel admins.
- Fix verification check display in voice chat participants list.
- Allow removing and blocking channels from voice chats.
2021-03-20 16:23:41 +04:00
John Preston
7b8e421996 Allow markup in some voice chat toasts. 2021-03-20 16:23:41 +04:00
John Preston
2bc2a0e459 Fix possible integer overflow. 2021-03-20 16:03:58 +04:00
John Preston
7cb4b4f8ab Don't try to use WebRTC built-in audio backends. 2021-03-20 15:43:35 +04:00
John Preston
b439ecce16 Allow all toasts to be multiline in voice chats. 2021-03-20 15:43:35 +04:00
John Preston
a33a4c0589 Fix maximize/restore voice chat title bar button. 2021-03-20 15:43:35 +04:00
John Preston
5278e2201f Make red 'Remove' in voice chat participant menu. 2021-03-20 15:43:35 +04:00
John Preston
3bd6b2268f Allow blocking channels in voice chats. 2021-03-20 15:43:35 +04:00
John Preston
a0a13c3b86 Update API scheme to layer 126. 2021-03-20 15:43:35 +04:00
John Preston
0052c7938f Fix verified icon in voice chat participants list. 2021-03-20 15:43:35 +04:00
John Preston
a14db3e492 Allow editing 'Manage Voice Chats' admin right in channels. 2021-03-20 15:43:35 +04:00
Ilya Fedin
7979b3b6c8 Fix devtoolset version in linux action 2021-03-20 14:33:02 +03:00
Ilya Fedin
3f25e92afd Add build options for libtgvoip backends
libtgvoip has options to disbale some backends, but they never were exposed via tdesktop's cmake

Since libtgvoip autoconf build system doesn't work anymore, it's worth to have these options in tdesktop's cmake.
2021-03-20 14:33:02 +03:00
Ilya Fedin
3d1cddaca5 Add a way to change default handler in snap 2021-03-20 14:20:09 +03:00
169 changed files with 7645 additions and 1497 deletions

View File

@@ -55,7 +55,7 @@ jobs:
defaults:
run:
shell: scl enable devtoolset-8 -- bash --noprofile --norc -eo pipefail {0}
shell: scl enable devtoolset-9 -- bash --noprofile --norc -eo pipefail {0}
strategy:
matrix:
@@ -65,6 +65,8 @@ jobs:
- "DESKTOP_APP_DISABLE_X11_INTEGRATION"
- "DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION"
- "DESKTOP_APP_DISABLE_GTK_INTEGRATION"
- "LIBTGVOIP_DISABLE_ALSA"
- "LIBTGVOIP_DISABLE_PULSEAUDIO"
env:
UPLOAD_ARTIFACT: "false"

3
.gitmodules vendored
View File

@@ -88,3 +88,6 @@
[submodule "Telegram/ThirdParty/tgcalls"]
path = Telegram/ThirdParty/tgcalls
url = https://github.com/TelegramMessenger/tgcalls.git
[submodule "Telegram/lib_webview"]
path = Telegram/lib_webview
url = https://github.com/desktop-app/lib_webview.git

View File

@@ -19,6 +19,7 @@ add_subdirectory(lib_storage)
add_subdirectory(lib_lottie)
add_subdirectory(lib_qr)
add_subdirectory(lib_webrtc)
add_subdirectory(lib_webview)
add_subdirectory(codegen)
get_filename_component(src_loc SourceFiles REALPATH)
@@ -26,6 +27,7 @@ get_filename_component(res_loc Resources REALPATH)
include(cmake/telegram_options.cmake)
include(cmake/lib_ffmpeg.cmake)
include(cmake/lib_stripe.cmake)
include(cmake/lib_tgvoip.cmake)
include(cmake/lib_tgcalls.cmake)
include(cmake/td_export.cmake)
@@ -55,7 +57,9 @@ PRIVATE
desktop-app::lib_storage
desktop-app::lib_lottie
desktop-app::lib_qr
desktop-app::lib_webview
desktop-app::lib_ffmpeg
desktop-app::lib_stripe
desktop-app::external_lz4
desktop-app::external_rlottie
desktop-app::external_zlib
@@ -70,7 +74,12 @@ PRIVATE
desktop-app::external_xxhash
)
if (LINUX)
if (WIN32)
target_link_libraries(Telegram
PRIVATE
desktop-app::lib_webview_winrt
)
elseif (LINUX)
target_link_libraries(Telegram
PRIVATE
desktop-app::external_glibmm
@@ -249,8 +258,6 @@ PRIVATE
boxes/sessions_box.h
boxes/share_box.cpp
boxes/share_box.h
boxes/single_choice_box.cpp
boxes/single_choice_box.h
boxes/sticker_set_box.cpp
boxes/sticker_set_box.h
boxes/stickers_box.cpp
@@ -383,8 +390,6 @@ PRIVATE
data/data_cloud_file.h
data/data_cloud_themes.cpp
data/data_cloud_themes.h
data/data_countries.cpp
data/data_countries.h
data/data_document.cpp
data/data_document.h
data/data_document_media.cpp
@@ -796,8 +801,6 @@ PRIVATE
passport/passport_panel.h
passport/passport_panel_controller.cpp
passport/passport_panel_controller.h
passport/passport_panel_details_row.cpp
passport/passport_panel_details_row.h
passport/passport_panel_edit_contact.cpp
passport/passport_panel_edit_contact.h
passport/passport_panel_edit_document.cpp
@@ -808,6 +811,10 @@ PRIVATE
passport/passport_panel_form.h
passport/passport_panel_password.cpp
passport/passport_panel_password.h
payments/payments_checkout_process.cpp
payments/payments_checkout_process.h
payments/payments_form.cpp
payments/payments_form.h
platform/linux/linux_desktop_environment.cpp
platform/linux/linux_desktop_environment.h
platform/linux/linux_gdk_helper.cpp
@@ -1007,8 +1014,6 @@ PRIVATE
ui/widgets/level_meter.h
ui/widgets/multi_select.cpp
ui/widgets/multi_select.h
ui/widgets/separate_panel.cpp
ui/widgets/separate_panel.h
ui/countryinput.cpp
ui/countryinput.h
ui/empty_userpic.cpp
@@ -1024,8 +1029,6 @@ PRIVATE
ui/search_field_controller.h
ui/special_buttons.cpp
ui/special_buttons.h
ui/special_fields.cpp
ui/special_fields.h
ui/unread_badge.cpp
ui/unread_badge.h
window/main_window.cpp

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1860,6 +1860,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payments_invoice_label_test" = "Test invoice";
"lng_payments_receipt_button" = "Receipt";
"lng_payments_checkout_title" = "Checkout";
"lng_payments_receipt_title" = "Receipt";
"lng_payments_total_label" = "Total";
"lng_payments_date_label" = "Date";
"lng_payments_pay_amount" = "Pay {amount}";
"lng_payments_payment_method" = "Payment Method";
"lng_payments_new_card" = "New Card...";
"lng_payments_shipping_address" = "Shipping Address";
"lng_payments_address_street1" = "Address 1";
"lng_payments_address_street2" = "Address 2";
"lng_payments_address_city" = "City";
"lng_payments_address_state" = "State";
"lng_payments_address_country" = "Country";
"lng_payments_address_postcode" = "Postcode";
"lng_payments_shipping_method" = "Shipping Method";
"lng_payments_info_name" = "Name";
"lng_payments_info_email" = "Email";
"lng_payments_info_phone" = "Phone";
"lng_payments_shipping_address_title" = "Shipping Information";
"lng_payments_save_shipping_about" = "You can save your shipping information for future use.";
"lng_payments_card_title" = "New Card";
"lng_payments_card_number" = "Card Number";
"lng_payments_card_holder" = "Cardholder name";
"lng_payments_billing_address" = "Billing Information";
"lng_payments_billing_country" = "Country";
"lng_payments_billing_zip_code" = "Zip Code";
"lng_payments_save_payment_about" = "You can save your payment information for future use.";
"lng_payments_save_information" = "Save Information";
"lng_call_status_incoming" = "is calling you...";
"lng_call_status_connecting" = "connecting...";
"lng_call_status_exchanging" = "exchanging encryption keys...";
@@ -1982,6 +2012,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_context_remove_hand" = "Cancel request to speak";
"lng_group_call_context_mute_for_me" = "Mute for me";
"lng_group_call_context_unmute_for_me" = "Unmute for me";
"lng_group_call_context_remove" = "Remove";
"lng_group_call_remove_channel" = "Remove {channel} from the voice chat?";
"lng_group_call_duration_days#one" = "{count} day";
"lng_group_call_duration_days#other" = "{count} days";
"lng_group_call_duration_hours#one" = "{count} hour";
@@ -2224,6 +2256,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_stopped_poll" = "{from} stopped poll:";
"lng_admin_log_invited" = "invited {user}";
"lng_admin_log_banned" = "banned {user}";
"lng_admin_log_unbanned" = "unbanned {user}";
"lng_admin_log_restricted" = "changed restrictions for {user} {until}";
"lng_admin_log_promoted" = "changed privileges for {user}";
"lng_admin_log_transferred" = "transferred ownership to {user}";
@@ -2471,6 +2504,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_export_state_chats_list" = "Processing chats...";
"lng_export_state_chats" = "Chats";
"lng_export_state_ready_progress" = "{ready} / {total}";
"lng_export_skip_file" = "Skip this file";
"lng_export_progress" = "You can close this window now. Please don't quit Telegram until the data export is completed.";
"lng_export_stop" = "Stop";
"lng_export_sure_stop" = "Are you sure you want to stop exporting your data?\n\nIf you do, you'll need to start over.";

View File

@@ -620,8 +620,8 @@ channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant;
channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant;
channelParticipantCreator#447dca4b flags:# user_id:int admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant;
channelParticipantAdmin#ccbebbaf flags:# can_edit:flags.0?true self:flags.1?true user_id:int inviter_id:flags.1?int promoted_by:int date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant;
channelParticipantBanned#1c0facaf flags:# left:flags.0?true user_id:int kicked_by:int date:int banned_rights:ChatBannedRights = ChannelParticipant;
channelParticipantLeft#c3c6796b user_id:int = ChannelParticipant;
channelParticipantBanned#50a1dfd6 flags:# left:flags.0?true peer:Peer kicked_by:int date:int banned_rights:ChatBannedRights = ChannelParticipant;
channelParticipantLeft#1b03f006 peer:Peer = ChannelParticipant;
channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter;
channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;
@@ -632,10 +632,10 @@ channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;
channelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter;
channelParticipantsMentions#e04b5ceb flags:# q:flags.0?string top_msg_id:flags.1?int = ChannelParticipantsFilter;
channels.channelParticipants#f56ee2a8 count:int participants:Vector<ChannelParticipant> users:Vector<User> = channels.ChannelParticipants;
channels.channelParticipants#9ab0feaf count:int participants:Vector<ChannelParticipant> chats:Vector<Chat> users:Vector<User> = channels.ChannelParticipants;
channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants;
channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector<User> = channels.ChannelParticipant;
channels.channelParticipant#dfb80317 participant:ChannelParticipant chats:Vector<Chat> users:Vector<User> = channels.ChannelParticipant;
help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector<MessageEntity> min_age_confirm:flags.1?int = help.TermsOfService;
@@ -1557,7 +1557,7 @@ channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = mes
channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector<int> = Bool;
channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;
channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants;
channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant;
channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant;
channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;
channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;
channels.createChannel#3d5fb10f flags:# broadcast:flags.0?true megagroup:flags.1?true for_import:flags.3?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string = Updates;
@@ -1573,7 +1573,7 @@ channels.deleteChannel#c0111fe3 channel:InputChannel = Updates;
channels.exportMessageLink#e63fadeb flags:# grouped:flags.0?true thread:flags.1?true channel:InputChannel id:int = ExportedMessageLink;
channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates;
channels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true = messages.Chats;
channels.editBanned#72796912 channel:InputChannel user_id:InputUser banned_rights:ChatBannedRights = Updates;
channels.editBanned#96e6cd81 channel:InputChannel participant:InputPeer banned_rights:ChatBannedRights = Updates;
channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector<InputUser> max_id:long min_id:long limit:int = channels.AdminLogResults;
channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool;
channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool;
@@ -1645,4 +1645,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
// LAYER 125
// LAYER 126

View File

@@ -9,7 +9,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="2.7.0.0" />
Version="2.7.1.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 2,7,0,0
PRODUCTVERSION 2,7,0,0
FILEVERSION 2,7,1,0
PRODUCTVERSION 2,7,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "2.7.0.0"
VALUE "FileVersion", "2.7.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.7.0.0"
VALUE "ProductVersion", "2.7.1.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,7,0,0
PRODUCTVERSION 2,7,0,0
FILEVERSION 2,7,1,0
PRODUCTVERSION 2,7,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
VALUE "FileVersion", "2.7.0.0"
VALUE "FileVersion", "2.7.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.7.0.0"
VALUE "ProductVersion", "2.7.1.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -1522,13 +1522,20 @@ void ApiWrap::applyLastParticipantsList(
auto botStatus = channel->mgInfo->botStatus;
const auto emptyAdminRights = MTP_chatAdminRights(MTP_flags(0));
const auto emptyRestrictedRights = MTP_chatBannedRights(
MTP_flags(0),
MTP_int(0));
for (const auto &p : list) {
const auto userId = p.match([](const auto &data) {
return data.vuser_id().v;
const auto participantId = p.match([](
const MTPDchannelParticipantBanned &data) {
return peerFromMTP(data.vpeer());
}, [](const MTPDchannelParticipantLeft &data) {
return peerFromMTP(data.vpeer());
}, [](const auto &data) {
return peerFromUser(data.vuser_id());
});
if (!participantId) {
continue;
}
const auto participant = _session->data().peer(participantId);
const auto user = participant->asUser();
const auto adminCanEdit = (p.type() == mtpc_channelParticipantAdmin)
? p.c_channelParticipantAdmin().is_can_edit()
: (p.type() == mtpc_channelParticipantCreator)
@@ -1541,28 +1548,27 @@ void ApiWrap::applyLastParticipantsList(
: emptyAdminRights;
const auto restrictedRights = (p.type() == mtpc_channelParticipantBanned)
? p.c_channelParticipantBanned().vbanned_rights()
: emptyRestrictedRights;
if (!userId) {
continue;
}
auto user = _session->data().user(userId);
: ChannelData::EmptyRestrictedRights(participant);
if (p.type() == mtpc_channelParticipantCreator) {
Assert(user != nullptr);
const auto &creator = p.c_channelParticipantCreator();
const auto rank = qs(creator.vrank().value_or_empty());
channel->mgInfo->creator = user;
channel->mgInfo->creatorRank = rank;
if (!channel->mgInfo->admins.empty()) {
Data::ChannelAdminChanges(channel).add(userId, rank);
Data::ChannelAdminChanges(channel).add(
peerToUser(participantId),
rank);
}
}
if (!base::contains(channel->mgInfo->lastParticipants, user)) {
if (user
&& !base::contains(channel->mgInfo->lastParticipants, user)) {
channel->mgInfo->lastParticipants.push_back(user);
if (adminRights.c_chatAdminRights().vflags().v) {
channel->mgInfo->lastAdmins.emplace(
user,
MegagroupInfo::Admin{ adminRights, adminCanEdit });
} else if (restrictedRights.c_chatBannedRights().vflags().v != 0) {
} else if (Data::ChatBannedRightsFlags(restrictedRights) != 0) {
channel->mgInfo->lastRestricted.emplace(
user,
MegagroupInfo::Restricted{ restrictedRights });
@@ -1607,22 +1613,29 @@ void ApiWrap::applyBotsList(
auto botStatus = channel->mgInfo->botStatus;
auto keyboardBotFound = !history || !history->lastKeyboardFrom;
for (const auto &p : list) {
const auto userId = p.match([](const auto &data) {
return data.vuser_id().v;
const auto participantId = p.match([](
const MTPDchannelParticipantBanned &data) {
return peerFromMTP(data.vpeer());
}, [](const MTPDchannelParticipantLeft &data) {
return peerFromMTP(data.vpeer());
}, [](const auto &data) {
return peerFromUser(data.vuser_id());
});
if (!userId) {
if (!participantId) {
continue;
}
auto user = _session->data().user(userId);
if (user->isBot()) {
const auto participant = _session->data().peer(participantId);
const auto user = participant->asUser();
if (user && user->isBot()) {
channel->mgInfo->bots.insert(user);
botStatus = 2;// (botStatus > 0/* || !i.key()->botInfo->readsAllHistory*/) ? 2 : 1;
if (!user->botInfo->inited) {
needBotsInfos = true;
}
}
if (!keyboardBotFound && user->id == history->lastKeyboardFrom) {
if (!keyboardBotFound
&& participant->id == history->lastKeyboardFrom) {
keyboardBotFound = true;
}
}
@@ -1657,7 +1670,7 @@ void ApiWrap::requestSelfParticipant(not_null<ChannelData*> channel) {
_selfParticipantRequests.emplace(channel);
request(MTPchannels_GetParticipant(
channel->inputChannel,
MTP_inputUserSelf()
MTP_inputPeerSelf()
)).done([=](const MTPchannels_ChannelParticipant &result) {
_selfParticipantRequests.erase(channel);
result.match([&](const MTPDchannels_channelParticipant &data) {
@@ -1698,11 +1711,13 @@ void ApiWrap::requestSelfParticipant(not_null<ChannelData*> channel) {
void ApiWrap::kickParticipant(
not_null<ChatData*> chat,
not_null<UserData*> user) {
not_null<PeerData*> participant) {
Expects(participant->isUser());
request(MTPmessages_DeleteChatUser(
MTP_flags(0),
chat->inputChat,
user->inputUser
participant->asUser()->inputUser
)).done([=](const MTPUpdates &result) {
applyUpdates(result);
}).send();
@@ -1710,21 +1725,21 @@ void ApiWrap::kickParticipant(
void ApiWrap::kickParticipant(
not_null<ChannelData*> channel,
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &currentRights) {
const auto kick = KickRequest(channel, user);
const auto kick = KickRequest(channel, participant);
if (_kickRequests.contains(kick)) return;
const auto rights = ChannelData::KickedRestrictedRights();
const auto rights = ChannelData::KickedRestrictedRights(participant);
const auto requestId = request(MTPchannels_EditBanned(
channel->inputChannel,
user->inputUser,
participant->input,
rights
)).done([=](const MTPUpdates &result) {
applyUpdates(result);
_kickRequests.remove(KickRequest(channel, user));
channel->applyEditBanned(user, currentRights, rights);
_kickRequests.remove(KickRequest(channel, participant));
channel->applyEditBanned(participant, currentRights, rights);
}).fail([this, kick](const MTP::Error &error) {
_kickRequests.remove(kick);
}).send();
@@ -1734,20 +1749,20 @@ void ApiWrap::kickParticipant(
void ApiWrap::unblockParticipant(
not_null<ChannelData*> channel,
not_null<UserData*> user) {
const auto kick = KickRequest(channel, user);
not_null<PeerData*> participant) {
const auto kick = KickRequest(channel, participant);
if (_kickRequests.contains(kick)) {
return;
}
const auto requestId = request(MTPchannels_EditBanned(
channel->inputChannel,
user->inputUser,
MTP_chatBannedRights(MTP_flags(0), MTP_int(0))
participant->input,
ChannelData::EmptyRestrictedRights(participant)
)).done([=](const MTPUpdates &result) {
applyUpdates(result);
_kickRequests.remove(KickRequest(channel, user));
_kickRequests.remove(KickRequest(channel, participant));
if (channel->kickedCount() > 0) {
channel->setKickedCount(channel->kickedCount() - 1);
} else {
@@ -3183,12 +3198,20 @@ void ApiWrap::refreshChannelAdmins(
const QVector<MTPChannelParticipant> &participants) {
Data::ChannelAdminChanges changes(channel);
for (const auto &p : participants) {
const auto userId = p.match([](const auto &data) {
return data.vuser_id().v;
const auto participantId = p.match([](
const MTPDchannelParticipantBanned &data) {
return peerFromMTP(data.vpeer());
}, [](const MTPDchannelParticipantLeft &data) {
return peerFromMTP(data.vpeer());
}, [](const auto &data) {
return peerFromUser(data.vuser_id());
});
const auto userId = peerToUser(participantId);
p.match([&](const MTPDchannelParticipantAdmin &data) {
Assert(peerIsUser(participantId));
changes.add(userId, qs(data.vrank().value_or_empty()));
}, [&](const MTPDchannelParticipantCreator &data) {
Assert(peerIsUser(participantId));
const auto rank = qs(data.vrank().value_or_empty());
if (const auto info = channel->mgInfo.get()) {
info->creator = channel->owner().userLoaded(userId);
@@ -3196,7 +3219,9 @@ void ApiWrap::refreshChannelAdmins(
}
changes.add(userId, rank);
}, [&](const auto &data) {
changes.remove(userId);
if (userId) {
changes.remove(userId);
}
});
}
}

View File

@@ -249,14 +249,16 @@ public:
void markMediaRead(not_null<HistoryItem*> item);
void requestSelfParticipant(not_null<ChannelData*> channel);
void kickParticipant(not_null<ChatData*> chat, not_null<UserData*> user);
void kickParticipant(
not_null<ChatData*> chat,
not_null<PeerData*> participant);
void kickParticipant(
not_null<ChannelData*> channel,
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &currentRights);
void unblockParticipant(
not_null<ChannelData*> channel,
not_null<UserData*> user);
not_null<PeerData*> participant);
void deleteAllFromUser(
not_null<ChannelData*> channel,
not_null<UserData*> from);
@@ -657,7 +659,7 @@ private:
using KickRequest = std::pair<
not_null<ChannelData*>,
not_null<UserData*>>;
not_null<PeerData*>>;
base::flat_map<KickRequest, mtpRequestId> _kickRequests;
base::flat_set<not_null<ChannelData*>> _selfParticipantRequests;

View File

@@ -268,7 +268,7 @@ AddContactBox::AddContactBox(
this,
st::defaultInputField,
tr::lng_contact_phone(),
ExtractPhonePrefix(session->user()->phone()),
Ui::ExtractPhonePrefix(session->user()->phone()),
phone)
, _invertOrder(langFirstNameGoesSecond()) {
if (!phone.isEmpty()) {

View File

@@ -151,7 +151,7 @@ void ChangePhoneBox::EnterPhone::prepare() {
this,
st::defaultInputField,
tr::lng_change_phone_new_title(),
ExtractPhonePrefix(_session->user()->phone()),
Ui::ExtractPhonePrefix(_session->user()->phone()),
phoneValue);
_phone->resize(st::boxWidth - 2 * st::boxPadding.left(), _phone->height());

View File

@@ -900,7 +900,7 @@ void DeleteMessagesBox::deleteAndClear() {
_moderateInChannel->session().api().kickParticipant(
_moderateInChannel,
_moderateFrom,
MTP_chatBannedRights(MTP_flags(0), MTP_int(0)));
ChannelData::EmptyRestrictedRights(_moderateFrom));
}
if (_reportSpam->checked()) {
_moderateInChannel->session().api().request(

View File

@@ -71,14 +71,6 @@ void ShowPhoneBannedError(const QString &phone) {
[=] { SendToBannedHelp(phone); close(); }));
}
QString ExtractPhonePrefix(const QString &phone) {
const auto pattern = phoneNumberParse(phone);
if (!pattern.isEmpty()) {
return phone.mid(0, pattern[0]);
}
return QString();
}
SentCodeField::SentCodeField(
QWidget *parent,
const style::InputField &st,

View File

@@ -22,7 +22,6 @@ class Session;
} // namespace Main
void ShowPhoneBannedError(const QString &phone);
[[nodiscard]] QString ExtractPhonePrefix(const QString &phone);
class SentCodeField : public Ui::InputField {
public:

View File

@@ -421,6 +421,7 @@ void AddSpecialBoxController::rebuildChatRows(not_null<ChatData*> chat) {
auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count;) {
auto row = delegate()->peerListRowAt(i);
Assert(row->peer()->isUser());
auto user = row->peer()->asUser();
if (participants.contains(user)) {
++i;
@@ -467,8 +468,9 @@ void AddSpecialBoxController::loadMoreRows() {
int availableCount,
const QVector<MTPChannelParticipant> &list) {
for (const auto &data : list) {
if (const auto user = _additional.applyParticipant(data)) {
appendRow(user);
if (const auto participant = _additional.applyParticipant(
data)) {
appendRow(participant);
}
}
if (const auto size = list.size()) {
@@ -491,20 +493,25 @@ void AddSpecialBoxController::loadMoreRows() {
}
void AddSpecialBoxController::rowClicked(not_null<PeerListRow*> row) {
auto user = row->peer()->asUser();
const auto participant = row->peer();
const auto user = participant->asUser();
switch (_role) {
case Role::Admins: return showAdmin(user);
case Role::Restricted: return showRestricted(user);
case Role::Kicked: return kickUser(user);
case Role::Admins:
Assert(user != nullptr);
return showAdmin(user);
case Role::Restricted:
Assert(user != nullptr);
return showRestricted(user);
case Role::Kicked: return kickUser(participant);
}
Unexpected("Role in AddSpecialBoxController::rowClicked()");
}
template <typename Callback>
bool AddSpecialBoxController::checkInfoLoaded(
not_null<UserData*> user,
not_null<PeerData*> participant,
Callback callback) {
if (_additional.infoLoaded(user)) {
if (_additional.infoLoaded(participant)) {
return true;
}
@@ -512,16 +519,15 @@ bool AddSpecialBoxController::checkInfoLoaded(
const auto channel = _peer->asChannel();
_api.request(MTPchannels_GetParticipant(
channel->inputChannel,
user->inputUser
participant->input
)).done([=](const MTPchannels_ChannelParticipant &result) {
Expects(result.type() == mtpc_channels_channelParticipant);
const auto &participant = result.c_channels_channelParticipant();
channel->owner().processUsers(participant.vusers());
_additional.applyParticipant(participant.vparticipant());
result.match([&](const MTPDchannels_channelParticipant &data) {
channel->owner().processUsers(data.vusers());
_additional.applyParticipant(data.vparticipant());
});
callback();
}).fail([=](const MTP::Error &error) {
_additional.setExternal(user);
_additional.setExternal(participant);
callback();
}).send();
return false;
@@ -724,15 +730,13 @@ void AddSpecialBoxController::showRestricted(
// Finally edit the restricted.
const auto currentRights = restrictedRights
? *restrictedRights
: MTPChatBannedRights(MTP_chatBannedRights(
MTP_flags(0),
MTP_int(0)));
: ChannelData::EmptyRestrictedRights(user);
auto box = Box<EditRestrictedBox>(
_peer,
user,
_additional.adminRights(user).has_value(),
currentRights);
if (_additional.canRestrictUser(user)) {
if (_additional.canRestrictParticipant(user)) {
const auto done = crl::guard(this, [=](
const MTPChatBannedRights &newRights) {
editRestrictedDone(user, newRights);
@@ -749,50 +753,61 @@ void AddSpecialBoxController::showRestricted(
}
void AddSpecialBoxController::editRestrictedDone(
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &rights) {
if (_editParticipantBox) {
_editParticipantBox->closeBox();
}
const auto date = base::unixtime::now(); // Incorrect, but ignored.
if (rights.c_chatBannedRights().vflags().v == 0) {
_additional.applyParticipant(MTP_channelParticipant(
MTP_int(user->bareId()),
MTP_int(date)));
if (Data::ChatBannedRightsFlags(rights) == 0) {
if (const auto user = participant->asUser()) {
_additional.applyParticipant(MTP_channelParticipant(
MTP_int(user->bareId()),
MTP_int(date)));
} else {
_additional.setExternal(participant);
}
} else {
const auto kicked = rights.c_chatBannedRights().is_view_messages();
const auto alreadyRestrictedBy = _additional.restrictedBy(user);
const auto kicked = Data::ChatBannedRightsFlags(rights)
& ChatRestriction::f_view_messages;
const auto alreadyRestrictedBy = _additional.restrictedBy(
participant);
_additional.applyParticipant(MTP_channelParticipantBanned(
MTP_flags(kicked
? MTPDchannelParticipantBanned::Flag::f_left
: MTPDchannelParticipantBanned::Flag(0)),
MTP_int(user->bareId()),
(participant->isUser()
? MTP_peerUser(MTP_int(participant->bareId()))
: participant->isChat()
? MTP_peerChat(MTP_int(participant->bareId()))
: MTP_peerChannel(MTP_int(participant->bareId()))),
MTP_int(alreadyRestrictedBy
? alreadyRestrictedBy->bareId()
: user->session().userId()),
: participant->session().userId()),
MTP_int(date),
rights));
}
if (const auto callback = _bannedDoneCallback) {
callback(user, rights);
callback(participant, rights);
}
}
void AddSpecialBoxController::kickUser(
not_null<UserData*> user,
not_null<PeerData*> participant,
bool sure) {
if (!checkInfoLoaded(user, [=] { kickUser(user); })) {
if (!checkInfoLoaded(participant, [=] { kickUser(participant); })) {
return;
}
const auto kickUserSure = crl::guard(this, [=] {
kickUser(user, true);
kickUser(participant, true);
});
// Check restrictions.
if (_additional.adminRights(user).has_value()
|| _additional.isCreator(user)) {
const auto user = participant->asUser();
if (user && (_additional.adminRights(user).has_value()
|| (_additional.isCreator(user)))) {
// The user is an admin or creator.
if (!_additional.isCreator(user) && _additional.canEditAdmin(user)) {
if (!sure) {
@@ -818,37 +833,39 @@ void AddSpecialBoxController::kickUser(
: tr::lng_profile_sure_kick_channel)(
tr::now,
lt_user,
user->name);
participant->name);
_editBox = Ui::show(
Box<ConfirmBox>(text, kickUserSure),
Ui::LayerOption::KeepOther);
return;
}
const auto restrictedRights = _additional.restrictedRights(user);
const auto restrictedRights = _additional.restrictedRights(participant);
const auto currentRights = restrictedRights
? *restrictedRights
: MTPChatBannedRights(MTP_chatBannedRights(
MTP_flags(0),
MTP_int(0)));
: ChannelData::EmptyRestrictedRights(participant);
const auto done = crl::guard(this, [=](
const MTPChatBannedRights &newRights) {
editRestrictedDone(user, newRights);
editRestrictedDone(participant, newRights);
});
const auto fail = crl::guard(this, [=] {
_editBox = nullptr;
});
const auto callback = SaveRestrictedCallback(_peer, user, done, fail);
callback(currentRights, ChannelData::KickedRestrictedRights());
const auto callback = SaveRestrictedCallback(
_peer,
participant,
done,
fail);
callback(currentRights, ChannelData::KickedRestrictedRights(participant));
}
bool AddSpecialBoxController::appendRow(not_null<UserData*> user) {
if (delegate()->peerListFindRow(user->id)
|| (_excludeSelf && user->isSelf())) {
bool AddSpecialBoxController::appendRow(not_null<PeerData*> participant) {
if (delegate()->peerListFindRow(participant->id)
|| (_excludeSelf && participant->isSelf())) {
return false;
}
delegate()->peerListAppendRow(createRow(user));
delegate()->peerListAppendRow(createRow(participant));
return true;
}
@@ -861,8 +878,8 @@ bool AddSpecialBoxController::prependRow(not_null<UserData*> user) {
}
std::unique_ptr<PeerListRow> AddSpecialBoxController::createRow(
not_null<UserData*> user) const {
return std::make_unique<PeerListRow>(user);
not_null<PeerData*> participant) const {
return std::make_unique<PeerListRow>(participant);
}
AddSpecialBoxSearchController::AddSpecialBoxSearchController(

View File

@@ -80,7 +80,7 @@ public:
const MTPChatAdminRights &adminRights,
const QString &rank)>;
using BannedDoneCallback = Fn<void(
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &bannedRights)>;
AddSpecialBoxController(
not_null<PeerData*> peer,
@@ -101,7 +101,7 @@ public:
private:
template <typename Callback>
bool checkInfoLoaded(not_null<UserData*> user, Callback callback);
bool checkInfoLoaded(not_null<PeerData*> participant, Callback callback);
void prepareChatRows(not_null<ChatData*> chat);
void rebuildChatRows(not_null<ChatData*> chat);
@@ -113,12 +113,13 @@ private:
const QString &rank);
void showRestricted(not_null<UserData*> user, bool sure = false);
void editRestrictedDone(
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &rights);
void kickUser(not_null<UserData*> user, bool sure = false);
bool appendRow(not_null<UserData*> user);
void kickUser(not_null<PeerData*> participant, bool sure = false);
bool appendRow(not_null<PeerData*> participant);
bool prependRow(not_null<UserData*> user);
std::unique_ptr<PeerListRow> createRow(not_null<UserData*> user) const;
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const;
void subscribeToMigration();
void migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);

View File

@@ -222,7 +222,8 @@ MTPChatAdminRights EditAdminBox::defaultRights() const {
| Flag::f_post_messages
| Flag::f_edit_messages
| Flag::f_delete_messages
| Flag::f_invite_users);
| Flag::f_invite_users
| Flag::f_manage_call);
return MTP_chatAdminRights(MTP_flags(flags));
}
@@ -611,11 +612,11 @@ void EditRestrictedBox::prepare() {
const auto defaultRestrictions = chat
? chat->defaultRestrictions()
: channel->defaultRestrictions();
const auto prepareRights = _oldRights.c_chatBannedRights().vflags().v
const auto prepareRights = Data::ChatBannedRightsFlags(_oldRights)
? _oldRights
: defaultRights();
const auto prepareFlags = FixDependentRestrictions(
prepareRights.c_chatBannedRights().vflags().v
Data::ChatBannedRightsFlags(prepareRights)
| defaultRestrictions
| ((channel && channel->isPublic())
? (Flag::f_change_info | Flag::f_pin_messages)
@@ -646,7 +647,7 @@ void EditRestrictedBox::prepare() {
disabledMessages);
addControl(std::move(checkboxes), QMargins());
_until = prepareRights.c_chatBannedRights().vuntil_date().v;
_until = Data::ChatBannedRightsUntilDate(prepareRights);
addControl(object_ptr<Ui::BoxContentDivider>(this), st::rightsUntilMargin);
addControl(
object_ptr<Ui::FlatLabel>(
@@ -766,7 +767,7 @@ void EditRestrictedBox::createUntilVariants() {
}
};
auto addCurrentVariant = [&](TimeId from, TimeId to) {
auto oldUntil = _oldRights.c_chatBannedRights().vuntil_date().v;
auto oldUntil = Data::ChatBannedRightsUntilDate(_oldRights);
if (oldUntil < _until) {
addCustomVariant(oldUntil, from, to);
}

View File

@@ -146,18 +146,18 @@ void SaveChannelAdmin(
void SaveChannelRestriction(
not_null<ChannelData*> channel,
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &oldRights,
const MTPChatBannedRights &newRights,
Fn<void()> onDone,
Fn<void()> onFail) {
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel,
user->inputUser,
participant->input,
newRights
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
channel->applyEditBanned(user, oldRights, newRights);
channel->applyEditBanned(participant, oldRights, newRights);
if (onDone) {
onDone();
}
@@ -243,7 +243,7 @@ Fn<void(
const MTPChatBannedRights &oldRights,
const MTPChatBannedRights &newRights)> SaveRestrictedCallback(
not_null<PeerData*> peer,
not_null<UserData*> user,
not_null<PeerData*> participant,
Fn<void(const MTPChatBannedRights &newRights)> onDone,
Fn<void()> onFail) {
return [=](
@@ -253,19 +253,21 @@ Fn<void(
const auto saveForChannel = [=](not_null<ChannelData*> channel) {
SaveChannelRestriction(
channel,
user,
participant,
oldRights,
newRights,
done,
onFail);
};
if (const auto chat = peer->asChatNotMigrated()) {
const auto flags = newRights.match([](
const MTPDchatBannedRights &data) {
return data.vflags().v;
});
if (flags & MTPDchatBannedRights::Flag::f_view_messages) {
SaveChatParticipantKick(chat, user, done, onFail);
const auto flags = Data::ChatBannedRightsFlags(newRights);
if (participant->isUser()
&& (flags & MTPDchatBannedRights::Flag::f_view_messages)) {
SaveChatParticipantKick(
chat,
participant->asUser(),
done,
onFail);
} else if (!flags) {
done();
} else {
@@ -313,9 +315,9 @@ ParticipantsAdditionalData::ParticipantsAdditionalData(
}
bool ParticipantsAdditionalData::infoLoaded(
not_null<UserData*> user) const {
not_null<PeerData*> participant) const {
return _peer->isChat()
|| (_infoNotLoaded.find(user) == end(_infoNotLoaded));
|| (_infoNotLoaded.find(participant) == end(_infoNotLoaded));
}
bool ParticipantsAdditionalData::canEditAdmin(
@@ -342,24 +344,27 @@ bool ParticipantsAdditionalData::canAddOrEditAdmin(
Unexpected("Peer in ParticipantsAdditionalData::canAddOrEditAdmin.");
}
bool ParticipantsAdditionalData::canRestrictUser(
not_null<UserData*> user) const {
if (!canEditAdmin(user) || user->isSelf()) {
bool ParticipantsAdditionalData::canRestrictParticipant(
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (user && (!canEditAdmin(user) || user->isSelf())) {
return false;
} else if (const auto chat = _peer->asChat()) {
return chat->canBanMembers();
} else if (const auto channel = _peer->asChannel()) {
return channel->canBanMembers();
}
Unexpected("Peer in ParticipantsAdditionalData::canRestrictUser.");
Unexpected("Peer in ParticipantsAdditionalData::canRestrictParticipant.");
}
bool ParticipantsAdditionalData::canRemoveUser(
not_null<UserData*> user) const {
if (canRestrictUser(user)) {
bool ParticipantsAdditionalData::canRemoveParticipant(
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (canRestrictParticipant(participant)) {
return true;
} else if (const auto chat = _peer->asChat()) {
return !user->isSelf()
return user
&& !user->isSelf()
&& chat->invitedByMe.contains(user)
&& (chat->amCreator() || !_admins.contains(user));
}
@@ -388,12 +393,12 @@ QString ParticipantsAdditionalData::adminRank(
}
auto ParticipantsAdditionalData::restrictedRights(
not_null<UserData*> user) const
not_null<PeerData*> participant) const
-> std::optional<MTPChatBannedRights> {
if (_peer->isChat()) {
return std::nullopt;
}
const auto i = _restrictedRights.find(user);
const auto i = _restrictedRights.find(participant);
return (i != end(_restrictedRights))
? std::make_optional(i->second)
: std::nullopt;
@@ -404,16 +409,18 @@ bool ParticipantsAdditionalData::isCreator(not_null<UserData*> user) const {
}
bool ParticipantsAdditionalData::isExternal(
not_null<UserData*> user) const {
not_null<PeerData*> participant) const {
return _peer->isChat()
? !_members.contains(user)
: _external.find(user) != end(_external);
? (participant->isUser()
&& !_members.contains(participant->asUser()))
: _external.find(participant) != end(_external);
}
bool ParticipantsAdditionalData::isKicked(not_null<UserData*> user) const {
bool ParticipantsAdditionalData::isKicked(
not_null<PeerData*> participant) const {
return _peer->isChat()
? false
: _kicked.find(user) != end(_kicked);
: _kicked.find(participant) != end(_kicked);
}
UserData *ParticipantsAdditionalData::adminPromotedBy(
@@ -426,29 +433,41 @@ UserData *ParticipantsAdditionalData::adminPromotedBy(
}
UserData *ParticipantsAdditionalData::restrictedBy(
not_null<UserData*> user) const {
not_null<PeerData*> participant) const {
if (_peer->isChat()) {
return nullptr;
}
const auto i = _restrictedBy.find(user);
const auto i = _restrictedBy.find(participant);
return (i != end(_restrictedBy)) ? i->second.get() : nullptr;
}
void ParticipantsAdditionalData::setExternal(not_null<UserData*> user) {
_infoNotLoaded.erase(user);
_external.emplace(user);
void ParticipantsAdditionalData::setExternal(
not_null<PeerData*> participant) {
if (const auto user = participant->asUser()) {
_adminRights.erase(user);
_adminCanEdit.erase(user);
_adminPromotedBy.erase(user);
_admins.erase(user);
}
_restrictedRights.erase(participant);
_kicked.erase(participant);
_restrictedBy.erase(participant);
_infoNotLoaded.erase(participant);
_external.emplace(participant);
}
void ParticipantsAdditionalData::checkForLoaded(not_null<UserData*> user) {
void ParticipantsAdditionalData::checkForLoaded(
not_null<PeerData*> participant) {
const auto contains = [](const auto &map, const auto &value) {
return map.find(value) != map.end();
};
if (_creator != user
&& !contains(_adminRights, user)
&& !contains(_restrictedRights, user)
&& !contains(_external, user)
&& !contains(_kicked, user)) {
_infoNotLoaded.emplace(user);
const auto user = participant->asUser();
if (!(user && _creator == user)
&& !(user && contains(_adminRights, user))
&& !contains(_restrictedRights, participant)
&& !contains(_external, participant)
&& !contains(_kicked, participant)) {
_infoNotLoaded.emplace(participant);
}
}
@@ -510,15 +529,15 @@ void ParticipantsAdditionalData::fillFromChannel(
}
}
UserData *ParticipantsAdditionalData::applyParticipant(
PeerData *ParticipantsAdditionalData::applyParticipant(
const MTPChannelParticipant &data) {
return applyParticipant(data, _role);
}
UserData *ParticipantsAdditionalData::applyParticipant(
PeerData *ParticipantsAdditionalData::applyParticipant(
const MTPChannelParticipant &data,
Role overrideRole) {
const auto logBad = [&]() -> UserData* {
const auto logBad = [&]() -> PeerData* {
LOG(("API Error: Bad participant type %1 got "
"while requesting for participants, role: %2"
).arg(data.type()
@@ -526,27 +545,28 @@ UserData *ParticipantsAdditionalData::applyParticipant(
return nullptr;
};
return data.match([&](const MTPDchannelParticipantCreator &data) {
return data.match([&](
const MTPDchannelParticipantCreator &data) -> PeerData* {
if (overrideRole != Role::Profile
&& overrideRole != Role::Members
&& overrideRole != Role::Admins) {
return logBad();
}
return applyCreator(data);
}, [&](const MTPDchannelParticipantAdmin &data) {
}, [&](const MTPDchannelParticipantAdmin &data) -> PeerData* {
if (overrideRole != Role::Profile
&& overrideRole != Role::Members
&& overrideRole != Role::Admins) {
return logBad();
}
return applyAdmin(data);
}, [&](const MTPDchannelParticipantSelf &data) {
}, [&](const MTPDchannelParticipantSelf &data) -> PeerData* {
if (overrideRole != Role::Profile
&& overrideRole != Role::Members) {
return logBad();
}
return applyRegular(data.vuser_id());
}, [&](const MTPDchannelParticipant &data) {
}, [&](const MTPDchannelParticipant &data) -> PeerData* {
if (overrideRole != Role::Profile
&& overrideRole != Role::Members) {
return logBad();
@@ -645,32 +665,35 @@ UserData *ParticipantsAdditionalData::applyRegular(MTPint userId) {
return user;
}
UserData *ParticipantsAdditionalData::applyBanned(
PeerData *ParticipantsAdditionalData::applyBanned(
const MTPDchannelParticipantBanned &data) {
const auto user = _peer->owner().userLoaded(data.vuser_id().v);
if (!user) {
const auto participant = _peer->owner().peerLoaded(
peerFromMTP(data.vpeer()));
if (!participant) {
return nullptr;
}
_infoNotLoaded.erase(user);
_adminRights.erase(user);
_adminCanEdit.erase(user);
_adminPromotedBy.erase(user);
if (data.is_left()) {
_kicked.emplace(user);
} else {
_kicked.erase(user);
_infoNotLoaded.erase(participant);
if (const auto user = participant->asUser()) {
_adminRights.erase(user);
_adminCanEdit.erase(user);
_adminPromotedBy.erase(user);
}
_restrictedRights[user] = data.vbanned_rights();
if (data.is_left()) {
_kicked.emplace(participant);
} else {
_kicked.erase(participant);
}
_restrictedRights[participant] = data.vbanned_rights();
if (const auto by = _peer->owner().userLoaded(data.vkicked_by().v)) {
const auto i = _restrictedBy.find(user);
const auto i = _restrictedBy.find(participant);
if (i == _restrictedBy.end()) {
_restrictedBy.emplace(user, by);
_restrictedBy.emplace(participant, by);
} else {
i->second = by;
}
}
return user;
return participant;
}
void ParticipantsAdditionalData::migrate(
@@ -922,9 +945,9 @@ void ParticipantsBoxController::addNewItem() {
editAdminDone(user, rights, rank);
});
const auto restrictedDone = crl::guard(this, [=](
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &rights) {
editRestrictedDone(user, rights);
editRestrictedDone(participant, rights);
});
const auto initBox = [](not_null<PeerListBox*> box) {
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
@@ -955,8 +978,10 @@ void ParticipantsBoxController::addNewParticipants() {
auto already = std::vector<not_null<UserData*>>();
already.reserve(count);
for (auto i = 0; i != count; ++i) {
already.emplace_back(
delegate()->peerListRowAt(i)->peer()->asUser());
const auto participant = delegate()->peerListRowAt(i)->peer();
if (const auto user = participant->asUser()) {
already.emplace_back(user);
}
}
AddParticipantsBoxController::Start(
_navigation,
@@ -1165,6 +1190,7 @@ void ParticipantsBoxController::rebuildChatParticipants(
auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count;) {
auto row = delegate()->peerListRowAt(i);
Assert(row->peer()->isUser());
auto user = row->peer()->asUser();
if (participants.contains(user)) {
++i;
@@ -1316,8 +1342,9 @@ void ParticipantsBoxController::loadMoreRows() {
int availableCount,
const QVector<MTPChannelParticipant> &list) {
for (const auto &data : list) {
if (const auto user = _additional.applyParticipant(data)) {
appendRow(user);
if (const auto participant = _additional.applyParticipant(
data)) {
appendRow(participant);
}
}
if (const auto size = list.size()) {
@@ -1398,31 +1425,32 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() {
}
void ParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {
Expects(row->peer()->isUser());
const auto user = row->peer()->asUser();
const auto participant = row->peer();
const auto user = participant->asUser();
if (_role == Role::Admins) {
Assert(user != nullptr);
showAdmin(user);
} else if (_role == Role::Restricted
&& (_peer->isChat() || _peer->isMegagroup())) {
&& (_peer->isChat() || _peer->isMegagroup())
&& user) {
showRestricted(user);
} else {
Assert(_navigation != nullptr);
_navigation->showPeerInfo(user);
_navigation->showPeerInfo(participant);
}
}
void ParticipantsBoxController::rowActionClicked(
not_null<PeerListRow*> row) {
Expects(row->peer()->isUser());
const auto user = row->peer()->asUser();
const auto participant = row->peer();
const auto user = participant->asUser();
if (_role == Role::Members || _role == Role::Profile) {
kickMember(user);
kickParticipant(participant);
} else if (_role == Role::Admins) {
Assert(user != nullptr);
removeAdmin(user);
} else {
removeKicked(row, user);
removeKicked(row, participant);
}
}
@@ -1433,28 +1461,30 @@ base::unique_qptr<Ui::PopupMenu> ParticipantsBoxController::rowContextMenu(
const auto chat = _peer->asChat();
const auto channel = _peer->asChannel();
const auto user = row->peer()->asUser();
const auto participant = row->peer();
const auto user = participant->asUser();
auto result = base::make_unique_q<Ui::PopupMenu>(parent);
if (_navigation) {
result->addAction(
tr::lng_context_view_profile(tr::now),
crl::guard(this, [=] { _navigation->showPeerInfo(user); }));
crl::guard(this, [=] {
_navigation->showPeerInfo(participant); }));
}
if (_role == Role::Kicked) {
if (_peer->isMegagroup()
&& _additional.canRestrictUser(user)) {
if (channel->canAddMembers()) {
&& _additional.canRestrictParticipant(participant)) {
if (user && channel->canAddMembers()) {
result->addAction(
tr::lng_context_add_to_group(tr::now),
crl::guard(this, [=] { unkickMember(user); }));
crl::guard(this, [=] { unkickParticipant(user); }));
}
result->addAction(
tr::lng_profile_delete_removed(tr::now),
crl::guard(this, [=] { removeKickedWithRow(user); }));
crl::guard(this, [=] { removeKickedWithRow(participant); }));
}
return result;
}
if (_additional.canAddOrEditAdmin(user)) {
if (user && _additional.canAddOrEditAdmin(user)) {
const auto isAdmin = _additional.isCreator(user)
|| _additional.adminRights(user).has_value();
result->addAction(
@@ -1463,7 +1493,7 @@ base::unique_qptr<Ui::PopupMenu> ParticipantsBoxController::rowContextMenu(
: tr::lng_context_promote_admin)(tr::now),
crl::guard(this, [=] { showAdmin(user); }));
}
if (_additional.canRestrictUser(user)) {
if (_additional.canRestrictParticipant(participant)) {
const auto canRestrictWithoutKick = [&] {
if (const auto chat = _peer->asChat()) {
return chat->amCreator();
@@ -1476,14 +1506,14 @@ base::unique_qptr<Ui::PopupMenu> ParticipantsBoxController::rowContextMenu(
crl::guard(this, [=] { showRestricted(user); }));
}
}
if (_additional.canRemoveUser(user)) {
if (!_additional.isKicked(user)) {
if (_additional.canRemoveParticipant(participant)) {
if (!_additional.isKicked(participant)) {
const auto isGroup = _peer->isChat() || _peer->isMegagroup();
result->addAction(
(isGroup
? tr::lng_context_remove_from_group
: tr::lng_profile_kick)(tr::now),
crl::guard(this, [=] { kickMember(user); }));
crl::guard(this, [=] { kickParticipant(user); }));
}
}
return result;
@@ -1569,9 +1599,7 @@ void ParticipantsBoxController::showRestricted(not_null<UserData*> user) {
const auto restrictedRights = _additional.restrictedRights(user);
const auto currentRights = restrictedRights
? *restrictedRights
: MTPChatBannedRights(MTP_chatBannedRights(
MTP_flags(0),
MTP_int(0)));
: ChannelData::EmptyRestrictedRights(user);
const auto hasAdminRights = _additional.adminRights(user).has_value();
auto box = Box<EditRestrictedBox>(
_peer,
@@ -1580,7 +1608,7 @@ void ParticipantsBoxController::showRestricted(not_null<UserData*> user) {
currentRights);
const auto chat = _peer->asChat();
const auto channel = _peer->asChannel();
if (_additional.canRestrictUser(user)) {
if (_additional.canRestrictParticipant(user)) {
const auto done = crl::guard(this, [=](
const MTPChatBannedRights &newRights) {
editRestrictedDone(user, newRights);
@@ -1597,72 +1625,84 @@ void ParticipantsBoxController::showRestricted(not_null<UserData*> user) {
}
void ParticipantsBoxController::editRestrictedDone(
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &rights) {
_addBox = nullptr;
if (_editParticipantBox) {
_editParticipantBox->closeBox();
}
const auto user = participant->asUser();
const auto date = base::unixtime::now(); // Incorrect, but ignored.
if (rights.c_chatBannedRights().vflags().v == 0) {
_additional.applyParticipant(MTP_channelParticipant(
MTP_int(user->bareId()),
MTP_int(date)));
if (Data::ChatBannedRightsFlags(rights) == 0) {
if (user) {
_additional.applyParticipant(MTP_channelParticipant(
MTP_int(user->bareId()),
MTP_int(date)));
} else {
_additional.setExternal(participant);
}
if (_role == Role::Kicked || _role == Role::Restricted) {
removeRow(user);
removeRow(participant);
}
} else {
const auto kicked = rights.c_chatBannedRights().is_view_messages();
const auto alreadyRestrictedBy = _additional.restrictedBy(user);
const auto kicked = Data::ChatBannedRightsFlags(rights)
& ChatRestriction::f_view_messages;
const auto alreadyRestrictedBy = _additional.restrictedBy(
participant);
_additional.applyParticipant(MTP_channelParticipantBanned(
MTP_flags(kicked
? MTPDchannelParticipantBanned::Flag::f_left
: MTPDchannelParticipantBanned::Flag(0)),
MTP_int(user->bareId()),
(participant->isUser()
? MTP_peerUser(MTP_int(participant->bareId()))
: participant->isChat()
? MTP_peerChat(MTP_int(participant->bareId()))
: MTP_peerChannel(MTP_int(participant->bareId()))),
MTP_int(alreadyRestrictedBy
? alreadyRestrictedBy->bareId()
: user->session().userId()),
: participant->session().userId()),
MTP_int(date),
rights));
if (kicked) {
if (_role == Role::Kicked) {
prependRow(user);
prependRow(participant);
} else if (_role == Role::Admins
|| _role == Role::Restricted
|| _role == Role::Members) {
removeRow(user);
removeRow(participant);
}
} else {
if (_role == Role::Restricted) {
prependRow(user);
prependRow(participant);
} else if (_role == Role::Kicked
|| _role == Role::Admins
|| _role == Role::Members) {
removeRow(user);
removeRow(participant);
}
}
}
recomputeTypeFor(user);
recomputeTypeFor(participant);
delegate()->peerListRefreshRows();
}
void ParticipantsBoxController::kickMember(not_null<UserData*> user) {
void ParticipantsBoxController::kickParticipant(not_null<PeerData*> participant) {
const auto user = participant->asUser();
const auto text = ((_peer->isChat() || _peer->isMegagroup())
? tr::lng_profile_sure_kick
: tr::lng_profile_sure_kick_channel)(
tr::now,
lt_user,
user->firstName);
user ? user->firstName : participant->name);
_editBox = Ui::show(
Box<ConfirmBox>(
text,
tr::lng_box_remove(tr::now),
crl::guard(this, [=] { kickMemberSure(user); })),
crl::guard(this, [=] { kickParticipantSure(participant); })),
Ui::LayerOption::KeepOther);
}
void ParticipantsBoxController::unkickMember(not_null<UserData*> user) {
void ParticipantsBoxController::unkickParticipant(not_null<UserData*> user) {
_editBox = nullptr;
if (const auto row = delegate()->peerListFindRow(user->id)) {
delegate()->peerListRemoveRow(row);
@@ -1671,25 +1711,24 @@ void ParticipantsBoxController::unkickMember(not_null<UserData*> user) {
_peer->session().api().addChatParticipants(_peer, { 1, user });
}
void ParticipantsBoxController::kickMemberSure(not_null<UserData*> user) {
void ParticipantsBoxController::kickParticipantSure(
not_null<PeerData*> participant) {
_editBox = nullptr;
const auto restrictedRights = _additional.restrictedRights(user);
const auto restrictedRights = _additional.restrictedRights(participant);
const auto currentRights = restrictedRights
? *restrictedRights
: MTPChatBannedRights(MTP_chatBannedRights(
MTP_flags(0),
MTP_int(0)));
: ChannelData::EmptyRestrictedRights(participant);
if (const auto row = delegate()->peerListFindRow(user->id)) {
if (const auto row = delegate()->peerListFindRow(participant->id)) {
delegate()->peerListRemoveRow(row);
delegate()->peerListRefreshRows();
}
auto &session = _peer->session();
if (const auto chat = _peer->asChat()) {
session.api().kickParticipant(chat, user);
session.api().kickParticipant(chat, participant);
} else if (const auto channel = _peer->asChannel()) {
session.api().kickParticipant(channel, user, currentRights);
session.api().kickParticipant(channel, participant, currentRights);
}
}
@@ -1730,36 +1769,37 @@ void ParticipantsBoxController::removeAdminSure(not_null<UserData*> user) {
}
void ParticipantsBoxController::removeKickedWithRow(
not_null<UserData*> user) {
if (const auto row = delegate()->peerListFindRow(user->id)) {
removeKicked(row, user);
not_null<PeerData*> participant) {
if (const auto row = delegate()->peerListFindRow(participant->id)) {
removeKicked(row, participant);
} else {
removeKicked(user);
removeKicked(participant);
}
}
void ParticipantsBoxController::removeKicked(not_null<UserData*> user) {
void ParticipantsBoxController::removeKicked(
not_null<PeerData*> participant) {
if (const auto channel = _peer->asChannel()) {
channel->session().api().unblockParticipant(channel, user);
channel->session().api().unblockParticipant(channel, participant);
}
}
void ParticipantsBoxController::removeKicked(
not_null<PeerListRow*> row,
not_null<UserData*> user) {
not_null<PeerData*> participant) {
delegate()->peerListRemoveRow(row);
if (_role != Role::Kicked
&& !delegate()->peerListFullRowsCount()) {
setDescriptionText(tr::lng_blocked_list_not_found(tr::now));
}
delegate()->peerListRefreshRows();
removeKicked(user);
removeKicked(participant);
}
bool ParticipantsBoxController::appendRow(not_null<UserData*> user) {
if (delegate()->peerListFindRow(user->id)) {
recomputeTypeFor(user);
bool ParticipantsBoxController::appendRow(not_null<PeerData*> participant) {
if (delegate()->peerListFindRow(participant->id)) {
recomputeTypeFor(participant);
return false;
} else if (auto row = createRow(user)) {
} else if (auto row = createRow(participant)) {
delegate()->peerListAppendRow(std::move(row));
if (_role != Role::Kicked) {
setDescriptionText(QString());
@@ -1769,16 +1809,16 @@ bool ParticipantsBoxController::appendRow(not_null<UserData*> user) {
return false;
}
bool ParticipantsBoxController::prependRow(not_null<UserData*> user) {
if (const auto row = delegate()->peerListFindRow(user->id)) {
recomputeTypeFor(user);
bool ParticipantsBoxController::prependRow(not_null<PeerData*> participant) {
if (const auto row = delegate()->peerListFindRow(participant->id)) {
recomputeTypeFor(participant);
refreshCustomStatus(row);
if (_role == Role::Admins) {
// Perhaps we've added a new admin from search.
delegate()->peerListPrependRowFromSearchResult(row);
}
return false;
} else if (auto row = createRow(user)) {
} else if (auto row = createRow(participant)) {
delegate()->peerListPrependRow(std::move(row));
if (_role != Role::Kicked) {
setDescriptionText(QString());
@@ -1788,8 +1828,8 @@ bool ParticipantsBoxController::prependRow(not_null<UserData*> user) {
return false;
}
bool ParticipantsBoxController::removeRow(not_null<UserData*> user) {
if (auto row = delegate()->peerListFindRow(user->id)) {
bool ParticipantsBoxController::removeRow(not_null<PeerData*> participant) {
if (auto row = delegate()->peerListFindRow(participant->id)) {
if (_role == Role::Admins) {
// Perhaps we are removing an admin from search results.
row->setCustomStatus(tr::lng_channel_admin_status_not_admin(tr::now));
@@ -1807,24 +1847,28 @@ bool ParticipantsBoxController::removeRow(not_null<UserData*> user) {
}
std::unique_ptr<PeerListRow> ParticipantsBoxController::createRow(
not_null<UserData*> user) const {
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (_role == Role::Profile) {
Assert(user != nullptr);
return std::make_unique<Row>(user, computeType(user));
}
const auto chat = _peer->asChat();
const auto channel = _peer->asChannel();
auto row = std::make_unique<PeerListRowWithLink>(user);
auto row = std::make_unique<PeerListRowWithLink>(participant);
refreshCustomStatus(row.get());
if (_role == Role::Admins
&& user
&& !_additional.isCreator(user)
&& _additional.adminRights(user).has_value()
&& _additional.canEditAdmin(user)) {
row->setActionLink(tr::lng_profile_kick(tr::now));
} else if (_role == Role::Kicked || _role == Role::Restricted) {
if (_additional.canRestrictUser(user)) {
if (_additional.canRestrictParticipant(participant)) {
row->setActionLink(tr::lng_profile_delete_removed(tr::now));
}
} else if (_role == Role::Members) {
Assert(user != nullptr);
if ((chat ? chat->canBanMembers() : channel->canBanMembers())
&& !_additional.isCreator(user)
&& (!_additional.adminRights(user)
@@ -1842,31 +1886,34 @@ std::unique_ptr<PeerListRow> ParticipantsBoxController::createRow(
}
auto ParticipantsBoxController::computeType(
not_null<UserData*> user) const -> Type {
not_null<PeerData*> participant) const -> Type {
const auto user = participant->asUser();
auto result = Type();
result.rights = _additional.isCreator(user)
result.rights = (user && _additional.isCreator(user))
? Rights::Creator
: _additional.adminRights(user).has_value()
: (user && _additional.adminRights(user).has_value())
? Rights::Admin
: Rights::Normal;
result.canRemove = _additional.canRemoveUser(user);
result.canRemove = _additional.canRemoveParticipant(participant);
return result;
}
void ParticipantsBoxController::recomputeTypeFor(
not_null<UserData*> user) {
not_null<PeerData*> participant) {
if (_role != Role::Profile) {
return;
}
if (const auto row = delegate()->peerListFindRow(user->id)) {
static_cast<Row*>(row)->setType(computeType(user));
if (const auto row = delegate()->peerListFindRow(participant->id)) {
static_cast<Row*>(row)->setType(computeType(participant));
}
}
void ParticipantsBoxController::refreshCustomStatus(
not_null<PeerListRow*> row) const {
const auto user = row->peer()->asUser();
const auto participant = row->peer();
const auto user = participant->asUser();
if (_role == Role::Admins) {
Assert(user != nullptr);
if (const auto by = _additional.adminPromotedBy(user)) {
row->setCustomStatus(tr::lng_channel_admin_status_promoted_by(
tr::now,
@@ -1882,7 +1929,7 @@ void ParticipantsBoxController::refreshCustomStatus(
}
}
} else if (_role == Role::Kicked || _role == Role::Restricted) {
const auto by = _additional.restrictedBy(user);
const auto by = _additional.restrictedBy(participant);
row->setCustomStatus((_role == Role::Kicked
? tr::lng_channel_banned_status_removed_by
: tr::lng_channel_banned_status_restricted_by)(

View File

@@ -33,7 +33,7 @@ Fn<void(
const MTPChatBannedRights &oldRights,
const MTPChatBannedRights &newRights)> SaveRestrictedCallback(
not_null<PeerData*> peer,
not_null<UserData*> user,
not_null<PeerData*> participant,
Fn<void(const MTPChatBannedRights &newRights)> onDone,
Fn<void()> onFail);
@@ -77,29 +77,31 @@ public:
ParticipantsAdditionalData(not_null<PeerData*> peer, Role role);
UserData *applyParticipant(const MTPChannelParticipant &data);
UserData *applyParticipant(
PeerData *applyParticipant(const MTPChannelParticipant &data);
PeerData *applyParticipant(
const MTPChannelParticipant &data,
Role overrideRole);
void setExternal(not_null<UserData*> user);
void checkForLoaded(not_null<UserData*> user);
void setExternal(not_null<PeerData*> participant);
void checkForLoaded(not_null<PeerData*> participant);
void fillFromPeer();
[[nodiscard]] bool infoLoaded(not_null<UserData*> user) const;
[[nodiscard]] bool infoLoaded(not_null<PeerData*> participant) const;
[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;
[[nodiscard]] bool canAddOrEditAdmin(not_null<UserData*> user) const;
[[nodiscard]] bool canRestrictUser(not_null<UserData*> user) const;
[[nodiscard]] bool canRemoveUser(not_null<UserData*> user) const;
[[nodiscard]] bool canRestrictParticipant(
not_null<PeerData*> participant) const;
[[nodiscard]] bool canRemoveParticipant(
not_null<PeerData*> participant) const;
[[nodiscard]] std::optional<MTPChatAdminRights> adminRights(
not_null<UserData*> user) const;
QString adminRank(not_null<UserData*> user) const;
[[nodiscard]] std::optional<MTPChatBannedRights> restrictedRights(
not_null<UserData*> user) const;
not_null<PeerData*> participant) const;
[[nodiscard]] bool isCreator(not_null<UserData*> user) const;
[[nodiscard]] bool isExternal(not_null<UserData*> user) const;
[[nodiscard]] bool isKicked(not_null<UserData*> user) const;
[[nodiscard]] bool isExternal(not_null<PeerData*> participant) const;
[[nodiscard]] bool isKicked(not_null<PeerData*> participant) const;
[[nodiscard]] UserData *adminPromotedBy(not_null<UserData*> user) const;
[[nodiscard]] UserData *restrictedBy(not_null<UserData*> user) const;
[[nodiscard]] UserData *restrictedBy(not_null<PeerData*> participant) const;
void migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);
@@ -107,7 +109,7 @@ private:
UserData *applyCreator(const MTPDchannelParticipantCreator &data);
UserData *applyAdmin(const MTPDchannelParticipantAdmin &data);
UserData *applyRegular(MTPint userId);
UserData *applyBanned(const MTPDchannelParticipantBanned &data);
PeerData *applyBanned(const MTPDchannelParticipantBanned &data);
void fillFromChat(not_null<ChatData*> chat);
void fillFromChannel(not_null<ChannelData*> channel);
@@ -124,11 +126,11 @@ private:
base::flat_map<not_null<UserData*>, QString> _adminRanks;
base::flat_set<not_null<UserData*>> _adminCanEdit;
base::flat_map<not_null<UserData*>, not_null<UserData*>> _adminPromotedBy;
std::map<not_null<UserData*>, MTPChatBannedRights> _restrictedRights;
std::set<not_null<UserData*>> _kicked;
std::map<not_null<UserData*>, not_null<UserData*>> _restrictedBy;
std::set<not_null<UserData*>> _external;
std::set<not_null<UserData*>> _infoNotLoaded;
std::map<not_null<PeerData*>, MTPChatBannedRights> _restrictedRights;
std::set<not_null<PeerData*>> _kicked;
std::map<not_null<PeerData*>, not_null<UserData*>> _restrictedBy;
std::set<not_null<PeerData*>> _external;
std::set<not_null<PeerData*>> _infoNotLoaded;
};
@@ -181,7 +183,7 @@ protected:
Role role);
virtual std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) const;
not_null<PeerData*> participant) const;
private:
using Row = Info::Profile::MemberListRow;
@@ -223,23 +225,25 @@ private:
const QString &rank);
void showRestricted(not_null<UserData*> user);
void editRestrictedDone(
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &rights);
void removeKicked(not_null<PeerListRow*> row, not_null<UserData*> user);
void removeKickedWithRow(not_null<UserData*> user);
void removeKicked(not_null<UserData*> user);
void kickMember(not_null<UserData*> user);
void kickMemberSure(not_null<UserData*> user);
void unkickMember(not_null<UserData*> user);
void removeKicked(
not_null<PeerListRow*> row,
not_null<PeerData*> participant);
void removeKickedWithRow(not_null<PeerData*> participant);
void removeKicked(not_null<PeerData*> participant);
void kickParticipant(not_null<PeerData*> participant);
void kickParticipantSure(not_null<PeerData*> participant);
void unkickParticipant(not_null<UserData*> user);
void removeAdmin(not_null<UserData*> user);
void removeAdminSure(not_null<UserData*> user);
bool appendRow(not_null<UserData*> user);
bool prependRow(not_null<UserData*> user);
bool removeRow(not_null<UserData*> user);
bool appendRow(not_null<PeerData*> participant);
bool prependRow(not_null<PeerData*> participant);
bool removeRow(not_null<PeerData*> participant);
void refreshCustomStatus(not_null<PeerListRow*> row) const;
bool feedMegagroupLastParticipants();
Type computeType(not_null<UserData*> user) const;
void recomputeTypeFor(not_null<UserData*> user);
Type computeType(not_null<PeerData*> participant) const;
void recomputeTypeFor(not_null<PeerData*> participant);
void subscribeToMigration();
void migrate(not_null<ChatData*> chat, not_null<ChannelData*> channel);

View File

@@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "boxes/add_contact_box.h"
#include "boxes/confirm_box.h"
#include "boxes/single_choice_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/edit_peer_type_box.h"
@@ -20,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_peer_invite_links.h"
#include "boxes/peers/edit_linked_chat_box.h"
#include "boxes/stickers_box.h"
#include "ui/boxes/single_choice_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "core/application.h"
#include "core/core_settings.h"

View File

@@ -151,6 +151,7 @@ std::vector<std::pair<ChatAdminRights, QString>> AdminRightLabels(
{ Flag::f_edit_messages, tr::lng_rights_channel_edit(tr::now) },
{ Flag::f_delete_messages, tr::lng_rights_channel_delete(tr::now) },
{ Flag::f_invite_users, tr::lng_rights_group_invite(tr::now) },
{ Flag::f_manage_call, tr::lng_rights_group_manage_calls(tr::now) },
{ Flag::f_add_admins, tr::lng_rights_add_admins(tr::now) }
};
}

View File

@@ -1144,7 +1144,8 @@ void GroupCall::broadcastPartStart(std::shared_ptr<LoadPartTask> task) {
rejoin();
return;
}
const auto status = MTP::IsFloodError(error)
const auto status = (MTP::IsFloodError(error)
|| error.type() == u"TIME_TOO_BIG"_q)
? Status::NotReady
: Status::ResyncNeeded;
finish({

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/calls_group_call.h"
#include "calls/calls_group_common.h"
#include "calls/calls_group_menu.h"
#include "calls/calls_volume_item.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
@@ -143,9 +144,6 @@ public:
void addActionRipple(QPoint point, Fn<void()> updateCallback) override;
void stopLastActionRipple() override;
int nameIconWidth() const override {
return 0;
}
QSize actionSize() const override {
return QSize(
st::groupCallActiveButton.width,
@@ -207,17 +205,17 @@ private:
};
struct StatusIcon {
StatusIcon(float volume)
: speaker(st::groupCallStatusSpeakerIcon)
, arcs(std::make_unique<Ui::Paint::ArcsAnimation>(
st::groupCallStatusSpeakerArcsAnimation,
kSpeakerThreshold,
volume,
Ui::Paint::ArcsAnimation::Direction::Right)) {
}
StatusIcon(bool shown, float volume);
const style::icon &speaker;
const std::unique_ptr<Ui::Paint::ArcsAnimation> arcs;
Ui::Paint::ArcsAnimation arcs;
Ui::Animations::Simple arcsAnimation;
Ui::Animations::Simple shownAnimation;
QString percent;
int percentWidth = 0;
int arcsWidth = 0;
int wasArcsWidth = 0;
bool shown = true;
rpl::lifetime lifetime;
};
@@ -249,7 +247,6 @@ private:
Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon.
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
Ui::Animations::Simple _arcsAnimation; // For volume arcs animation.
QString _aboutText;
crl::time _speakingLastTime = 0;
uint64 _raisedHandRating = 0;
@@ -375,6 +372,26 @@ private:
};
[[nodiscard]] QString StatusPercentString(float volume) {
return QString::number(int(std::round(volume * 200))) + '%';
}
[[nodiscard]] int StatusPercentWidth(const QString &percent) {
return st::normalFont->width(percent);
}
Row::StatusIcon::StatusIcon(bool shown, float volume)
: speaker(st::groupCallStatusSpeakerIcon)
, arcs(
st::groupCallStatusSpeakerArcsAnimation,
kSpeakerThreshold,
volume,
Ui::Paint::ArcsAnimation::Direction::Right)
, percent(StatusPercentString(volume))
, percentWidth(StatusPercentWidth(percent))
, shown(shown) {
}
Row::Row(
not_null<RowDelegate*> delegate,
not_null<PeerData*> participantPeer)
@@ -435,32 +452,30 @@ void Row::setSpeaking(bool speaking) {
|| (_state == State::MutedByMe)
|| (_state == State::Muted)
|| (_state == State::RaisedHand)) {
_statusIcon = nullptr;
if (_statusIcon) {
_statusIcon = nullptr;
_delegate->rowUpdateRow(this);
}
} else if (!_statusIcon) {
_statusIcon = std::make_unique<StatusIcon>(
(_volume != Group::kDefaultVolume),
(float)_volume / Group::kMaxVolume);
_statusIcon->arcs->setStrokeRatio(kArcsStrokeRatio);
_statusIcon->arcsWidth = _statusIcon->arcs->finishedWidth();
const auto wasArcsWidth = _statusIcon->lifetime.make_state<int>(0);
_statusIcon->arcs->startUpdateRequests(
_statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio);
_statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth();
_statusIcon->arcs.startUpdateRequests(
) | rpl::start_with_next([=] {
if (!_arcsAnimation.animating()) {
*wasArcsWidth = _statusIcon->arcsWidth;
if (!_statusIcon->arcsAnimation.animating()) {
_statusIcon->wasArcsWidth = _statusIcon->arcsWidth;
}
auto callback = [=](float64 value) {
if (_statusIcon) {
_statusIcon->arcs->update(crl::now());
_statusIcon->arcsWidth = anim::interpolate(
*wasArcsWidth,
_statusIcon->arcs->finishedWidth(),
value);
}
_statusIcon->arcs.update(crl::now());
_statusIcon->arcsWidth = anim::interpolate(
_statusIcon->wasArcsWidth,
_statusIcon->arcs.finishedWidth(),
value);
_delegate->rowUpdateRow(this);
};
_arcsAnimation.start(
_statusIcon->arcsAnimation.start(
std::move(callback),
0.,
1.,
@@ -535,7 +550,20 @@ void Row::setSsrc(uint32 ssrc) {
void Row::setVolume(int volume) {
_volume = volume;
if (_statusIcon) {
_statusIcon->arcs->setValue((float)volume / Group::kMaxVolume);
const auto floatVolume = (float)volume / Group::kMaxVolume;
_statusIcon->arcs.setValue(floatVolume);
_statusIcon->percent = StatusPercentString(floatVolume);
_statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent);
const auto shown = (volume != Group::kDefaultVolume);
if (_statusIcon->shown != shown) {
_statusIcon->shown = shown;
_statusIcon->shownAnimation.start(
[=] { _delegate->rowUpdateRow(this); },
shown ? 0. : 1.,
shown ? 1. : 0.,
st::groupCallSpeakerArcsAnimation.duration);
}
}
}
@@ -660,21 +688,20 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback {
}
int Row::statusIconWidth() const {
if (!_statusIcon) {
if (!_statusIcon || !_speaking) {
return 0;
}
return _speaking
? (_statusIcon->speaker.width() + _statusIcon->arcsWidth)
: 0;
const auto shown = _statusIcon->shownAnimation.value(
_statusIcon->shown ? 1. : 0.);
const auto full = _statusIcon->speaker.width()
+ _statusIcon->arcsWidth
+ _statusIcon->percentWidth
+ st::normalFont->spacew;
return int(std::round(shown * full));
}
int Row::statusIconHeight() const {
if (!_statusIcon) {
return 0;
}
return _speaking
? _statusIcon->speaker.height()
: 0;
return (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0;
}
void Row::paintStatusIcon(
@@ -685,6 +712,12 @@ void Row::paintStatusIcon(
if (!_statusIcon) {
return;
}
const auto shown = _statusIcon->shownAnimation.value(
_statusIcon->shown ? 1. : 0.);
if (shown == 0.) {
return;
}
p.setFont(font);
const auto color = (_speaking
? st.statusFgActive
@@ -699,17 +732,34 @@ void Row::paintStatusIcon(
+ QPoint(
speakerRect.width() - st::groupCallStatusSpeakerArcsSkip,
speakerRect.height() / 2);
const auto fullWidth = speakerRect.width()
+ _statusIcon->arcsWidth
+ _statusIcon->percentWidth
+ st::normalFont->spacew;
const auto volume = std::round(_volume / 100.);
p.save();
if (shown < 1.) {
const auto centerx = speakerRect.x() + fullWidth / 2;
const auto centery = speakerRect.y() + speakerRect.height() / 2;
p.translate(centerx, centery);
p.scale(shown, shown);
p.translate(-centerx, -centery);
}
_statusIcon->speaker.paint(
p,
speakerRect.topLeft(),
speakerRect.width(),
color);
p.save();
p.translate(arcPosition);
_statusIcon->arcs->paint(p, color);
_statusIcon->arcs.paint(p, color);
p.translate(-arcPosition);
p.setFont(st::normalFont);
p.setPen(st.statusFgActive);
p.drawTextLeft(
st.statusPosition.x() + speakerRect.width() + _statusIcon->arcsWidth,
st.statusPosition.y(),
fullWidth,
_statusIcon->percent);
p.restore();
}
@@ -738,11 +788,14 @@ void Row::paintStatusText(
if (about.isEmpty()
&& _state != State::Invited
&& _state != State::MutedByMe) {
p.save();
paintStatusIcon(p, st, font, selected);
const auto translatedWidth = statusIconWidth();
p.translate(translatedWidth, 0);
const auto guard = gsl::finally([&] { p.restore(); });
const auto guard = gsl::finally([&] {
p.translate(-translatedWidth, 0);
});
PeerListRow::paintStatusText(
p,
st,
@@ -821,9 +874,7 @@ void Row::paintAction(
void Row::refreshStatus() {
setCustomStatus(
(_speaking
? u"%1% %2"_q
.arg(std::round(_volume / 100.))
.arg(tr::lng_group_call_active(tr::now))
? tr::lng_group_call_active(tr::now)
: _raisedHandStatus
? tr::lng_group_call_raised_hand_status(tr::now)
: tr::lng_group_call_inactive(tr::now)),
@@ -1573,7 +1624,7 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
Window::SectionShow::Way::Forward);
});
};
const auto removeFromGroup = crl::guard(this, [=] {
const auto removeFromVoiceChat = crl::guard(this, [=] {
_kickParticipantRequests.fire_copy(participantPeer);
});
@@ -1610,9 +1661,7 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
}
const auto canKick = [&] {
const auto user = participantPeer->asUser();
if (!user) {
return false;
} else if (static_cast<Row*>(row.get())->state()
if (static_cast<Row*>(row.get())->state()
== Row::State::Invited) {
return false;
} else if (const auto chat = _peer->asChat()) {
@@ -1620,16 +1669,16 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
|| (user
&& chat->canBanMembers()
&& !chat->admins.contains(user));
} else if (const auto group = _peer->asMegagroup()) {
return group->amCreator()
|| (user && group->canRestrictUser(user));
} else if (const auto channel = _peer->asChannel()) {
return channel->canRestrictParticipant(participantPeer);
}
return false;
}();
if (canKick) {
result->addAction(
tr::lng_context_remove_from_group(tr::now),
removeFromGroup);
result->addAction(MakeAttentionAction(
result->menu(),
tr::lng_group_call_context_remove(tr::now),
removeFromVoiceChat));
}
}
if (result->empty()) {

View File

@@ -517,16 +517,10 @@ base::unique_qptr<Ui::Menu::ItemBase> MakeRecordingAction(
base::unique_qptr<Ui::Menu::ItemBase> MakeFinishAction(
not_null<Ui::Menu::Menu*> menu,
Fn<void()> callback) {
return base::make_unique_q<Ui::Menu::Action>(
return MakeAttentionAction(
menu,
st::groupCallFinishMenu,
Ui::Menu::CreateAction(
menu,
tr::lng_group_call_end(tr::now),
std::move(callback)),
nullptr,
nullptr);
tr::lng_group_call_end(tr::now),
std::move(callback));
}
} // namespace
@@ -675,6 +669,21 @@ void FillMenu(
BoxContext::GroupCallPanel));
}
}));
}
base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
not_null<Ui::Menu::Menu*> menu,
const QString &text,
Fn<void()> callback) {
return base::make_unique_q<Ui::Menu::Action>(
menu,
st::groupCallFinishMenu,
Ui::Menu::CreateAction(
menu,
text,
std::move(callback)),
nullptr,
nullptr);
}

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
namespace Ui {
class DropdownMenu;
@@ -15,6 +16,11 @@ class GenericBox;
class BoxContent;
} // namespace Ui
namespace Ui::Menu {
class ItemBase;
class Menu;
} // namespace Ui::Menu
namespace Calls {
class GroupCall;
} // namespace Calls
@@ -45,4 +51,9 @@ void FillMenu(
Fn<void()> chooseJoinAs,
Fn<void(object_ptr<Ui::BoxContent>)> showBox);
[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
not_null<Ui::Menu::Menu*> menu,
const QString &text,
Fn<void()> callback);
} // namespace Calls::Group

View File

@@ -23,7 +23,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/layer_manager.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/toasts/common_toasts.h"
#include "ui/special_buttons.h"
#include "info/profile/info_profile_values.h" // Info::Profile::Value.
@@ -82,7 +81,7 @@ private:
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) const override;
not_null<PeerData*> participant) const override;
not_null<PeerData*> _peer;
const base::flat_set<not_null<UserData*>> _alreadyIn;
@@ -190,8 +189,9 @@ bool InviteController::isAlreadyIn(not_null<UserData*> user) const {
}
std::unique_ptr<PeerListRow> InviteController::createRow(
not_null<UserData*> user) const {
if (user->isSelf() || user->isBot()) {
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (!user || user->isSelf() || user->isBot()) {
return nullptr;
}
auto result = std::make_unique<PeerListRow>(user);
@@ -291,9 +291,10 @@ Panel::Panel(not_null<GroupCall*> call)
call->allowedToSpeakNotifications(
) | rpl::start_with_next([=] {
if (isActive()) {
Ui::Toast::Show(
widget(),
tr::lng_group_call_can_speak_here(tr::now));
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { tr::lng_group_call_can_speak_here(tr::now) },
});
} else {
const auto real = _peer->groupCall();
const auto name = (real
@@ -301,13 +302,12 @@ Panel::Panel(not_null<GroupCall*> call)
&& !real->title().isEmpty())
? real->title()
: _peer->name;
Ui::Toast::Show(Ui::Toast::Config{
Ui::ShowMultilineToast({
.text = tr::lng_group_call_can_speak(
tr::now,
lt_chat,
Ui::Text::Bold(name),
Ui::Text::WithEntities),
.st = &st::defaultToast,
});
}
}, widget()->lifetime());
@@ -529,16 +529,17 @@ void Panel::initWithCall(GroupCall *call) {
_members->kickParticipantRequests(
) | rpl::start_with_next([=](not_null<PeerData*> participantPeer) {
if (const auto user = participantPeer->asUser()) {
kickMember(user);
}
kickParticipant(participantPeer);
}, _callLifetime);
const auto showBox = [=](object_ptr<Ui::BoxContent> next) {
_layerBg->showBox(std::move(next));
};
const auto showToast = [=](QString text) {
Ui::Toast::Show(widget(), text);
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { text },
});
};
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
_peer,
@@ -659,9 +660,10 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
const auto skip = st::groupCallRecordingMarkSkip;
_recordingMark->resize(size + 2 * skip, size + 2 * skip);
_recordingMark->setClickedCallback([=] {
Ui::Toast::Show(
widget(),
tr::lng_group_call_is_recorded(tr::now));
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { tr::lng_group_call_is_recorded(tr::now) },
});
});
const auto animate = [=] {
const auto opaque = state->opaque;
@@ -697,13 +699,16 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool recorded) {
validateRecordingMark(recorded);
Ui::Toast::Show(
widget(),
(recorded
? tr::lng_group_call_recording_started(tr::now)
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = (recorded
? tr::lng_group_call_recording_started
: (_call && _call->recordingStoppedByMe())
? tr::lng_group_call_recording_saved(tr::now)
: tr::lng_group_call_recording_stopped(tr::now)));
? tr::lng_group_call_recording_saved
: tr::lng_group_call_recording_stopped)(
tr::now,
Ui::Text::RichLangValue),
});
}, widget()->lifetime());
validateRecordingMark(real->recordStartDate() != 0);
@@ -755,7 +760,10 @@ void Panel::chooseJoinAs() {
_layerBg->showBox(std::move(next));
};
const auto showToast = [=](QString text) {
Ui::Toast::Show(widget(), text);
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = { text },
});
};
_joinAsProcess.start(
_peer,
@@ -849,28 +857,24 @@ void Panel::addMembers() {
}
const auto result = call->inviteUsers(users);
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
Ui::Toast::Show(
widget(),
Ui::Toast::Config{
.text = tr::lng_group_call_invite_done_user(
tr::now,
lt_user,
Ui::Text::Bold((*user)->firstName),
Ui::Text::WithEntities),
.st = &st::defaultToast,
});
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = tr::lng_group_call_invite_done_user(
tr::now,
lt_user,
Ui::Text::Bold((*user)->firstName),
Ui::Text::WithEntities),
});
} else if (const auto count = std::get_if<int>(&result)) {
if (*count > 0) {
Ui::Toast::Show(
widget(),
Ui::Toast::Config{
.text = tr::lng_group_call_invite_done_many(
tr::now,
lt_count,
*count,
Ui::Text::RichLangValue),
.st = &st::defaultToast,
});
Ui::ShowMultilineToast({
.parentOverride = widget(),
.text = tr::lng_group_call_invite_done_many(
tr::now,
lt_count,
*count,
Ui::Text::RichLangValue),
});
}
} else {
Unexpected("Result in GroupCall::inviteUsers.");
@@ -954,15 +958,22 @@ void Panel::addMembers() {
_layerBg->showBox(Box<PeerListsBox>(std::move(controllers), initBox));
}
void Panel::kickMember(not_null<UserData*> user) {
void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
_layerBg->showBox(Box([=](not_null<Ui::GenericBox*> box) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
tr::lng_profile_sure_kick(
tr::now,
lt_user,
user->firstName),
(!participantPeer->isUser()
? tr::lng_group_call_remove_channel(
tr::now,
lt_channel,
participantPeer->name)
: (_peer->isBroadcast()
? tr::lng_profile_sure_kick_channel
: tr::lng_profile_sure_kick)(
tr::now,
lt_user,
participantPeer->asUser()->firstName)),
st::groupCallBoxLabel),
style::margins(
st::boxRowPadding.left(),
@@ -971,26 +982,29 @@ void Panel::kickMember(not_null<UserData*> user) {
st::boxPadding.bottom()));
box->addButton(tr::lng_box_remove(), [=] {
box->closeBox();
kickMemberSure(user);
kickParticipantSure(participantPeer);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
}
void Panel::kickMemberSure(not_null<UserData*> user) {
void Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {
if (const auto chat = _peer->asChat()) {
chat->session().api().kickParticipant(chat, user);
chat->session().api().kickParticipant(chat, participantPeer);
} else if (const auto channel = _peer->asChannel()) {
const auto currentRestrictedRights = [&]() -> MTPChatBannedRights {
const auto it = channel->mgInfo->lastRestricted.find(user);
return (it != channel->mgInfo->lastRestricted.cend())
? it->second.rights
: MTP_chatBannedRights(MTP_flags(0), MTP_int(0));
const auto currentRestrictedRights = [&] {
const auto user = participantPeer->asUser();
if (!channel->mgInfo || !user) {
return ChannelData::EmptyRestrictedRights(participantPeer);
}
const auto i = channel->mgInfo->lastRestricted.find(user);
return (i != channel->mgInfo->lastRestricted.cend())
? i->second.rights
: ChannelData::EmptyRestrictedRights(participantPeer);
}();
channel->session().api().kickParticipant(
channel,
user,
participantPeer,
currentRestrictedRights);
}
}

View File

@@ -89,8 +89,8 @@ private:
void showMainMenu();
void chooseJoinAs();
void addMembers();
void kickMember(not_null<UserData*> user);
void kickMemberSure(not_null<UserData*> user);
void kickParticipant(not_null<PeerData*> participantPeer);
void kickParticipantSure(not_null<PeerData*> participantPeer);
[[nodiscard]] QRect computeTitleRect() const;
void refreshTitle();
void refreshTitleGeometry();

View File

@@ -19,7 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/input_fields.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/toasts/common_toasts.h"
#include "lang/lang_keys.h"
#include "boxes/share_box.h"
#include "history/history_message.h" // GetErrorTextForSending.
@@ -35,7 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_group_call.h"
#include "data/data_changes.h"
#include "core/application.h"
#include "boxes/single_choice_box.h"
#include "ui/boxes/single_choice_box.h"
#include "webrtc/webrtc_audio_input_tester.h"
#include "webrtc/webrtc_media_devices.h"
#include "settings/settings_common.h"
@@ -535,9 +535,10 @@ void SettingsBox(
box->getDelegate()->show(std::move(next));
});
const auto showToast = crl::guard(box, [=](QString text) {
Ui::Toast::Show(
box->getDelegate()->outerContainer(),
text);
Ui::ShowMultilineToast({
.parentOverride = box->getDelegate()->outerContainer(),
.text = { text },
});
});
auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction(
peer,
@@ -572,9 +573,10 @@ void SettingsBox(
}
QGuiApplication::clipboard()->setText(link);
if (weakBox) {
Ui::Toast::Show(
box->getDelegate()->outerContainer(),
tr::lng_create_channel_link_copied(tr::now));
Ui::ShowMultilineToast({
.parentOverride = box->getDelegate()->outerContainer(),
.text = { tr::lng_create_channel_link_copied(tr::now) },
});
}
return true;
};

View File

@@ -625,8 +625,8 @@ void TopBar::setInfoLabels() {
const auto user = call->user();
const auto fullName = user->name;
const auto shortName = user->firstName;
_fullInfoLabel->setText(fullName.toUpper());
_shortInfoLabel->setText(shortName.toUpper());
_fullInfoLabel->setText(fullName);
_shortInfoLabel->setText(shortName);
} else if (const auto group = _groupCall.get()) {
const auto peer = group->peer();
const auto real = peer->groupCall();
@@ -634,8 +634,8 @@ void TopBar::setInfoLabels() {
const auto text = _isGroupConnecting.current()
? tr::lng_group_call_connecting(tr::now)
: (real && real->id() == group->id() && !real->title().isEmpty())
? real->title().toUpper()
: name.toUpper();
? real->title()
: name;
_fullInfoLabel->setText(text);
_shortInfoLabel->setText(text);
}

View File

@@ -13,9 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
enum {
MaxSelectedItems = 100,
MaxPhoneCodeLength = 4, // max length of country phone code
MaxPhoneTailLength = 32, // rest of the phone number, without country code (seen 12 at least), need more for service numbers
LocalEncryptIterCount = 4000, // key derivation iteration count
LocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway)
LocalEncryptSaltSize = 32, // 256 bit

View File

@@ -108,7 +108,7 @@ void ComputeExternalUpdater() {
while (!fileStream.atEnd()) {
const auto path = fileStream.readLine();
if (path == (cWorkingDir() + cExeName())) {
if (path == (cExeDir() + cExeName())) {
SetUpdaterDisabledAtStartup();
return;
}

View File

@@ -126,6 +126,10 @@ QString UiIntegration::timeFormat() {
return cTimeFormat();
}
QWidget *UiIntegration::modalWindowParent() {
return Core::App().getModalParent();
}
std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
const EntityLinkData &data,
const std::any &context) {

View File

@@ -46,6 +46,8 @@ public:
void startFontsEnd() override;
QString timeFormat() override;
QWidget *modalWindowParent() override;
std::shared_ptr<ClickHandler> createLinkHandler(
const EntityLinkData &data,
const std::any &context) override;

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 = 2007000;
constexpr auto AppVersionStr = "2.7";
constexpr auto AppVersion = 2007001;
constexpr auto AppVersionStr = "2.7.1";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -188,7 +188,13 @@ void ChannelData::setKickedCount(int newKickedCount) {
}
}
MTPChatBannedRights ChannelData::KickedRestrictedRights() {
MTPChatBannedRights ChannelData::EmptyRestrictedRights(
not_null<PeerData*> participant) {
return MTP_chatBannedRights(MTP_flags(0), MTP_int(0));
}
MTPChatBannedRights ChannelData::KickedRestrictedRights(
not_null<PeerData*> participant) {
using Flag = MTPDchatBannedRights::Flag;
const auto flags = Flag::f_view_messages
| Flag::f_send_messages
@@ -199,7 +205,7 @@ MTPChatBannedRights ChannelData::KickedRestrictedRights() {
| Flag::f_send_games
| Flag::f_send_inline;
return MTP_chatBannedRights(
MTP_flags(flags),
MTP_flags(participant->isUser() ? flags : Flag::f_view_messages),
MTP_int(std::numeric_limits<int32>::max()));
}
@@ -267,11 +273,17 @@ void ChannelData::applyEditAdmin(
session().changes().peerUpdated(this, UpdateFlag::Admins);
}
void ChannelData::applyEditBanned(not_null<UserData*> user, const MTPChatBannedRights &oldRights, const MTPChatBannedRights &newRights) {
void ChannelData::applyEditBanned(
not_null<PeerData*> participant,
const MTPChatBannedRights &oldRights,
const MTPChatBannedRights &newRights) {
auto flags = UpdateFlag::BannedUsers | UpdateFlag::None;
auto isKicked = (newRights.c_chatBannedRights().vflags().v & MTPDchatBannedRights::Flag::f_view_messages);
auto isRestricted = !isKicked && (newRights.c_chatBannedRights().vflags().v != 0);
if (mgInfo) {
auto isKicked = Data::ChatBannedRightsFlags(newRights)
& ChatRestriction::f_view_messages;
auto isRestricted = !isKicked
&& (Data::ChatBannedRightsFlags(newRights) != 0);
const auto user = participant->asUser();
if (mgInfo && user) {
// If rights are empty - still remove admin? TODO check
if (mgInfo->lastAdmins.contains(user)) {
mgInfo->lastAdmins.remove(user);
@@ -284,7 +296,9 @@ void ChannelData::applyEditBanned(not_null<UserData*> user, const MTPChatBannedR
auto it = mgInfo->lastRestricted.find(user);
if (isRestricted) {
if (it == mgInfo->lastRestricted.cend()) {
mgInfo->lastRestricted.emplace(user, MegagroupInfo::Restricted { newRights });
mgInfo->lastRestricted.emplace(
user,
MegagroupInfo::Restricted { newRights });
setRestrictedCount(restrictedCount() + 1);
} else {
it->second.rights = newRights;
@@ -297,7 +311,9 @@ void ChannelData::applyEditBanned(not_null<UserData*> user, const MTPChatBannedR
}
}
if (isKicked) {
auto i = ranges::find(mgInfo->lastParticipants, user);
auto i = ranges::find(
mgInfo->lastParticipants,
not_null{ user });
if (i != mgInfo->lastParticipants.end()) {
mgInfo->lastParticipants.erase(i);
}
@@ -319,9 +335,9 @@ void ChannelData::applyEditBanned(not_null<UserData*> user, const MTPChatBannedR
}
}
Data::ChannelAdminChanges(this).remove(peerToUser(user->id));
} else {
} else if (!mgInfo) {
if (isKicked) {
if (membersCount() > 1) {
if (user && membersCount() > 1) {
setMembersCount(membersCount() - 1);
flags |= UpdateFlag::Members;
}
@@ -484,7 +500,7 @@ bool ChannelData::canDelete() const {
}
bool ChannelData::canEditLastAdmin(not_null<UserData*> user) const {
// Duplicated in ParticipantsBoxController::canEditAdmin :(
// Duplicated in ParticipantsAdditionalData::canEditAdmin :(
if (mgInfo) {
auto i = mgInfo->lastAdmins.find(user);
if (i != mgInfo->lastAdmins.cend()) {
@@ -496,7 +512,7 @@ bool ChannelData::canEditLastAdmin(not_null<UserData*> user) const {
}
bool ChannelData::canEditAdmin(not_null<UserData*> user) const {
// Duplicated in ParticipantsBoxController::canEditAdmin :(
// Duplicated in ParticipantsAdditionalData::canEditAdmin :(
if (user->isSelf()) {
return false;
} else if (amCreator()) {
@@ -507,14 +523,17 @@ bool ChannelData::canEditAdmin(not_null<UserData*> user) const {
return adminRights() & AdminRight::f_add_admins;
}
bool ChannelData::canRestrictUser(not_null<UserData*> user) const {
// Duplicated in ParticipantsBoxController::canRestrictUser :(
if (user->isSelf()) {
bool ChannelData::canRestrictParticipant(
not_null<PeerData*> participant) const {
// Duplicated in ParticipantsAdditionalData::canRestrictParticipant :(
if (participant->isSelf()) {
return false;
} else if (amCreator()) {
return true;
} else if (!canEditLastAdmin(user)) {
return false;
} else if (const auto user = participant->asUser()) {
if (!canEditLastAdmin(user)) {
return false;
}
}
return adminRights() & AdminRight::f_ban_users;
}
@@ -543,12 +562,14 @@ void ChannelData::setAdminRights(const MTPChatAdminRights &rights) {
}
void ChannelData::setRestrictions(const MTPChatBannedRights &rights) {
if (rights.c_chatBannedRights().vflags().v == restrictions()
&& rights.c_chatBannedRights().vuntil_date().v == _restrictedUntil) {
const auto restrictedFlags = Data::ChatBannedRightsFlags(rights);
const auto restrictedUntilDate = Data::ChatBannedRightsUntilDate(rights);
if (restrictedFlags == restrictions()
&& restrictedUntilDate == _restrictedUntil) {
return;
}
_restrictedUntil = rights.c_chatBannedRights().vuntil_date().v;
_restrictions.set(rights.c_chatBannedRights().vflags().v);
_restrictedUntil = restrictedUntilDate;
_restrictions.set(restrictedFlags);
if (isMegagroup()) {
const auto self = session().user();
if (hasRestrictions()) {
@@ -568,10 +589,11 @@ void ChannelData::setRestrictions(const MTPChatBannedRights &rights) {
}
void ChannelData::setDefaultRestrictions(const MTPChatBannedRights &rights) {
if (rights.c_chatBannedRights().vflags().v == defaultRestrictions()) {
const auto restrictionFlags = Data::ChatBannedRightsFlags(rights);
if (restrictionFlags == defaultRestrictions()) {
return;
}
_defaultRestrictions.set(rights.c_chatBannedRights().vflags().v);
_defaultRestrictions.set(restrictionFlags);
session().changes().peerUpdated(this, UpdateFlag::Rights);
}
@@ -903,8 +925,13 @@ void ApplyMegagroupAdmins(
auto admins = ranges::make_subrange(
list.begin(), list.end()
) | ranges::views::transform([](const MTPChannelParticipant &p) {
const auto userId = p.match([](const auto &data) {
return data.vuser_id().v;
const auto participantId = p.match([](
const MTPDchannelParticipantBanned &data) {
return peerFromMTP(data.vpeer());
}, [](const MTPDchannelParticipantLeft &data) {
return peerFromMTP(data.vpeer());
}, [](const auto &data) {
return peerFromUser(data.vuser_id());
});
const auto rank = p.match([](const MTPDchannelParticipantAdmin &data) {
return qs(data.vrank().value_or_empty());
@@ -913,10 +940,13 @@ void ApplyMegagroupAdmins(
}, [](const auto &data) {
return QString();
});
return std::make_pair(userId, rank);
return std::make_pair(participantId, rank);
}) | ranges::views::filter([](const auto &pair) {
return peerIsUser(pair.first);
});
for (const auto &[userId, rank] : admins) {
adding.emplace(userId, rank);
for (const auto &[participantId, rank] : admins) {
Assert(peerIsUser(participantId));
adding.emplace(peerToUser(participantId), rank);
}
if (channel->mgInfo->creator) {
adding.emplace(

View File

@@ -209,7 +209,10 @@ public:
return flags() & MTPDchannel::Flag::f_fake;
}
static MTPChatBannedRights KickedRestrictedRights();
static MTPChatBannedRights EmptyRestrictedRights(
not_null<PeerData*> participant);
static MTPChatBannedRights KickedRestrictedRights(
not_null<PeerData*> participant);
static constexpr auto kRestrictUntilForever = TimeId(INT_MAX);
[[nodiscard]] static bool IsRestrictedForever(TimeId until) {
return !until || (until == kRestrictUntilForever);
@@ -220,7 +223,7 @@ public:
const MTPChatAdminRights &newRights,
const QString &rank);
void applyEditBanned(
not_null<UserData*> user,
not_null<PeerData*> participant,
const MTPChatBannedRights &oldRights,
const MTPChatBannedRights &newRights);
@@ -310,7 +313,8 @@ public:
[[nodiscard]] bool canEditStickers() const;
[[nodiscard]] bool canDelete() const;
[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;
[[nodiscard]] bool canRestrictUser(not_null<UserData*> user) const;
[[nodiscard]] bool canRestrictParticipant(
not_null<PeerData*> participant) const;
void setInviteLink(const QString &newInviteLink);
[[nodiscard]] QString inviteLink() const {

View File

@@ -156,10 +156,11 @@ void ChatData::setAdminRights(const MTPChatAdminRights &rights) {
}
void ChatData::setDefaultRestrictions(const MTPChatBannedRights &rights) {
if (rights.c_chatBannedRights().vflags().v == defaultRestrictions()) {
const auto restrictionFlags = Data::ChatBannedRightsFlags(rights);
if (restrictionFlags == defaultRestrictions()) {
return;
}
_defaultRestrictions.set(rights.c_chatBannedRights().vflags().v);
_defaultRestrictions.set(restrictionFlags);
session().changes().peerUpdated(this, UpdateFlag::Rights);
}

View File

@@ -659,7 +659,7 @@ void GroupCall::applyActiveUpdate(
void GroupCall::checkFinishSpeakingByActive() {
const auto now = crl::now();
auto nearest = 0;
auto nearest = crl::time(0);
auto stop = std::vector<not_null<PeerData*>>();
for (auto i = begin(_speakingByActiveFinishes)
; i != end(_speakingByActiveFinishes);) {

View File

@@ -1172,4 +1172,16 @@ std::optional<int> ResolvePinnedCount(
: std::nullopt;
}
ChatRestrictions ChatBannedRightsFlags(const MTPChatBannedRights &rights) {
return rights.match([](const MTPDchatBannedRights &data) {
return data.vflags().v;
});
}
TimeId ChatBannedRightsUntilDate(const MTPChatBannedRights &rights) {
return rights.match([](const MTPDchatBannedRights &data) {
return data.vuntil_date().v;
});
}
} // namespace Data

View File

@@ -95,6 +95,11 @@ struct UnavailableReason {
}
};
[[nodiscard]] ChatRestrictions ChatBannedRightsFlags(
const MTPChatBannedRights &rights);
[[nodiscard]] TimeId ChatBannedRightsUntilDate(
const MTPChatBannedRights &rights);
} // namespace Data
class PeerClickHandler : public ClickHandler {

View File

@@ -1820,7 +1820,7 @@ Utf8String FormatDateTime(
).toUtf8();
}
Utf8String FormatMoneyAmount(uint64 amount, const Utf8String &currency) {
Utf8String FormatMoneyAmount(int64 amount, const Utf8String &currency) {
return Ui::FillAmountAndCurrency(
amount,
QString::fromUtf8(currency)).toUtf8();

View File

@@ -660,7 +660,7 @@ Utf8String FormatDateTime(
QChar dateSeparator = QChar('.'),
QChar timeSeparator = QChar(':'),
QChar separator = QChar(' '));
Utf8String FormatMoneyAmount(uint64 amount, const Utf8String &currency);
Utf8String FormatMoneyAmount(int64 amount, const Utf8String &currency);
Utf8String FormatFileSize(int64 size);
Utf8String FormatDuration(int64 seconds);

View File

@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_response.h"
#include "base/value_ordering.h"
#include "base/bytes.h"
#include "base/openssl_help.h"
#include <set>
#include <deque>
@@ -182,6 +183,7 @@ struct ApiWrap::FileProcess {
Fn<bool(FileProgress)> progress;
FnMut<void(const QString &relativePath)> done;
uint64 randomId = 0;
Data::FileLocation location;
Data::FileOrigin origin;
int offset = 0;
@@ -192,6 +194,7 @@ struct ApiWrap::FileProcess {
QByteArray bytes;
};
std::deque<Request> requests;
mtpRequestId requestId = 0;
};
struct ApiWrap::FileProgress {
@@ -383,6 +386,7 @@ auto ApiWrap::fileRequest(const Data::FileLocation &location, int offset) {
Expects(location.dcId != 0
|| location.data.type() == mtpc_inputTakeoutFileLocation);
Expects(_takeoutId.has_value());
Expects(_fileProcess->requestId == 0);
return std::move(_mtp.request(MTPInvokeWithTakeout<MTPupload_GetFile>(
MTP_long(*_takeoutId),
@@ -392,6 +396,7 @@ auto ApiWrap::fileRequest(const Data::FileLocation &location, int offset) {
MTP_int(offset),
MTP_int(kFileChunkSize))
)).fail([=](const MTP::Error &result) {
_fileProcess->requestId = 0;
if (result.type() == qstr("TAKEOUT_FILE_EMPTY")
&& _otherDataProcess != nullptr) {
filePartDone(
@@ -853,6 +858,7 @@ bool ApiWrap::loadUserpicProgress(FileProgress progress) {
< _userpicsProcess->slice->list.size()));
return _userpicsProcess->fileProgress(DownloadProgress{
_fileProcess->randomId,
_fileProcess->relativePath,
_userpicsProcess->fileIndex,
progress.ready,
@@ -1061,6 +1067,17 @@ void ApiWrap::finishExport(FnMut<void()> done) {
)).done(std::move(done)).send();
}
void ApiWrap::skipFile(uint64 randomId) {
if (!_fileProcess || _fileProcess->randomId != randomId) {
return;
}
LOG(("Export Info: File skipped."));
Assert(!_fileProcess->requests.empty());
Assert(_fileProcess->requestId != 0);
_mtp.request(base::take(_fileProcess->requestId)).cancel();
base::take(_fileProcess)->done(QString());
}
void ApiWrap::cancelExportFast() {
if (_takeoutId.has_value()) {
const auto requestId = mainRequest(MTPaccount_FinishTakeoutSession(
@@ -1591,10 +1608,11 @@ bool ApiWrap::loadMessageFileProgress(FileProgress progress) {
&& (_chatProcess->fileIndex < _chatProcess->slice->list.size()));
return _chatProcess->fileProgress(DownloadProgress{
_fileProcess->relativePath,
_chatProcess->fileIndex,
progress.ready,
progress.total });
.randomId = _fileProcess->randomId,
.path = _fileProcess->relativePath,
.itemIndex = _chatProcess->fileIndex,
.ready = progress.ready,
.total = progress.total });
}
void ApiWrap::loadMessageFileDone(const QString &relativePath) {
@@ -1740,6 +1758,8 @@ void ApiWrap::loadFile(
}
loadFilePart();
Ensures(_fileProcess->requestId != 0);
}
auto ApiWrap::prepareFileProcess(
@@ -1758,11 +1778,13 @@ auto ApiWrap::prepareFileProcess(
result->location = file.location;
result->size = file.size;
result->origin = origin;
result->randomId = openssl::RandomValue<uint64>();
return result;
}
void ApiWrap::loadFilePart() {
if (!_fileProcess
|| _fileProcess->requestId
|| _fileProcess->requests.size() >= kFileRequestsCount
|| (_fileProcess->size > 0
&& _fileProcess->offset >= _fileProcess->size)) {
@@ -1771,16 +1793,18 @@ void ApiWrap::loadFilePart() {
const auto offset = _fileProcess->offset;
_fileProcess->requests.push_back({ offset });
fileRequest(
_fileProcess->requestId = fileRequest(
_fileProcess->location,
_fileProcess->offset
).done([=](const MTPupload_File &result) {
_fileProcess->requestId = 0;
filePartDone(offset, result);
}).send();
_fileProcess->offset += kFileChunkSize;
if (_fileProcess->size > 0
&& _fileProcess->requests.size() < kFileRequestsCount) {
// Only one request at a time supported right now.
//const auto runner = _runner;
//crl::on_main([=] {
// QTimer::singleShot(kFileNextRequestDelay, [=] {
@@ -1854,6 +1878,7 @@ void ApiWrap::filePartDone(int offset, const MTPupload_File &result) {
void ApiWrap::filePartRefreshReference(int offset) {
Expects(_fileProcess != nullptr);
Expects(_fileProcess->requestId == 0);
const auto &origin = _fileProcess->origin;
if (!origin.messageId) {
@@ -1870,26 +1895,33 @@ void ApiWrap::filePartRefreshReference(int offset) {
origin.peer.c_inputPeerChannelFromMessage().vpeer(),
origin.peer.c_inputPeerChannelFromMessage().vmsg_id(),
origin.peer.c_inputPeerChannelFromMessage().vchannel_id());
mainRequest(MTPchannels_GetMessages(
_fileProcess->requestId = mainRequest(MTPchannels_GetMessages(
channel,
MTP_vector<MTPInputMessage>(
1,
MTP_inputMessageID(MTP_int(origin.messageId)))
)).fail([=](const MTP::Error &error) {
_fileProcess->requestId = 0;
filePartUnavailable();
return true;
}).done([=](const MTPmessages_Messages &result) {
_fileProcess->requestId = 0;
filePartExtractReference(offset, result);
}).send();
} else {
splitRequest(origin.split, MTPmessages_GetMessages(
MTP_vector<MTPInputMessage>(
1,
MTP_inputMessageID(MTP_int(origin.messageId)))
)).fail([=](const MTP::Error &error) {
_fileProcess->requestId = splitRequest(
origin.split,
MTPmessages_GetMessages(
MTP_vector<MTPInputMessage>(
1,
MTP_inputMessageID(MTP_int(origin.messageId)))
)
).fail([=](const MTP::Error &error) {
_fileProcess->requestId = 0;
filePartUnavailable();
return true;
}).done([=](const MTPmessages_Messages &result) {
_fileProcess->requestId = 0;
filePartExtractReference(offset, result);
}).send();
}
@@ -1899,6 +1931,7 @@ void ApiWrap::filePartExtractReference(
int offset,
const MTPmessages_Messages &result) {
Expects(_fileProcess != nullptr);
Expects(_fileProcess->requestId == 0);
result.match([&](const MTPDmessages_messagesNotModified &data) {
error("Unexpected messagesNotModified received.");
@@ -1922,10 +1955,11 @@ void ApiWrap::filePartExtractReference(
_fileProcess->location,
message.thumb().file.location);
if (refresh1 || refresh2) {
fileRequest(
_fileProcess->requestId = fileRequest(
_fileProcess->location,
offset
).done([=](const MTPupload_File &result) {
_fileProcess->requestId = 0;
filePartDone(offset, result);
}).send();
return;

View File

@@ -60,6 +60,7 @@ public:
FnMut<void(Data::File&&)> done);
struct DownloadProgress {
uint64 randomId = 0;
QString path;
int itemIndex = 0;
int ready = 0;
@@ -83,6 +84,7 @@ public:
FnMut<void()> done);
void finishExport(FnMut<void()> done);
void skipFile(uint64 randomId);
void cancelExportFast();
~ApiWrap();

View File

@@ -51,12 +51,14 @@ public:
void startExport(
const Settings &settings,
const Environment &environment);
void skipFile(uint64 randomId);
void cancelExportFast();
private:
using Step = ProcessingState::Step;
using DownloadProgress = ApiWrap::DownloadProgress;
[[nodiscard]] bool stopped() const;
void setState(State &&state);
void ioError(const QString &path);
bool ioCatchError(Output::Result result);
@@ -166,8 +168,15 @@ rpl::producer<State> ControllerObject::state() const {
});
}
bool ControllerObject::stopped() const {
return v::is<CancelledState>(_state)
|| v::is<ApiErrorState>(_state)
|| v::is<OutputErrorState>(_state)
|| v::is<FinishedState>(_state);
}
void ControllerObject::setState(State &&state) {
if (v::is<CancelledState>(_state)) {
if (stopped()) {
return;
}
_state = std::move(state);
@@ -245,6 +254,13 @@ void ControllerObject::startExport(
exportNext();
}
void ControllerObject::skipFile(uint64 randomId) {
if (stopped()) {
return;
}
_api.skipFile(randomId);
}
void ControllerObject::fillExportSteps() {
using Type = Settings::Type;
_steps.push_back(Step::Initializing);
@@ -518,6 +534,7 @@ ProcessingState ControllerObject::stateUserpics(
result.entityIndex = _userpicsWritten + progress.itemIndex;
result.entityCount = std::max(_userpicsCount, result.entityIndex);
result.bytesType = ProcessingState::FileType::Photo;
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
@@ -570,6 +587,7 @@ void ControllerObject::fillMessagesState(
result.itemIndex = _messagesWritten + progress.itemIndex;
result.itemCount = std::max(_messagesCount, result.itemIndex);
result.bytesType = ProcessingState::FileType::File; // TODO
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
@@ -643,6 +661,12 @@ void Controller::startExport(
});
}
void Controller::skipFile(uint64 randomId) {
_wrapped.with([=](Implementation &unwrapped) {
unwrapped.skipFile(randomId);
});
}
void Controller::cancelExportFast() {
LOG(("Export Info: Cancelled export."));

View File

@@ -74,6 +74,7 @@ struct ProcessingState {
int itemIndex = 0;
int itemCount = 0;
uint64 bytesRandomId = 0;
FileType bytesType = FileType::None;
QString bytesName;
int bytesLoaded = 0;
@@ -136,6 +137,7 @@ public:
void startExport(
const Settings &settings,
const Environment &environment);
void skipFile(uint64 randomId);
void cancelExportFast();
rpl::lifetime &lifetime();

View File

@@ -50,7 +50,8 @@ exportErrorLabel: FlatLabel(boxLabel) {
exportProgressDuration: 200;
exportProgressRowHeight: 30px;
exportProgressRowPadding: margins(22px, 10px, 22px, 20px);
exportProgressRowPadding: margins(22px, 10px, 22px, 10px);
exportProgressRowSkip: 10px;
exportProgressLabel: FlatLabel(boxLabel) {
textFg: windowBoldFg;
maxHeight: 20px;

View File

@@ -26,8 +26,9 @@ Content ContentFromState(
const QString &id,
const QString &label,
const QString &info,
float64 progress) {
result.rows.push_back({ id, label, info, progress });
float64 progress,
uint64 randomId = 0) {
result.rows.push_back({ id, label, info, progress, randomId });
};
const auto pushMain = [&](const QString &label) {
const auto info = (state.entityCount > 0)
@@ -56,7 +57,10 @@ Content ContentFromState(
: addPart(state.entityIndex, state.entityCount);
push("main", label, info, doneProgress + addProgress);
};
const auto pushBytes = [&](const QString &id, const QString &label) {
const auto pushBytes = [&](
const QString &id,
const QString &label,
uint64 randomId) {
if (!state.bytesCount) {
return;
}
@@ -64,7 +68,7 @@ Content ContentFromState(
const auto info = Ui::FormatDownloadText(
state.bytesLoaded,
state.bytesCount);
push(id, label, info, progress);
push(id, label, info, progress, randomId);
};
switch (state.step) {
case Step::Initializing:
@@ -80,7 +84,8 @@ Content ContentFromState(
pushMain(tr::lng_export_state_userpics(tr::now));
pushBytes(
"userpic" + QString::number(state.entityIndex),
state.bytesName);
state.bytesName,
state.bytesRandomId);
break;
case Step::Contacts:
pushMain(tr::lng_export_option_contacts(tr::now));
@@ -117,7 +122,8 @@ Content ContentFromState(
+ QString::number(state.entityIndex)
+ '_'
+ QString::number(state.itemIndex)),
state.bytesName);
state.bytesName,
state.bytesRandomId);
break;
default: Unexpected("Step in ContentFromState.");
}

View File

@@ -22,6 +22,7 @@ struct Content {
QString label;
QString info;
float64 progress = 0.;
uint64 randomId = 0;
};
std::vector<Row> rows;

View File

@@ -299,6 +299,11 @@ void PanelController::showProgress() {
ContentFromState(_settings.get(), ProcessingState())
) | rpl::then(progressState()));
progress->skipFileClicks(
) | rpl::start_with_next([=](uint64 randomId) {
_process->skipFile(randomId);
}, progress->lifetime());
progress->cancelClicks(
) | rpl::start_with_next([=] {
stopWithConfirmation();

View File

@@ -18,6 +18,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Export {
namespace View {
namespace {
constexpr auto kShowSkipFileTimeout = 5 * crl::time(1000);
} // namespace
class ProgressWidget::Row : public Ui::RpWidget {
public:
@@ -235,13 +240,26 @@ ProgressWidget::ProgressWidget(
QWidget *parent,
rpl::producer<Content> content)
: RpWidget(parent)
, _body(this) {
, _body(this)
, _fileShowSkipTimer([=] { _skipFile->show(anim::type::normal); }) {
widthValue(
) | rpl::start_with_next([=](int width) {
_body->resizeToWidth(width);
_body->moveToLeft(0, 0);
}, _body->lifetime());
auto skipFileWrap = _body->add(object_ptr<Ui::FixedHeightWidget>(
_body.data(),
st::defaultLinkButton.font->height + st::exportProgressRowSkip));
_skipFile = base::make_unique_q<Ui::FadeWrap<Ui::LinkButton>>(
skipFileWrap,
object_ptr<Ui::LinkButton>(
this,
tr::lng_export_skip_file(tr::now),
st::defaultLinkButton));
_skipFile->hide(anim::type::instant);
_skipFile->moveToLeft(st::exportProgressRowPadding.left(), 0);
_about = _body->add(
object_ptr<Ui::FlatLabel>(
this,
@@ -262,6 +280,11 @@ ProgressWidget::ProgressWidget(
setupBottomButton(_cancel.get());
}
rpl::producer<uint64> ProgressWidget::skipFileClicks() const {
return _skipFile->entity()->clicks(
) | rpl::map([=] { return _fileRandomId; });
}
rpl::producer<> ProgressWidget::cancelClicks() const {
return _cancel
? (_cancel->clicks() | rpl::to_empty)
@@ -294,14 +317,32 @@ void ProgressWidget::updateState(Content &&content) {
if (index < _rows.size()) {
_rows[index]->updateData(std::move(row));
} else {
if (index > 0) {
_body->insert(
index * 2 - 1,
object_ptr<Ui::FixedHeightWidget>(
this,
st::exportProgressRowSkip));
}
_rows.push_back(_body->insert(
index,
index * 2,
object_ptr<Row>(this, std::move(row)),
st::exportProgressRowPadding));
_rows.back()->show();
}
++index;
}
const auto fileRandomId = !content.rows.empty()
? content.rows.back().randomId
: uint64(0);
if (_fileRandomId != fileRandomId) {
_fileShowSkipTimer.cancel();
_skipFile->hide(anim::type::normal);
_fileRandomId = fileRandomId;
if (_fileRandomId) {
_fileShowSkipTimer.callOnce(kShowSkipFileTimeout);
}
}
for (const auto count = _rows.size(); index != count; ++index) {
_rows[index]->updateData(Content::Row());
}
@@ -312,6 +353,8 @@ void ProgressWidget::updateState(Content &&content) {
void ProgressWidget::showDone() {
_cancel = nullptr;
_skipFile->hide(anim::type::instant);
_fileShowSkipTimer.cancel();
_about->setText(tr::lng_export_about_done(tr::now));
_done = base::make_unique_q<Ui::RoundButton>(
this,

View File

@@ -10,11 +10,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "export/view/export_view_content.h"
#include "base/object_ptr.h"
#include "base/timer.h"
namespace Ui {
class VerticalLayout;
class RoundButton;
class FlatLabel;
class LinkButton;
template <typename Widget>
class FadeWrap;
} // namespace Ui
namespace Export {
@@ -26,6 +30,7 @@ public:
QWidget *parent,
rpl::producer<Content> content);
rpl::producer<uint64> skipFileClicks() const;
rpl::producer<> cancelClicks() const;
rpl::producer<> doneClicks() const;
@@ -42,11 +47,15 @@ private:
object_ptr<Ui::VerticalLayout> _body;
std::vector<not_null<Row*>> _rows;
base::unique_qptr<Ui::FadeWrap<Ui::LinkButton>> _skipFile;
QPointer<Ui::FlatLabel> _about;
base::unique_qptr<Ui::RoundButton> _cancel;
base::unique_qptr<Ui::RoundButton> _done;
rpl::event_stream<> _doneClicks;
uint64 _fileRandomId = 0;
base::Timer _fileShowSkipTimer;
};
} // namespace View

View File

@@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/media/history_view_media.h"
#include "payments/payments_checkout_process.h"
#include "data/data_session.h"
#include "styles/style_chat.h"
@@ -121,7 +122,7 @@ void activateBotCommand(
} break;
case ButtonType::Buy: {
Ui::show(Box<InformBox>(tr::lng_payments_not_supported(tr::now)));
Payments::CheckoutProcess::Start(msg);
} break;
case ButtonType::Url: {
@@ -260,7 +261,7 @@ void showChatsList(not_null<Main::Session*> session) {
if (const auto m = CheckMainWidget(session)) {
m->ui_showPeerHistory(
0,
Window::SectionShow::Way::ClearStack,
::Window::SectionShow::Way::ClearStack,
0);
}
}
@@ -277,7 +278,7 @@ void showPeerHistory(not_null<const PeerData*> peer, MsgId msgId) {
if (const auto m = CheckMainWidget(&peer->session())) {
m->ui_showPeerHistory(
peer->id,
Window::SectionShow::Way::ClearStack,
::Window::SectionShow::Way::ClearStack,
msgId);
}
}

View File

@@ -432,8 +432,13 @@ void InnerWidget::requestAdmins() {
auto filtered = (
list
) | ranges::views::transform([&](const MTPChannelParticipant &p) {
const auto userId = p.match([](const auto &data) {
return data.vuser_id().v;
const auto participantId = p.match([](
const MTPDchannelParticipantBanned &data) {
return peerFromMTP(data.vpeer());
}, [](const MTPDchannelParticipantLeft &data) {
return peerFromMTP(data.vpeer());
}, [](const auto &data) {
return peerFromUser(data.vuser_id());
});
const auto canEdit = p.match([](
const MTPDchannelParticipantAdmin &data) {
@@ -441,10 +446,13 @@ void InnerWidget::requestAdmins() {
}, [](const auto &) {
return false;
});
return std::make_pair(userId, canEdit);
return std::make_pair(participantId, canEdit);
}) | ranges::views::transform([&](auto &&pair) {
return std::make_pair(
session().data().userLoaded(pair.first),
(peerIsUser(pair.first)
? session().data().userLoaded(
peerToUser(pair.first))
: nullptr),
pair.second);
}) | ranges::views::filter([&](auto &&pair) {
return (pair.first != nullptr);
@@ -1304,11 +1312,11 @@ void InnerWidget::suggestRestrictUser(not_null<UserData*> user) {
Ui::LayerOption::KeepOther);
};
if (base::contains(_admins, user)) {
editRestrictions(true, MTP_chatBannedRights(MTP_flags(0), MTP_int(0)));
editRestrictions(true, ChannelData::EmptyRestrictedRights(user));
} else {
_api.request(MTPchannels_GetParticipant(
_channel->inputChannel,
user->inputUser
user->input
)).done([=](const MTPchannels_ChannelParticipant &result) {
Expects(result.type() == mtpc_channels_channelParticipant);
@@ -1321,15 +1329,11 @@ void InnerWidget::suggestRestrictUser(not_null<UserData*> user) {
} else {
auto hasAdminRights = (type == mtpc_channelParticipantAdmin)
|| (type == mtpc_channelParticipantCreator);
auto bannedRights = MTP_chatBannedRights(
MTP_flags(0),
MTP_int(0));
auto bannedRights = ChannelData::EmptyRestrictedRights(user);
editRestrictions(hasAdminRights, bannedRights);
}
}).fail([=](const MTP::Error &error) {
auto bannedRights = MTP_chatBannedRights(
MTP_flags(0),
MTP_int(0));
auto bannedRights = ChannelData::EmptyRestrictedRights(user);
editRestrictions(false, bannedRights);
}).send();
}
@@ -1352,8 +1356,7 @@ void InnerWidget::restrictUser(
}
void InnerWidget::restrictUserDone(not_null<UserData*> user, const MTPChatBannedRights &rights) {
Expects(rights.type() == mtpc_chatBannedRights);
if (rights.c_chatBannedRights().vflags().v) {
if (Data::ChatBannedRightsFlags(rights)) {
_admins.erase(std::remove(_admins.begin(), _admins.end(), user), _admins.end());
_adminsCanEdit.erase(std::remove(_adminsCanEdit.begin(), _adminsCanEdit.end(), user), _adminsCanEdit.end());
}

View File

@@ -204,14 +204,11 @@ TextWithEntities GenerateAdminChangeText(
QString GenerateBannedChangeText(
const MTPChatBannedRights *newRights,
const MTPChatBannedRights *prevRights) {
Expects(!newRights || newRights->type() == mtpc_chatBannedRights);
Expects(!prevRights || prevRights->type() == mtpc_chatBannedRights);
using Flag = MTPDchatBannedRights::Flag;
using Flags = MTPDchatBannedRights::Flags;
auto newFlags = newRights ? newRights->c_chatBannedRights().vflags().v : Flags(0);
auto prevFlags = prevRights ? prevRights->c_chatBannedRights().vflags().v : Flags(0);
auto newFlags = newRights ? Data::ChatBannedRightsFlags(*newRights) : Flags(0);
auto prevFlags = prevRights ? Data::ChatBannedRightsFlags(*prevRights) : Flags(0);
static auto phraseMap = std::map<Flags, tr::phrase<>>{
{ Flag::f_view_messages, tr::lng_admin_log_banned_view_messages },
{ Flag::f_send_messages, tr::lng_admin_log_banned_send_messages },
@@ -230,19 +227,21 @@ QString GenerateBannedChangeText(
}
TextWithEntities GenerateBannedChangeText(
PeerId participantId,
const TextWithEntities &user,
const MTPChatBannedRights *newRights,
const MTPChatBannedRights *prevRights) {
Expects(!newRights || newRights->type() == mtpc_chatBannedRights);
using Flag = MTPDchatBannedRights::Flag;
using Flags = MTPDchatBannedRights::Flags;
auto newFlags = newRights ? newRights->c_chatBannedRights().vflags().v : Flags(0);
auto newUntil = newRights ? newRights->c_chatBannedRights().vuntil_date().v : TimeId(0);
auto newFlags = newRights ? Data::ChatBannedRightsFlags(*newRights) : Flags(0);
auto newUntil = newRights ? Data::ChatBannedRightsUntilDate(*newRights) : TimeId(0);
auto prevFlags = prevRights ? Data::ChatBannedRightsFlags(*prevRights) : Flags(0);
auto indefinitely = ChannelData::IsRestrictedForever(newUntil);
if (newFlags & Flag::f_view_messages) {
return tr::lng_admin_log_banned(tr::now, lt_user, user, Ui::Text::WithEntities);
} else if (newFlags == 0 && (prevFlags & Flag::f_view_messages) && !peerIsUser(participantId)) {
return tr::lng_admin_log_unbanned(tr::now, lt_user, user, Ui::Text::WithEntities);
}
auto untilText = indefinitely
? tr::lng_admin_log_restricted_forever(tr::now)
@@ -344,21 +343,23 @@ TextWithEntities GenerateInviteLinkChangeText(
return result;
};
auto GenerateUserString(
auto GenerateParticipantString(
not_null<Main::Session*> session,
MTPint userId) {
PeerId participantId) {
// User name in "User name (@username)" format with entities.
auto user = session->data().user(userId.v);
auto name = TextWithEntities { user->name };
auto entityData = QString::number(user->id)
+ '.'
+ QString::number(user->accessHash());
name.entities.push_back({
EntityType::MentionName,
0,
name.text.size(),
entityData });
auto username = user->userName();
auto peer = session->data().peer(participantId);
auto name = TextWithEntities { peer->name };
if (const auto user = peer->asUser()) {
auto entityData = QString::number(user->id)
+ '.'
+ QString::number(user->accessHash());
name.entities.push_back({
EntityType::MentionName,
0,
name.text.size(),
entityData });
}
auto username = peer->userName();
if (username.isEmpty()) {
return name;
}
@@ -381,32 +382,10 @@ auto GenerateParticipantChangeTextInner(
const MTPChannelParticipant &participant,
const MTPChannelParticipant *oldParticipant) {
const auto oldType = oldParticipant ? oldParticipant->type() : 0;
return participant.match([&](const MTPDchannelParticipantCreator &data) {
// No valid string here :(
return tr::lng_admin_log_transferred(
tr::now,
lt_user,
GenerateUserString(&channel->session(), data.vuser_id()),
Ui::Text::WithEntities);
}, [&](const MTPDchannelParticipantAdmin &data) {
auto user = GenerateUserString(&channel->session(), data.vuser_id());
return GenerateAdminChangeText(
channel,
user,
&data.vadmin_rights(),
(oldType == mtpc_channelParticipantAdmin)
? &oldParticipant->c_channelParticipantAdmin().vadmin_rights()
: nullptr);
}, [&](const MTPDchannelParticipantBanned &data) {
auto user = GenerateUserString(&channel->session(), data.vuser_id());
return GenerateBannedChangeText(
user,
&data.vbanned_rights(),
(oldType == mtpc_channelParticipantBanned)
? &oldParticipant->c_channelParticipantBanned().vbanned_rights()
: nullptr);
}, [&](const auto &data) {
auto user = GenerateUserString(&channel->session(), data.vuser_id());
const auto generateOther = [&](PeerId participantId) {
auto user = GenerateParticipantString(
&channel->session(),
participantId);
if (oldType == mtpc_channelParticipantAdmin) {
return GenerateAdminChangeText(
channel,
@@ -415,11 +394,49 @@ auto GenerateParticipantChangeTextInner(
&oldParticipant->c_channelParticipantAdmin().vadmin_rights());
} else if (oldType == mtpc_channelParticipantBanned) {
return GenerateBannedChangeText(
participantId,
user,
nullptr,
&oldParticipant->c_channelParticipantBanned().vbanned_rights());
}
return tr::lng_admin_log_invited(tr::now, lt_user, user, Ui::Text::WithEntities);
};
return participant.match([&](const MTPDchannelParticipantCreator &data) {
// No valid string here :(
return tr::lng_admin_log_transferred(
tr::now,
lt_user,
GenerateParticipantString(
&channel->session(),
peerFromUser(data.vuser_id())),
Ui::Text::WithEntities);
}, [&](const MTPDchannelParticipantAdmin &data) {
const auto user = GenerateParticipantString(
&channel->session(),
peerFromUser(data.vuser_id()));
return GenerateAdminChangeText(
channel,
user,
&data.vadmin_rights(),
(oldType == mtpc_channelParticipantAdmin
? &oldParticipant->c_channelParticipantAdmin().vadmin_rights()
: nullptr));
}, [&](const MTPDchannelParticipantBanned &data) {
const auto participantId = peerFromMTP(data.vpeer());
const auto user = GenerateParticipantString(
&channel->session(),
participantId);
return GenerateBannedChangeText(
participantId,
user,
&data.vbanned_rights(),
(oldType == mtpc_channelParticipantBanned
? &oldParticipant->c_channelParticipantBanned().vbanned_rights()
: nullptr));
}, [&](const MTPDchannelParticipantLeft &data) {
return generateOther(peerFromMTP(data.vpeer()));
}, [&](const auto &data) {
return generateOther(peerFromUser(data.vuser_id()));
});
}

View File

@@ -701,9 +701,11 @@ bool HistoryItem::suggestReport() const {
}
bool HistoryItem::suggestBanReport() const {
auto channel = history()->peer->asChannel();
auto fromUser = from()->asUser();
if (!channel || !fromUser || !channel->canRestrictUser(fromUser)) {
const auto channel = history()->peer->asChannel();
const auto fromUser = from()->asUser();
if (!channel
|| !fromUser
|| !channel->canRestrictParticipant(fromUser)) {
return false;
}
return !isPost() && !out();

View File

@@ -857,7 +857,12 @@ MsgId HistoryMessage::computeRepliesInboxReadTillFull() const {
? history()->owner().historyLoaded(
peerFromChannel(views->commentsMegagroupId))
: history().get();
return group ? std::max(local, group->inboxReadTillId()) : local;
if (const auto megagroup = group->peer->asChannel()) {
if (megagroup->amIn()) {
return std::max(local, group->inboxReadTillId());
}
}
return local;
}
MsgId HistoryMessage::repliesOutboxReadTill() const {
@@ -891,7 +896,12 @@ MsgId HistoryMessage::computeRepliesOutboxReadTillFull() const {
? history()->owner().historyLoaded(
peerFromChannel(views->commentsMegagroupId))
: history().get();
return group ? std::max(local, group->outboxReadTillId()) : local;
if (const auto megagroup = group->peer->asChannel()) {
if (megagroup->amIn()) {
return std::max(local, group->outboxReadTillId());
}
}
return local;
}
void HistoryMessage::setRepliesMaxId(MsgId maxId) {

View File

@@ -958,7 +958,9 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) {
UpdateComponents(HistoryServicePayment::Bit());
const auto amount = data.vtotal_amount().v;
const auto currency = qs(data.vcurrency());
Get<HistoryServicePayment>()->amount = Ui::FillAmountAndCurrency(amount, currency);
Get<HistoryServicePayment>()->amount = Ui::FillAmountAndCurrency(
amount,
currency);
} else if (message.vaction().type() == mtpc_messageActionGroupCall) {
const auto &data = message.vaction().c_messageActionGroupCall();
if (data.vduration()) {

View File

@@ -46,12 +46,24 @@ PhoneWidget::PhoneWidget(
, _code(this, st::introCountryCode)
, _phone(this, st::introPhone)
, _checkRequestTimer([=] { checkRequest(); }) {
connect(_phone, SIGNAL(voidBackspace(QKeyEvent*)), _code, SLOT(startErasing(QKeyEvent*)));
connect(_country, SIGNAL(codeChanged(const QString &)), _code, SLOT(codeSelected(const QString &)));
connect(_code, SIGNAL(codeChanged(const QString &)), _country, SLOT(onChooseCode(const QString &)));
connect(_code, SIGNAL(codeChanged(const QString &)), _phone, SLOT(onChooseCode(const QString &)));
_phone->frontBackspaceEvent(
) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
_code->startErasing(e);
}, _code->lifetime());
connect(_country, &CountryInput::codeChanged, [=](const QString &code) {
_code->codeSelected(code);
});
_code->codeChanged(
) | rpl::start_with_next([=](const QString &code) {
_country->onChooseCode(code);
_phone->chooseCode(code);
}, _code->lifetime());
connect(_country, SIGNAL(codeChanged(const QString &)), _phone, SLOT(onChooseCode(const QString &)));
connect(_code, SIGNAL(addedToNumber(const QString &)), _phone, SLOT(addedToNumber(const QString &)));
_code->addedToNumber(
) | rpl::start_with_next([=](const QString &added) {
_phone->addedToNumber(added);
}, _phone->lifetime());
connect(_phone, &Ui::PhonePartInput::changed, [=] { phoneChanged(); });
connect(_code, &Ui::CountryCodeInput::changed, [=] { phoneChanged(); });
@@ -61,8 +73,8 @@ PhoneWidget::PhoneWidget(
setErrorCentered(true);
setupQrLogin();
if (!_country->onChooseCountry(getData()->country)) {
_country->onChooseCountry(qsl("US"));
if (!_country->chooseCountry(getData()->country)) {
_country->chooseCountry(qsl("US"));
}
_changed = false;
}
@@ -251,7 +263,7 @@ QString PhoneWidget::fullNumber() const {
}
void PhoneWidget::selectCountry(const QString &country) {
_country->onChooseCountry(country);
_country->chooseCountry(country);
}
void PhoneWidget::setInnerFocus() {

View File

@@ -486,7 +486,6 @@ void OverlayWidget::moveEvent(QMoveEvent *e) {
DEBUG_LOG(("Viewer Pos: Moved to %1, %2")
.arg(newPos.x())
.arg(newPos.y()));
moveToScreen();
OverlayParent::moveEvent(e);
}

View File

@@ -101,7 +101,7 @@ void Panel::showBox(
}
void Panel::showToast(const QString &text) {
_widget->showToast(text);
_widget->showToast({ text });
}
Panel::~Panel() = default;

View File

@@ -9,10 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "passport/passport_panel_edit_document.h"
#include "passport/passport_panel_details_row.h"
#include "passport/passport_panel_edit_contact.h"
#include "passport/passport_panel_edit_scans.h"
#include "passport/passport_panel.h"
#include "passport/ui/passport_details_row.h"
#include "base/openssl_help.h"
#include "base/unixtime.h"
#include "boxes/passcode_box.h"
@@ -212,7 +212,7 @@ EditDocumentScheme GetDocumentScheme(
result.rows = {
{
ValueClass::Fields,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("first_name"),
tr::lng_passport_first_name(tr::now),
NameValidate,
@@ -221,7 +221,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("middle_name"),
tr::lng_passport_middle_name(tr::now),
NameOrEmptyValidate,
@@ -231,7 +231,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("last_name"),
tr::lng_passport_last_name(tr::now),
NameValidate,
@@ -241,7 +241,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Date,
Ui::PanelDetailsType::Date,
qsl("birth_date"),
tr::lng_passport_birth_date(tr::now),
DateValidate,
@@ -249,7 +249,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Gender,
Ui::PanelDetailsType::Gender,
qsl("gender"),
tr::lng_passport_gender(tr::now),
GenderValidate,
@@ -257,7 +257,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Country,
Ui::PanelDetailsType::Country,
qsl("country_code"),
tr::lng_passport_country(tr::now),
CountryValidate,
@@ -265,7 +265,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Country,
Ui::PanelDetailsType::Country,
qsl("residence_country_code"),
tr::lng_passport_residence_country(tr::now),
CountryValidate,
@@ -273,7 +273,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Scans,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("document_no"),
tr::lng_passport_document_number(tr::now),
DocumentValidate,
@@ -282,7 +282,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Scans,
PanelDetailsType::Date,
Ui::PanelDetailsType::Date,
qsl("expiry_date"),
tr::lng_passport_expiry_date(tr::now),
DateOrEmptyValidate,
@@ -344,7 +344,7 @@ EditDocumentScheme GetDocumentScheme(
auto additional = std::initializer_list<Row>{
{
ValueClass::Additional,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("first_name_native"),
tr::lng_passport_first_name(tr::now),
NativeNameValidate,
@@ -355,7 +355,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Additional,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("middle_name_native"),
tr::lng_passport_middle_name(tr::now),
NativeNameOrEmptyValidate,
@@ -366,7 +366,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Additional,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("last_name_native"),
tr::lng_passport_last_name(tr::now),
NativeNameValidate,
@@ -411,7 +411,7 @@ EditDocumentScheme GetDocumentScheme(
result.rows = {
{
ValueClass::Fields,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("street_line1"),
tr::lng_passport_street(tr::now),
StreetValidate,
@@ -420,7 +420,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("street_line2"),
tr::lng_passport_street(tr::now),
DontValidate,
@@ -429,7 +429,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("city"),
tr::lng_passport_city(tr::now),
CityValidate,
@@ -438,7 +438,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Text,
Ui::PanelDetailsType::Text,
qsl("state"),
tr::lng_passport_state(tr::now),
DontValidate,
@@ -447,7 +447,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Country,
Ui::PanelDetailsType::Country,
qsl("country_code"),
tr::lng_passport_country(tr::now),
CountryValidate,
@@ -455,7 +455,7 @@ EditDocumentScheme GetDocumentScheme(
},
{
ValueClass::Fields,
PanelDetailsType::Postcode,
Ui::PanelDetailsType::Postcode,
qsl("post_code"),
tr::lng_passport_postcode(tr::now),
PostcodeValidate,

View File

@@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_panel_edit_contact.h"
#include "passport/passport_panel_controller.h"
#include "passport/passport_panel_details_row.h"
#include "passport/ui/passport_details_row.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
@@ -278,7 +278,8 @@ void PanelEditContact::setupControls(
wrap.data(),
fieldStyle,
std::move(fieldPlaceholder),
ExtractPhonePrefix(_controller->bot()->session().user()->phone()),
Ui::ExtractPhonePrefix(
_controller->bot()->session().user()->phone()),
data);
} else {
_field = Ui::CreateChild<Ui::MaskedInputField>(

View File

@@ -8,8 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_panel_edit_document.h"
#include "passport/passport_panel_controller.h"
#include "passport/passport_panel_details_row.h"
#include "passport/passport_panel_edit_scans.h"
#include "passport/ui/passport_details_row.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/labels.h"
@@ -19,6 +19,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "data/data_countries.h"
#include "data/data_user.h" // ->bot()->session()
#include "main/main_session.h" // ->session().user()
#include "ui/text/text_utilities.h" // Ui::Text::ToUpper
#include "boxes/abstract_box.h"
#include "boxes/confirm_box.h"
@@ -363,7 +366,7 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
const ValueMap &fields) {
accumulate_max(
maxLabelWidth,
PanelDetailsRow::LabelWidth(row.label));
Ui::PanelDetailsRow::LabelWidth(row.label));
});
if (maxLabelWidth > 0) {
if (error && !error->isEmpty()) {
@@ -513,12 +516,20 @@ void PanelEditDocument::createDetailsRow(
};
const auto current = valueOrEmpty(fields, row.key);
const auto showBox = [controller = _controller](
object_ptr<Ui::BoxContent> box) {
controller->show(std::move(box));
};
const auto isoByPhone = Data::CountryISO2ByPhone(
_controller->bot()->session().user()->phone());
const auto [it, ok] = _details.emplace(
i,
container->add(PanelDetailsRow::Create(
container->add(Ui::PanelDetailsRow::Create(
container,
showBox,
isoByPhone,
row.inputType,
_controller,
row.label,
maxLabelWidth,
current.text,
@@ -537,7 +548,7 @@ void PanelEditDocument::createDetailsRow(
}, it->second->lifetime());
}
not_null<PanelDetailsRow*> PanelEditDocument::findRow(
not_null<Ui::PanelDetailsRow*> PanelEditDocument::findRow(
const QString &key) const {
for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {
const auto &row = _scheme.rows[i];
@@ -636,7 +647,7 @@ bool PanelEditDocument::validate() {
_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
error = firsttop.y();
}
auto first = QPointer<PanelDetailsRow>();
auto first = QPointer<Ui::PanelDetailsRow>();
for (const auto &[i, field] : ranges::views::reverse(_details)) {
const auto &row = _scheme.rows[i];
if (row.valueClass == Scheme::ValueClass::Additional

View File

@@ -24,15 +24,19 @@ template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Passport::Ui {
using namespace ::Ui;
enum class PanelDetailsType;
class PanelDetailsRow;
} // namespace Passport::Ui
namespace Passport {
class PanelController;
struct ValueMap;
struct ScanInfo;
class EditScans;
class PanelDetailsRow;
enum class FileType;
enum class PanelDetailsType;
struct ScanListData;
struct EditDocumentScheme {
@@ -50,7 +54,7 @@ struct EditDocumentScheme {
using Validator = Fn<std::optional<QString>(const QString &value)>;
using Formatter = Fn<QString(const QString &value)>;
ValueClass valueClass = ValueClass::Fields;
PanelDetailsType inputType = PanelDetailsType();
Ui::PanelDetailsType inputType = Ui::PanelDetailsType();
QString key;
QString label;
Validator error;
@@ -140,7 +144,7 @@ private:
const Scheme::Row &row,
const ValueMap &fields,
int maxLabelWidth);
not_null<PanelDetailsRow*> findRow(const QString &key) const;
not_null<Ui::PanelDetailsRow*> findRow(const QString &key) const;
not_null<PanelController*> _controller;
Scheme _scheme;
@@ -151,7 +155,7 @@ private:
QPointer<EditScans> _editScans;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;
std::map<int, QPointer<PanelDetailsRow>> _details;
std::map<int, QPointer<Ui::PanelDetailsRow>> _details;
bool _fieldsChanged = false;
bool _additionalShown = false;

View File

@@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_panel_edit_scans.h"
#include "passport/passport_panel_controller.h"
#include "passport/passport_panel_details_row.h"
#include "passport/ui/passport_details_row.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/box_content_divider.h"

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_panel_form.h"
#include "passport/passport_panel_controller.h"
#include "passport/ui/passport_form_row.h"
#include "lang/lang_keys.h"
#include "boxes/abstract_box.h"
#include "core/click_handler_types.h"
@@ -30,145 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Passport {
class PanelForm::Row : public Ui::RippleButton {
public:
explicit Row(QWidget *parent);
void updateContent(
const QString &title,
const QString &description,
bool ready,
bool error,
anim::type animated);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
int countAvailableWidth() const;
int countAvailableWidth(int newWidth) const;
Ui::Text::String _title;
Ui::Text::String _description;
int _titleHeight = 0;
int _descriptionHeight = 0;
bool _ready = false;
bool _error = false;
Ui::Animations::Simple _errorAnimation;
};
PanelForm::Row::Row(QWidget *parent)
: RippleButton(parent, st::passportRowRipple)
, _title(st::boxWideWidth / 2)
, _description(st::boxWideWidth / 2) {
}
void PanelForm::Row::updateContent(
const QString &title,
const QString &description,
bool ready,
bool error,
anim::type animated) {
_title.setText(
st::semiboldTextStyle,
title,
Ui::NameTextOptions());
_description.setText(
st::defaultTextStyle,
description,
TextParseOptions {
TextParseMultiline,
0,
0,
Qt::LayoutDirectionAuto
});
_ready = ready && !error;
if (_error != error) {
_error = error;
if (animated == anim::type::instant) {
_errorAnimation.stop();
} else {
_errorAnimation.start(
[=] { update(); },
_error ? 0. : 1.,
_error ? 1. : 0.,
st::fadeWrapDuration);
}
}
resizeToWidth(width());
update();
}
int PanelForm::Row::resizeGetHeight(int newWidth) {
const auto availableWidth = countAvailableWidth(newWidth);
_titleHeight = _title.countHeight(availableWidth);
_descriptionHeight = _description.countHeight(availableWidth);
const auto result = st::passportRowPadding.top()
+ _titleHeight
+ st::passportRowSkip
+ _descriptionHeight
+ st::passportRowPadding.bottom();
return result;
}
int PanelForm::Row::countAvailableWidth(int newWidth) const {
return newWidth
- st::passportRowPadding.left()
- st::passportRowPadding.right()
- (_ready
? st::passportRowReadyIcon
: st::passportRowEmptyIcon).width()
- st::passportRowIconSkip;
}
int PanelForm::Row::countAvailableWidth() const {
return countAvailableWidth(width());
}
void PanelForm::Row::paintEvent(QPaintEvent *e) {
Painter p(this);
paintRipple(p, 0, 0);
const auto left = st::passportRowPadding.left();
const auto availableWidth = countAvailableWidth();
auto top = st::passportRowPadding.top();
const auto error = _errorAnimation.value(_error ? 1. : 0.);
p.setPen(st::passportRowTitleFg);
_title.drawLeft(p, left, top, availableWidth, width());
top += _titleHeight + st::passportRowSkip;
p.setPen(anim::pen(
st::passportRowDescriptionFg,
st::boxTextFgError,
error));
_description.drawLeft(p, left, top, availableWidth, width());
top += _descriptionHeight + st::passportRowPadding.bottom();
const auto &icon = _ready
? st::passportRowReadyIcon
: st::passportRowEmptyIcon;
if (error > 0. && !_ready) {
icon.paint(
p,
width() - st::passportRowPadding.right() - icon.width(),
(height() - icon.height()) / 2,
width(),
anim::color(st::menuIconFgOver, st::boxTextFgError, error));
} else {
icon.paint(
p,
width() - st::passportRowPadding.right() - icon.width(),
(height() - icon.height()) / 2,
width());
}
}
PanelForm::PanelForm(
QWidget *parent,
not_null<PanelController*> controller)

View File

@@ -19,6 +19,11 @@ class FlatLabel;
class UserpicButton;
} // namespace Ui
namespace Passport::Ui {
using namespace ::Ui;
class FormRow;
} // namespace Passport::Ui
namespace Passport {
class PanelController;
@@ -33,7 +38,7 @@ protected:
void resizeEvent(QResizeEvent *e) override;
private:
class Row;
using Row = Ui::FormRow;
void setupControls();
not_null<Ui::RpWidget*> setupContent();

View File

@@ -5,9 +5,8 @@ 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 "passport/passport_panel_details_row.h"
#include "passport/ui/passport_details_row.h"
#include "passport/passport_panel_controller.h"
#include "lang/lang_keys.h"
#include "base/platform/base_platform_info.h"
#include "ui/widgets/input_fields.h"
@@ -15,17 +14,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/countryinput.h"
#include "main/main_session.h"
#include "data/data_user.h"
#include "ui/layers/box_content.h"
#include "ui/boxes/country_select_box.h"
#include "data/data_countries.h"
#include "styles/style_layers.h"
#include "styles/style_passport.h"
namespace Passport {
#include <QtCore/QRegularExpression>
namespace Passport::Ui {
namespace {
class PostcodeInput : public Ui::MaskedInputField {
class PostcodeInput : public MaskedInputField {
public:
PostcodeInput(
QWidget *parent,
@@ -103,7 +103,8 @@ class CountryRow : public PanelDetailsRow {
public:
CountryRow(
QWidget *parent,
not_null<PanelController*> controller,
Fn<void(object_ptr<BoxContent>)> showBox,
const QString &defaultCountry,
const QString &label,
int maxLabelWidth,
const QString &value);
@@ -121,15 +122,16 @@ private:
void toggleError(bool shown);
void errorAnimationCallback();
not_null<PanelController*> _controller;
object_ptr<Ui::LinkButton> _link;
QString _defaultCountry;
Fn<void(object_ptr<BoxContent>)> _showBox;
object_ptr<LinkButton> _link;
rpl::variable<QString> _value;
bool _errorShown = false;
Ui::Animations::Simple _errorAnimation;
Animations::Simple _errorAnimation;
};
class DateInput final : public Ui::MaskedInputField {
class DateInput final : public MaskedInputField {
public:
using MaskedInputField::MaskedInputField;
@@ -191,21 +193,21 @@ private:
int number(const object_ptr<DateInput> &field) const;
object_ptr<DateInput> _day;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator1;
object_ptr<PaddingWrap<FlatLabel>> _separator1;
object_ptr<DateInput> _month;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator2;
object_ptr<PaddingWrap<FlatLabel>> _separator2;
object_ptr<DateInput> _year;
rpl::variable<QString> _value;
style::cursor _cursor = style::cur_default;
Ui::Animations::Simple _a_borderShown;
Animations::Simple _a_borderShown;
int _borderAnimationStart = 0;
Ui::Animations::Simple _a_borderOpacity;
Animations::Simple _a_borderOpacity;
bool _borderVisible = false;
Ui::Animations::Simple _a_error;
Animations::Simple _a_error;
bool _error = false;
Ui::Animations::Simple _a_focused;
Animations::Simple _a_focused;
bool _focused = false;
};
@@ -238,18 +240,18 @@ private:
void hideGenderError();
void errorAnimationCallback();
std::unique_ptr<Ui::AbstractCheckView> createRadioView(
Ui::RadioView* &weak) const;
std::unique_ptr<AbstractCheckView> createRadioView(
RadioView* &weak) const;
std::shared_ptr<Ui::RadioenumGroup<Gender>> _group;
Ui::RadioView *_maleRadio = nullptr;
Ui::RadioView *_femaleRadio = nullptr;
object_ptr<Ui::Radioenum<Gender>> _male;
object_ptr<Ui::Radioenum<Gender>> _female;
std::shared_ptr<RadioenumGroup<Gender>> _group;
RadioView *_maleRadio = nullptr;
RadioView *_femaleRadio = nullptr;
object_ptr<Radioenum<Gender>> _male;
object_ptr<Radioenum<Gender>> _female;
rpl::variable<QString> _value;
bool _errorShown = false;
Ui::Animations::Simple _errorAnimation;
Animations::Simple _errorAnimation;
};
@@ -308,12 +310,14 @@ QString CountryString(const QString &code) {
CountryRow::CountryRow(
QWidget *parent,
not_null<PanelController*> controller,
Fn<void(object_ptr<BoxContent>)> showBox,
const QString &defaultCountry,
const QString &label,
int maxLabelWidth,
const QString &value)
: PanelDetailsRow(parent, label, maxLabelWidth)
, _controller(controller)
, _defaultCountry(defaultCountry)
, _showBox(std::move(showBox))
, _link(this, CountryString(value), st::boxLinkButton)
, _value(value) {
_value.changes(
@@ -380,20 +384,23 @@ void CountryRow::errorAnimationCallback() {
void CountryRow::chooseCountry() {
const auto top = _value.current();
const auto name = Data::CountryNameByISO2(top);
const auto isoByPhone = Data::CountryISO2ByPhone(
_controller->bot()->session().user()->phone());
const auto box = _controller->show(Box<CountrySelectBox>(!name.isEmpty()
const auto country = !name.isEmpty()
? top
: !isoByPhone.isEmpty()
? isoByPhone
: Platform::SystemCountry(),
CountrySelectBox::Type::Countries));
connect(box, &CountrySelectBox::countryChosen, this, [=](QString iso) {
: !_defaultCountry.isEmpty()
? _defaultCountry
: Platform::SystemCountry();
auto box = Box<CountrySelectBox>(
country,
CountrySelectBox::Type::Countries);
const auto raw = box.data();
raw->countryChosen(
) | rpl::start_with_next([=](QString iso) {
_value = iso;
_link->setText(CountryString(iso));
hideCountryError();
box->closeBox();
});
raw->closeBox();
}, lifetime());
_showBox(std::move(box));
}
QDate ValidateDate(const QString &value) {
@@ -528,7 +535,7 @@ DateRow::DateRow(
GetDay(value))
, _separator1(
this,
object_ptr<Ui::FlatLabel>(
object_ptr<FlatLabel>(
this,
QString(" / "),
st::passportDetailsSeparator),
@@ -540,7 +547,7 @@ DateRow::DateRow(
GetMonth(value))
, _separator2(
this,
object_ptr<Ui::FlatLabel>(
object_ptr<FlatLabel>(
this,
QString(" / "),
st::passportDetailsSeparator),
@@ -552,7 +559,7 @@ DateRow::DateRow(
GetYear(value))
, _value(valueCurrent()) {
const auto focused = [=](const object_ptr<DateInput> &field) {
return [this, pointer = Ui::MakeWeak(field.data())]{
return [this, pointer = MakeWeak(field.data())]{
_borderAnimationStart = pointer->borderAnimationStart()
+ pointer->x()
- _day->x();
@@ -565,15 +572,15 @@ DateRow::DateRow(
const auto changed = [=] {
_value = valueCurrent();
};
connect(_day, &Ui::MaskedInputField::focused, focused(_day));
connect(_month, &Ui::MaskedInputField::focused, focused(_month));
connect(_year, &Ui::MaskedInputField::focused, focused(_year));
connect(_day, &Ui::MaskedInputField::blurred, blurred);
connect(_month, &Ui::MaskedInputField::blurred, blurred);
connect(_year, &Ui::MaskedInputField::blurred, blurred);
connect(_day, &Ui::MaskedInputField::changed, changed);
connect(_month, &Ui::MaskedInputField::changed, changed);
connect(_year, &Ui::MaskedInputField::changed, changed);
connect(_day, &MaskedInputField::focused, focused(_day));
connect(_month, &MaskedInputField::focused, focused(_month));
connect(_year, &MaskedInputField::focused, focused(_year));
connect(_day, &MaskedInputField::blurred, blurred);
connect(_month, &MaskedInputField::blurred, blurred);
connect(_year, &MaskedInputField::blurred, blurred);
connect(_day, &MaskedInputField::changed, changed);
connect(_month, &MaskedInputField::changed, changed);
connect(_year, &MaskedInputField::changed, changed);
_day->setMaxValue(31);
_day->putNext() | rpl::start_with_next([=](QChar ch) {
putNext(_month, ch);
@@ -845,8 +852,8 @@ GenderRow::GenderRow(
const QString &value)
: PanelDetailsRow(parent, label, maxLabelWidth)
, _group(StringToGender(value).has_value()
? std::make_shared<Ui::RadioenumGroup<Gender>>(*StringToGender(value))
: std::make_shared<Ui::RadioenumGroup<Gender>>())
? std::make_shared<RadioenumGroup<Gender>>(*StringToGender(value))
: std::make_shared<RadioenumGroup<Gender>>())
, _male(
this,
_group,
@@ -868,9 +875,9 @@ GenderRow::GenderRow(
});
}
std::unique_ptr<Ui::AbstractCheckView> GenderRow::createRadioView(
Ui::RadioView* &weak) const {
auto result = std::make_unique<Ui::RadioView>(st::defaultRadio, false);
std::unique_ptr<AbstractCheckView> GenderRow::createRadioView(
RadioView* &weak) const {
auto result = std::make_unique<RadioView>(st::defaultRadio, false);
weak = result.get();
return result;
}
@@ -959,8 +966,9 @@ PanelDetailsRow::PanelDetailsRow(
object_ptr<PanelDetailsRow> PanelDetailsRow::Create(
QWidget *parent,
Fn<void(object_ptr<BoxContent>)> showBox,
const QString &defaultCountry,
Type type,
not_null<PanelController*> controller,
const QString &label,
int maxLabelWidth,
const QString &value,
@@ -969,7 +977,7 @@ object_ptr<PanelDetailsRow> PanelDetailsRow::Create(
auto result = [&]() -> object_ptr<PanelDetailsRow> {
switch (type) {
case Type::Text:
return object_ptr<AbstractTextRow<Ui::InputField>>(
return object_ptr<AbstractTextRow<InputField>>(
parent,
label,
maxLabelWidth,
@@ -985,7 +993,8 @@ object_ptr<PanelDetailsRow> PanelDetailsRow::Create(
case Type::Country:
return object_ptr<CountryRow>(
parent,
controller,
showBox,
defaultCountry,
label,
maxLabelWidth,
value);
@@ -1062,7 +1071,7 @@ void PanelDetailsRow::showError(std::optional<QString> error) {
if (!_error) {
_error.create(
this,
object_ptr<Ui::FlatLabel>(
object_ptr<FlatLabel>(
this,
*error,
st::passportVerifyErrorLabel));
@@ -1122,4 +1131,4 @@ void PanelDetailsRow::paintEvent(QPaintEvent *e) {
p.drawTextLeft(padding.left(), padding.top(), width(), _label);
}
} // namespace Passport
} // namespace Passport::Ui

View File

@@ -11,18 +11,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/animations.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/widgets/labels.h"
#include "boxes/abstract_box.h"
namespace Ui {
class BoxContent;
class InputField;
class FlatLabel;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Passport {
namespace Passport::Ui {
class PanelController;
using namespace ::Ui;
enum class PanelDetailsType {
Text,
@@ -32,7 +32,7 @@ enum class PanelDetailsType {
Gender,
};
class PanelDetailsRow : public Ui::RpWidget {
class PanelDetailsRow : public RpWidget {
public:
using Type = PanelDetailsType;
@@ -43,8 +43,9 @@ public:
static object_ptr<PanelDetailsRow> Create(
QWidget *parent,
Fn<void(object_ptr<BoxContent>)> showBox,
const QString &defaultCountry,
Type type,
not_null<PanelController*> controller,
const QString &label,
int maxLabelWidth,
const QString &value,
@@ -74,11 +75,11 @@ private:
QString _label;
int _maxLabelWidth = 0;
object_ptr<Ui::SlideWrap<Ui::FlatLabel>> _error = { nullptr };
object_ptr<SlideWrap<FlatLabel>> _error = { nullptr };
bool _errorShown = false;
bool _errorHideSubscription = false;
Ui::Animations::Simple _errorAnimation;
Animations::Simple _errorAnimation;
};
} // namespace Passport
} // namespace Passport::Ui

View File

@@ -0,0 +1,125 @@
/*
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 "passport/ui/passport_form_row.h"
#include "ui/text/text_options.h"
#include "styles/style_passport.h"
#include "styles/style_layers.h"
namespace Passport::Ui {
FormRow::FormRow(QWidget *parent)
: RippleButton(parent, st::passportRowRipple)
, _title(st::boxWideWidth / 2)
, _description(st::boxWideWidth / 2) {
}
void FormRow::updateContent(
const QString &title,
const QString &description,
bool ready,
bool error,
anim::type animated) {
_title.setText(
st::semiboldTextStyle,
title,
NameTextOptions());
_description.setText(
st::defaultTextStyle,
description,
TextParseOptions {
TextParseMultiline,
0,
0,
Qt::LayoutDirectionAuto
});
_ready = ready && !error;
if (_error != error) {
_error = error;
if (animated == anim::type::instant) {
_errorAnimation.stop();
} else {
_errorAnimation.start(
[=] { update(); },
_error ? 0. : 1.,
_error ? 1. : 0.,
st::fadeWrapDuration);
}
}
resizeToWidth(width());
update();
}
int FormRow::resizeGetHeight(int newWidth) {
const auto availableWidth = countAvailableWidth(newWidth);
_titleHeight = _title.countHeight(availableWidth);
_descriptionHeight = _description.countHeight(availableWidth);
const auto result = st::passportRowPadding.top()
+ _titleHeight
+ st::passportRowSkip
+ _descriptionHeight
+ st::passportRowPadding.bottom();
return result;
}
int FormRow::countAvailableWidth(int newWidth) const {
return newWidth
- st::passportRowPadding.left()
- st::passportRowPadding.right()
- (_ready
? st::passportRowReadyIcon
: st::passportRowEmptyIcon).width()
- st::passportRowIconSkip;
}
int FormRow::countAvailableWidth() const {
return countAvailableWidth(width());
}
void FormRow::paintEvent(QPaintEvent *e) {
Painter p(this);
paintRipple(p, 0, 0);
const auto left = st::passportRowPadding.left();
const auto availableWidth = countAvailableWidth();
auto top = st::passportRowPadding.top();
const auto error = _errorAnimation.value(_error ? 1. : 0.);
p.setPen(st::passportRowTitleFg);
_title.drawLeft(p, left, top, availableWidth, width());
top += _titleHeight + st::passportRowSkip;
p.setPen(anim::pen(
st::passportRowDescriptionFg,
st::boxTextFgError,
error));
_description.drawLeft(p, left, top, availableWidth, width());
top += _descriptionHeight + st::passportRowPadding.bottom();
const auto &icon = _ready
? st::passportRowReadyIcon
: st::passportRowEmptyIcon;
if (error > 0. && !_ready) {
icon.paint(
p,
width() - st::passportRowPadding.right() - icon.width(),
(height() - icon.height()) / 2,
width(),
anim::color(st::menuIconFgOver, st::boxTextFgError, error));
} else {
icon.paint(
p,
width() - st::passportRowPadding.right() - icon.width(),
(height() - icon.height()) / 2,
width());
}
}
} // namespace Passport::Ui

View File

@@ -0,0 +1,48 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/text/text.h"
#include "ui/effects/animations.h"
#include "ui/widgets/buttons.h"
namespace Passport::Ui {
using namespace ::Ui;
class FormRow : public RippleButton {
public:
explicit FormRow(QWidget *parent);
void updateContent(
const QString &title,
const QString &description,
bool ready,
bool error,
anim::type animated);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
int countAvailableWidth() const;
int countAvailableWidth(int newWidth) const;
Text::String _title;
Text::String _description;
int _titleHeight = 0;
int _descriptionHeight = 0;
bool _ready = false;
bool _error = false;
Animations::Simple _errorAnimation;
};
} // namespace Passport::Ui

View File

@@ -0,0 +1,462 @@
/*
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 "payments/payments_checkout_process.h"
#include "payments/payments_form.h"
#include "payments/ui/payments_panel.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "storage/storage_domain.h"
#include "history/history_item.h"
#include "history/history.h"
#include "data/data_user.h" // UserData::isBot.
#include "core/local_url_handlers.h" // TryConvertUrlToLocal.
#include "core/file_utilities.h" // File::OpenUrl.
#include "apiwrap.h"
// #TODO payments errors
#include "mainwindow.h"
#include "ui/toasts/common_toasts.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
namespace Payments {
namespace {
struct SessionProcesses {
base::flat_map<FullMsgId, std::unique_ptr<CheckoutProcess>> map;
rpl::lifetime lifetime;
};
base::flat_map<not_null<Main::Session*>, SessionProcesses> Processes;
[[nodiscard]] SessionProcesses &LookupSessionProcesses(
not_null<const HistoryItem*> item) {
const auto session = &item->history()->session();
const auto i = Processes.find(session);
if (i != end(Processes)) {
return i->second;
}
const auto j = Processes.emplace(session).first;
auto &result = j->second;
session->account().sessionChanges(
) | rpl::start_with_next([=] {
Processes.erase(session);
}, result.lifetime);
return result;
}
} // namespace
void CheckoutProcess::Start(not_null<const HistoryItem*> item) {
auto &processes = LookupSessionProcesses(item);
const auto session = &item->history()->session();
const auto id = item->fullId();
const auto i = processes.map.find(id);
if (i != end(processes.map)) {
i->second->requestActivate();
return;
}
const auto j = processes.map.emplace(
id,
std::make_unique<CheckoutProcess>(session, id, PrivateTag{})).first;
j->second->requestActivate();
}
CheckoutProcess::CheckoutProcess(
not_null<Main::Session*> session,
FullMsgId itemId,
PrivateTag)
: _session(session)
, _form(std::make_unique<Form>(session, itemId))
, _panel(std::make_unique<Ui::Panel>(panelDelegate())) {
_form->updates(
) | rpl::start_with_next([=](const FormUpdate &update) {
handleFormUpdate(update);
}, _lifetime);
_panel->backRequests(
) | rpl::start_with_next([=] {
showForm();
}, _panel->lifetime());
showForm();
}
CheckoutProcess::~CheckoutProcess() {
}
void CheckoutProcess::requestActivate() {
_panel->requestActivate();
}
not_null<Ui::PanelDelegate*> CheckoutProcess::panelDelegate() {
return static_cast<PanelDelegate*>(this);
}
void CheckoutProcess::handleFormUpdate(const FormUpdate &update) {
v::match(update, [&](const FormReady &) {
performInitialSilentValidation();
if (!_initialSilentValidation) {
showForm();
}
}, [&](const ThumbnailUpdated &data) {
_panel->updateFormThumbnail(data.thumbnail);
}, [&](const ValidateFinished &) {
if (_initialSilentValidation) {
_initialSilentValidation = false;
}
showForm();
if (_submitState == SubmitState::Validation) {
_submitState = SubmitState::Validated;
panelSubmit();
}
}, [&](const PaymentMethodUpdate &) {
showForm();
}, [&](const VerificationNeeded &data) {
if (!_panel->showWebview(data.url, false)) {
File::OpenUrl(data.url);
panelCloseSure();
}
}, [&](const PaymentFinished &data) {
const auto weak = base::make_weak(this);
_session->api().applyUpdates(data.updates);
if (weak) {
panelCloseSure();
}
}, [&](const Error &error) {
handleError(error);
});
}
void CheckoutProcess::handleError(const Error &error) {
const auto showToast = [&](const TextWithEntities &text) {
if (_panel) {
_panel->requestActivate();
_panel->showToast(text);
} else {
App::wnd()->activate();
Ui::ShowMultilineToast({ .text = text });
}
};
const auto &id = error.id;
switch (error.type) {
case Error::Type::Form:
if (true
|| id == u"PROVIDER_ACCOUNT_INVALID"_q
|| id == u"PROVIDER_ACCOUNT_TIMEOUT"_q) {
showToast({ "Error: " + id });
}
break;
case Error::Type::Validate: {
if (_submitState == SubmitState::Validation) {
_submitState = SubmitState::None;
}
if (_initialSilentValidation) {
_initialSilentValidation = false;
showForm();
return;
}
using InfoField = Ui::InformationField;
using CardField = Ui::CardField;
if (id == u"REQ_INFO_NAME_INVALID"_q) {
showInformationError(InfoField::Name);
} else if (id == u"REQ_INFO_EMAIL_INVALID"_q) {
showInformationError(InfoField::Email);
} else if (id == u"REQ_INFO_PHONE_INVALID"_q) {
showInformationError(InfoField::Phone);
} else if (id == u"ADDRESS_STREET_LINE1_INVALID"_q) {
showInformationError(InfoField::ShippingStreet);
} else if (id == u"ADDRESS_CITY_INVALID"_q) {
showInformationError(InfoField::ShippingCity);
} else if (id == u"ADDRESS_STATE_INVALID"_q) {
showInformationError(InfoField::ShippingState);
} else if (id == u"ADDRESS_COUNTRY_INVALID"_q) {
showInformationError(InfoField::ShippingCountry);
} else if (id == u"ADDRESS_POSTCODE_INVALID"_q) {
showInformationError(InfoField::ShippingPostcode);
} else if (id == u"LOCAL_CARD_NUMBER_INVALID"_q) {
showCardError(CardField::Number);
} else if (id == u"LOCAL_CARD_EXPIRE_DATE_INVALID"_q) {
showCardError(CardField::ExpireDate);
} else if (id == u"LOCAL_CARD_CVC_INVALID"_q) {
showCardError(CardField::Cvc);
} else if (id == u"LOCAL_CARD_HOLDER_NAME_INVALID"_q) {
showCardError(CardField::Name);
} else if (id == u"LOCAL_CARD_BILLING_COUNTRY_INVALID"_q) {
showCardError(CardField::AddressCountry);
} else if (id == u"LOCAL_CARD_BILLING_ZIP_INVALID"_q) {
showCardError(CardField::AddressZip);
} else if (id == u"SHIPPING_BOT_TIMEOUT"_q) {
showToast({ "Error: Bot Timeout!" }); // #TODO payments errors message
} else if (id == u"SHIPPING_NOT_AVAILABLE"_q) {
showToast({ "Error: Shipping to the selected country is not available!" }); // #TODO payments errors message
} else {
showToast({ "Error: " + id });
}
} break;
case Error::Type::Stripe: {
using Field = Ui::CardField;
if (id == u"InvalidNumber"_q || id == u"IncorrectNumber"_q) {
showCardError(Field::Number);
} else if (id == u"InvalidCVC"_q || id == u"IncorrectCVC"_q) {
showCardError(Field::Cvc);
} else if (id == u"InvalidExpiryMonth"_q
|| id == u"InvalidExpiryYear"_q
|| id == u"ExpiredCard"_q) {
showCardError(Field::ExpireDate);
} else if (id == u"CardDeclined"_q) {
// #TODO payments errors message
showToast({ "Error: " + id });
} else if (id == u"ProcessingError"_q) {
// #TODO payments errors message
showToast({ "Error: " + id });
} else {
showToast({ "Error: " + id });
}
} break;
case Error::Type::Send:
if (_submitState == SubmitState::Finishing) {
_submitState = SubmitState::None;
}
if (id == u"PAYMENT_FAILED"_q) {
showToast({ "Error: Payment Failed. Your card has not been billed." }); // #TODO payments errors message
} else if (id == u"BOT_PRECHECKOUT_FAILED"_q) {
showToast({ "Error: PreCheckout Failed. Your card has not been billed." }); // #TODO payments errors message
} else if (id == u"REQUESTED_INFO_INVALID"_q
|| id == u"SHIPPING_OPTION_INVALID"_q
|| id == u"PAYMENT_CREDENTIALS_INVALID"_q
|| id == u"PAYMENT_CREDENTIALS_ID_INVALID"_q) {
showToast({ "Error: " + id + ". Your card has not been billed." });
}
break;
default: Unexpected("Error type in CheckoutProcess::handleError.");
}
}
void CheckoutProcess::panelRequestClose() {
panelCloseSure(); // #TODO payments confirmation
}
void CheckoutProcess::panelCloseSure() {
const auto i = Processes.find(_session);
if (i == end(Processes)) {
return;
}
const auto j = ranges::find(i->second.map, this, [](const auto &pair) {
return pair.second.get();
});
if (j == end(i->second.map)) {
return;
}
i->second.map.erase(j);
if (i->second.map.empty()) {
Processes.erase(i);
}
}
void CheckoutProcess::panelSubmit() {
if (_submitState == SubmitState::Validation
|| _submitState == SubmitState::Finishing) {
return;
}
const auto &method = _form->paymentMethod();
const auto &invoice = _form->invoice();
const auto &options = _form->shippingOptions();
if (!options.list.empty() && options.selectedId.isEmpty()) {
chooseShippingOption();
return;
} else if (_submitState != SubmitState::Validated
&& options.list.empty()
&& (invoice.isShippingAddressRequested
|| invoice.isNameRequested
|| invoice.isEmailRequested
|| invoice.isPhoneRequested)) {
_submitState = SubmitState::Validation;
_form->validateInformation(_form->savedInformation());
return;
} else if (!method.newCredentials && !method.savedCredentials) {
editPaymentMethod();
return;
}
_submitState = SubmitState::Finishing;
_form->submit();
}
void CheckoutProcess::panelWebviewMessage(const QJsonDocument &message) {
if (!message.isArray()) {
LOG(("Payments Error: "
"Not an array received in buy_callback arguments."));
return;
}
const auto list = message.array();
if (list.at(0).toString() != "payment_form_submit") {
return;
} else if (!list.at(1).isString()) {
LOG(("Payments Error: "
"Not a string received in buy_callback result."));
return;
}
auto error = QJsonParseError();
const auto document = QJsonDocument::fromJson(
list.at(1).toString().toUtf8(),
&error);
if (error.error != QJsonParseError::NoError) {
LOG(("Payments Error: "
"Failed to parse buy_callback arguments, error: %1."
).arg(error.errorString()));
return;
} else if (!document.isObject()) {
LOG(("Payments Error: "
"Not an object decoded in buy_callback result."));
return;
}
const auto root = document.object();
const auto title = root.value("title").toString();
const auto credentials = root.value("credentials");
if (!credentials.isObject()) {
LOG(("Payments Error: "
"Not an object received in payment credentials."));
return;
}
crl::on_main(this, [=] {
_form->setPaymentCredentials(NewCredentials{
.title = title,
.data = QJsonDocument(
credentials.toObject()
).toJson(QJsonDocument::Compact),
.saveOnServer = false, // #TODO payments save
});
});
}
bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) {
if (Core::TryConvertUrlToLocal(uri) == uri) {
return true;
}
crl::on_main(this, [=] {
panelCloseSure();
App::wnd()->activate();
});
return false;
}
void CheckoutProcess::panelEditPaymentMethod() {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
editPaymentMethod();
}
void CheckoutProcess::panelValidateCard(Ui::UncheckedCardDetails data) {
_form->validateCard(data);
}
void CheckoutProcess::panelEditShippingInformation() {
showEditInformation(Ui::InformationField::ShippingStreet);
}
void CheckoutProcess::panelEditName() {
showEditInformation(Ui::InformationField::Name);
}
void CheckoutProcess::panelEditEmail() {
showEditInformation(Ui::InformationField::Email);
}
void CheckoutProcess::panelEditPhone() {
showEditInformation(Ui::InformationField::Phone);
}
void CheckoutProcess::showForm() {
_panel->showForm(
_form->invoice(),
_form->savedInformation(),
_form->paymentMethod().ui,
_form->shippingOptions());
}
void CheckoutProcess::showEditInformation(Ui::InformationField field) {
if (_submitState != SubmitState::None) {
return;
}
_panel->showEditInformation(
_form->invoice(),
_form->savedInformation(),
field);
}
void CheckoutProcess::showInformationError(Ui::InformationField field) {
if (_submitState != SubmitState::None) {
return;
}
_panel->showInformationError(
_form->invoice(),
_form->savedInformation(),
field);
}
void CheckoutProcess::showCardError(Ui::CardField field) {
if (_submitState != SubmitState::None) {
return;
}
_panel->showCardError(_form->paymentMethod().ui.native, field);
}
void CheckoutProcess::chooseShippingOption() {
_panel->chooseShippingOption(_form->shippingOptions());
}
void CheckoutProcess::editPaymentMethod() {
_panel->choosePaymentMethod(_form->paymentMethod().ui);
}
void CheckoutProcess::panelChooseShippingOption() {
if (_submitState != SubmitState::None) {
return;
}
chooseShippingOption();
}
void CheckoutProcess::panelChangeShippingOption(const QString &id) {
_form->setShippingOption(id);
showForm();
}
void CheckoutProcess::panelValidateInformation(
Ui::RequestedInformation data) {
_form->validateInformation(data);
}
void CheckoutProcess::panelShowBox(object_ptr<Ui::BoxContent> box) {
_panel->showBox(std::move(box));
}
void CheckoutProcess::performInitialSilentValidation() {
const auto &invoice = _form->invoice();
const auto &saved = _form->savedInformation();
if (invoice.receipt
|| (invoice.isNameRequested && saved.name.isEmpty())
|| (invoice.isEmailRequested && saved.email.isEmpty())
|| (invoice.isPhoneRequested && saved.phone.isEmpty())
|| (invoice.isShippingAddressRequested && !saved.shippingAddress)) {
return;
}
_initialSilentValidation = true;
_form->validateInformation(saved);
}
QString CheckoutProcess::panelWebviewDataPath() {
return _session->domain().local().webviewDataPath();
}
} // namespace Payments

View File

@@ -0,0 +1,98 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "payments/ui/payments_panel_delegate.h"
#include "base/weak_ptr.h"
class HistoryItem;
namespace Main {
class Session;
} // namespace Main
namespace Payments::Ui {
class Panel;
enum class InformationField;
enum class CardField;
} // namespace Payments::Ui
namespace Payments {
class Form;
struct FormUpdate;
struct Error;
class CheckoutProcess final
: public base::has_weak_ptr
, private Ui::PanelDelegate {
struct PrivateTag {};
public:
static void Start(not_null<const HistoryItem*> item);
CheckoutProcess(
not_null<Main::Session*> session,
FullMsgId itemId,
PrivateTag);
~CheckoutProcess();
void requestActivate();
private:
enum class SubmitState {
None,
Validation,
Validated,
Finishing,
};
[[nodiscard]] not_null<PanelDelegate*> panelDelegate();
void handleFormUpdate(const FormUpdate &update);
void handleError(const Error &error);
void showForm();
void showEditInformation(Ui::InformationField field);
void showInformationError(Ui::InformationField field);
void showCardError(Ui::CardField field);
void chooseShippingOption();
void editPaymentMethod();
void performInitialSilentValidation();
void panelRequestClose() override;
void panelCloseSure() override;
void panelSubmit() override;
void panelWebviewMessage(const QJsonDocument &message) override;
bool panelWebviewNavigationAttempt(const QString &uri) override;
void panelEditPaymentMethod() override;
void panelEditShippingInformation() override;
void panelEditName() override;
void panelEditEmail() override;
void panelEditPhone() override;
void panelChooseShippingOption() override;
void panelChangeShippingOption(const QString &id) override;
void panelValidateInformation(Ui::RequestedInformation data) override;
void panelValidateCard(Ui::UncheckedCardDetails data) override;
void panelShowBox(object_ptr<Ui::BoxContent> box) override;
QString panelWebviewDataPath() override;
const not_null<Main::Session*> _session;
const std::unique_ptr<Form> _form;
const std::unique_ptr<Ui::Panel> _panel;
SubmitState _submitState = SubmitState::None;
bool _initialSilentValidation = false;
rpl::lifetime _lifetime;
};
} // namespace Payments

View File

@@ -0,0 +1,669 @@
/*
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 "payments/payments_form.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_media_types.h"
#include "data/data_user.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_file_origin.h"
#include "data/data_countries.h"
#include "history/history_item.h"
#include "stripe/stripe_api_client.h"
#include "stripe/stripe_error.h"
#include "stripe/stripe_token.h"
#include "stripe/stripe_card_validator.h"
#include "ui/image/image.h"
#include "apiwrap.h"
#include "styles/style_payments.h" // paymentsThumbnailSize.
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
namespace Payments {
namespace {
[[nodiscard]] Ui::Address ParseAddress(const MTPPostAddress &address) {
return address.match([](const MTPDpostAddress &data) {
return Ui::Address{
.address1 = qs(data.vstreet_line1()),
.address2 = qs(data.vstreet_line2()),
.city = qs(data.vcity()),
.state = qs(data.vstate()),
.countryIso2 = qs(data.vcountry_iso2()),
.postcode = qs(data.vpost_code()),
};
});
}
[[nodiscard]] std::vector<Ui::LabeledPrice> ParsePrices(
const MTPVector<MTPLabeledPrice> &data) {
return ranges::views::all(
data.v
) | ranges::views::transform([](const MTPLabeledPrice &price) {
return price.match([&](const MTPDlabeledPrice &data) {
return Ui::LabeledPrice{
.label = qs(data.vlabel()),
.price = *reinterpret_cast<const int64*>(&data.vamount().v),
};
});
}) | ranges::to_vector;
}
[[nodiscard]] MTPPaymentRequestedInfo Serialize(
const Ui::RequestedInformation &information) {
using Flag = MTPDpaymentRequestedInfo::Flag;
return MTP_paymentRequestedInfo(
MTP_flags((information.name.isEmpty() ? Flag(0) : Flag::f_name)
| (information.email.isEmpty() ? Flag(0) : Flag::f_email)
| (information.phone.isEmpty() ? Flag(0) : Flag::f_phone)
| (information.shippingAddress
? Flag::f_shipping_address
: Flag(0))),
MTP_string(information.name),
MTP_string(information.phone),
MTP_string(information.email),
MTP_postAddress(
MTP_string(information.shippingAddress.address1),
MTP_string(information.shippingAddress.address2),
MTP_string(information.shippingAddress.city),
MTP_string(information.shippingAddress.state),
MTP_string(information.shippingAddress.countryIso2),
MTP_string(information.shippingAddress.postcode)));
}
[[nodiscard]] QString CardTitle(const Stripe::Card &card) {
// Like server stores saved_credentials title.
return Stripe::CardBrandToString(card.brand()).toLower()
+ " *"
+ card.last4();
}
} // namespace
Form::Form(not_null<Main::Session*> session, FullMsgId itemId)
: _session(session)
, _api(&_session->mtp())
, _msgId(itemId) {
fillInvoiceFromMessage();
if (_receiptMsgId) {
requestReceipt();
} else {
requestForm();
}
}
Form::~Form() = default;
void Form::fillInvoiceFromMessage() {
if (const auto item = _session->data().message(_msgId)) {
if (const auto media = item->media()) {
if (const auto invoice = media->invoice()) {
_receiptMsgId = FullMsgId(
_msgId.channel,
invoice->receiptMsgId);
_invoice.cover = Ui::Cover{
.title = invoice->title,
.description = invoice->description,
};
if (_receiptMsgId) {
_invoice.receipt.paid = true;
}
if (const auto photo = invoice->photo) {
loadThumbnail(photo);
}
}
}
}
}
void Form::loadThumbnail(not_null<PhotoData*> photo) {
Expects(!_thumbnailLoadProcess);
auto view = photo->createMediaView();
if (auto good = prepareGoodThumbnail(view); !good.isNull()) {
_invoice.cover.thumbnail = std::move(good);
return;
}
_thumbnailLoadProcess = std::make_unique<ThumbnailLoadProcess>();
if (auto blurred = prepareBlurredThumbnail(view); !blurred.isNull()) {
_invoice.cover.thumbnail = std::move(blurred);
_thumbnailLoadProcess->blurredSet = true;
} else {
_invoice.cover.thumbnail = prepareEmptyThumbnail();
}
_thumbnailLoadProcess->view = std::move(view);
photo->load(Data::PhotoSize::Thumbnail, _msgId);
_session->downloaderTaskFinished(
) | rpl::start_with_next([=] {
const auto &view = _thumbnailLoadProcess->view;
if (auto good = prepareGoodThumbnail(view); !good.isNull()) {
_invoice.cover.thumbnail = std::move(good);
_thumbnailLoadProcess = nullptr;
} else if (_thumbnailLoadProcess->blurredSet) {
return;
} else if (auto blurred = prepareBlurredThumbnail(view)
; !blurred.isNull()) {
_invoice.cover.thumbnail = std::move(blurred);
_thumbnailLoadProcess->blurredSet = true;
} else {
return;
}
_updates.fire(ThumbnailUpdated{ _invoice.cover.thumbnail });
}, _thumbnailLoadProcess->lifetime);
}
QImage Form::prepareGoodThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const {
using Size = Data::PhotoSize;
if (const auto large = view->image(Size::Large)) {
return prepareThumbnail(large);
} else if (const auto thumbnail = view->image(Size::Thumbnail)) {
return prepareThumbnail(thumbnail);
}
return QImage();
}
QImage Form::prepareBlurredThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const {
if (const auto small = view->image(Data::PhotoSize::Small)) {
return prepareThumbnail(small, true);
} else if (const auto blurred = view->thumbnailInline()) {
return prepareThumbnail(blurred, true);
}
return QImage();
}
QImage Form::prepareThumbnail(
not_null<const Image*> image,
bool blurred) const {
auto result = image->original().scaled(
st::paymentsThumbnailSize * cIntRetinaFactor(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
Images::prepareRound(result, ImageRoundRadius::Large);
result.setDevicePixelRatio(cRetinaFactor());
return result;
}
QImage Form::prepareEmptyThumbnail() const {
auto result = QImage(
st::paymentsThumbnailSize * cIntRetinaFactor(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(cRetinaFactor());
result.fill(Qt::transparent);
return result;
}
void Form::requestForm() {
_api.request(MTPpayments_GetPaymentForm(
MTP_int(_msgId.msg)
)).done([=](const MTPpayments_PaymentForm &result) {
result.match([&](const auto &data) {
processForm(data);
});
}).fail([=](const MTP::Error &error) {
_updates.fire(Error{ Error::Type::Form, error.type() });
}).send();
}
void Form::requestReceipt() {
_api.request(MTPpayments_GetPaymentReceipt(
MTP_int(_receiptMsgId.msg)
)).done([=](const MTPpayments_PaymentReceipt &result) {
result.match([&](const auto &data) {
processReceipt(data);
});
}).fail([=](const MTP::Error &error) {
_updates.fire(Error{ Error::Type::Form, error.type() });
}).send();
}
void Form::processForm(const MTPDpayments_paymentForm &data) {
_session->data().processUsers(data.vusers());
data.vinvoice().match([&](const auto &data) {
processInvoice(data);
});
processDetails(data);
if (const auto info = data.vsaved_info()) {
info->match([&](const auto &data) {
processSavedInformation(data);
});
}
if (const auto credentials = data.vsaved_credentials()) {
credentials->match([&](const auto &data) {
processSavedCredentials(data);
});
}
fillPaymentMethodInformation();
_updates.fire(FormReady{});
}
void Form::processReceipt(const MTPDpayments_paymentReceipt &data) {
_session->data().processUsers(data.vusers());
data.vinvoice().match([&](const auto &data) {
processInvoice(data);
});
processDetails(data);
if (const auto info = data.vinfo()) {
info->match([&](const auto &data) {
processSavedInformation(data);
});
}
if (const auto shipping = data.vshipping()) {
processShippingOptions({ *shipping });
if (!_shippingOptions.list.empty()) {
_shippingOptions.selectedId = _shippingOptions.list.front().id;
}
}
_paymentMethod.savedCredentials = SavedCredentials{
.id = "(used)",
.title = qs(data.vcredentials_title()),
};
fillPaymentMethodInformation();
_updates.fire(FormReady{});
}
void Form::processInvoice(const MTPDinvoice &data) {
_invoice = Ui::Invoice{
.cover = std::move(_invoice.cover),
.prices = ParsePrices(data.vprices()),
.currency = qs(data.vcurrency()),
.isNameRequested = data.is_name_requested(),
.isPhoneRequested = data.is_phone_requested(),
.isEmailRequested = data.is_email_requested(),
.isShippingAddressRequested = data.is_shipping_address_requested(),
.isFlexible = data.is_flexible(),
.isTest = data.is_test(),
.phoneSentToProvider = data.is_phone_to_provider(),
.emailSentToProvider = data.is_email_to_provider(),
};
}
void Form::processDetails(const MTPDpayments_paymentForm &data) {
const auto nativeParams = data.vnative_params();
auto nativeParamsJson = nativeParams
? nativeParams->match(
[&](const MTPDdataJSON &data) { return data.vdata().v; })
: QByteArray();
_details = FormDetails{
.url = qs(data.vurl()),
.nativeProvider = qs(data.vnative_provider().value_or_empty()),
.nativeParamsJson = std::move(nativeParamsJson),
.botId = data.vbot_id().v,
.providerId = data.vprovider_id().v,
.canSaveCredentials = data.is_can_save_credentials(),
.passwordMissing = data.is_password_missing(),
};
if (_details.botId) {
if (const auto bot = _session->data().userLoaded(_details.botId)) {
_invoice.cover.seller = bot->name;
}
}
}
void Form::processDetails(const MTPDpayments_paymentReceipt &data) {
_invoice.receipt = Ui::Receipt{
.date = data.vdate().v,
.totalAmount = *reinterpret_cast<const int64*>(
&data.vtotal_amount().v),
.currency = qs(data.vcurrency()),
.paid = true,
};
_details = FormDetails{
.botId = data.vbot_id().v,
.providerId = data.vprovider_id().v,
};
if (_details.botId) {
if (const auto bot = _session->data().userLoaded(_details.botId)) {
_invoice.cover.seller = bot->name;
}
}
}
void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) {
const auto address = data.vshipping_address();
_savedInformation = Ui::RequestedInformation{
.defaultPhone = defaultPhone(),
.defaultCountry = defaultCountry(),
.name = qs(data.vname().value_or_empty()),
.phone = qs(data.vphone().value_or_empty()),
.email = qs(data.vemail().value_or_empty()),
.shippingAddress = address ? ParseAddress(*address) : Ui::Address(),
};
}
void Form::processSavedCredentials(
const MTPDpaymentSavedCredentialsCard &data) {
// #TODO payments save
//_nativePayment.savedCredentials = SavedCredentials{
// .id = qs(data.vid()),
// .title = qs(data.vtitle()),
//};
refreshPaymentMethodDetails();
}
void Form::refreshPaymentMethodDetails() {
const auto &saved = _paymentMethod.savedCredentials;
const auto &entered = _paymentMethod.newCredentials;
_paymentMethod.ui.title = entered ? entered.title : saved.title;
_paymentMethod.ui.ready = entered || saved;
_paymentMethod.ui.native.defaultCountry = defaultCountry();
}
QString Form::defaultPhone() const {
return _session->user()->phone();
}
QString Form::defaultCountry() const {
return Data::CountryISO2ByPhone(defaultPhone());
}
void Form::fillPaymentMethodInformation() {
_paymentMethod.native = NativePaymentMethod();
_paymentMethod.ui.native = Ui::NativeMethodDetails();
_paymentMethod.ui.url = _details.url;
if (_details.nativeProvider == "stripe") {
fillStripeNativeMethod();
}
refreshPaymentMethodDetails();
}
void Form::fillStripeNativeMethod() {
auto error = QJsonParseError();
auto document = QJsonDocument::fromJson(
_details.nativeParamsJson,
&error);
if (error.error != QJsonParseError::NoError) {
LOG(("Payment Error: Could not decode native_params, error %1: %2"
).arg(error.error
).arg(error.errorString()));
return;
} else if (!document.isObject()) {
LOG(("Payment Error: Not an object in native_params."));
return;
}
const auto object = document.object();
const auto value = [&](QStringView key) {
return object.value(key);
};
const auto key = value(u"publishable_key").toString();
if (key.isEmpty()) {
LOG(("Payment Error: No publishable_key in native_params."));
return;
}
_paymentMethod.native = NativePaymentMethod{
.data = StripePaymentMethod{
.publishableKey = key,
},
};
_paymentMethod.ui.native = Ui::NativeMethodDetails{
.supported = true,
.needCountry = value(u"need_country").toBool(),
.needZip = value(u"need_zip").toBool(),
.needCardholderName = value(u"need_cardholder_name").toBool(),
};
}
void Form::submit() {
Expects(!_paymentMethod.newCredentials.data.isEmpty()); // #TODO payments save
using Flag = MTPpayments_SendPaymentForm::Flag;
_api.request(MTPpayments_SendPaymentForm(
MTP_flags((_requestedInformationId.isEmpty()
? Flag(0)
: Flag::f_requested_info_id)
| (_shippingOptions.selectedId.isEmpty()
? Flag(0)
: Flag::f_shipping_option_id)),
MTP_int(_msgId.msg),
MTP_string(_requestedInformationId),
MTP_string(_shippingOptions.selectedId),
MTP_inputPaymentCredentials(
MTP_flags(0),
MTP_dataJSON(MTP_bytes(_paymentMethod.newCredentials.data)))
)).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
_updates.fire(PaymentFinished{ data.vupdates() });
}, [&](const MTPDpayments_paymentVerificationNeeded &data) {
_updates.fire(VerificationNeeded{ qs(data.vurl()) });
});
}).fail([=](const MTP::Error &error) {
_updates.fire(Error{ Error::Type::Send, error.type() });
}).send();
}
void Form::validateInformation(const Ui::RequestedInformation &information) {
if (_validateRequestId) {
if (_validatedInformation == information) {
return;
}
_api.request(base::take(_validateRequestId)).cancel();
}
_validatedInformation = information;
if (!validateInformationLocal(information)) {
return;
}
Assert(!_invoice.isShippingAddressRequested
|| information.shippingAddress);
Assert(!_invoice.isNameRequested || !information.name.isEmpty());
Assert(!_invoice.isEmailRequested || !information.email.isEmpty());
Assert(!_invoice.isPhoneRequested || !information.phone.isEmpty());
_validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo(
MTP_flags(0), // #TODO payments save information
MTP_int(_msgId.msg),
Serialize(information)
)).done([=](const MTPpayments_ValidatedRequestedInfo &result) {
_validateRequestId = 0;
const auto oldSelectedId = _shippingOptions.selectedId;
result.match([&](const MTPDpayments_validatedRequestedInfo &data) {
_requestedInformationId = data.vid().value_or_empty();
processShippingOptions(
data.vshipping_options().value_or_empty());
});
_shippingOptions.selectedId = ranges::contains(
_shippingOptions.list,
oldSelectedId,
&Ui::ShippingOption::id
) ? oldSelectedId : QString();
if (_shippingOptions.selectedId.isEmpty()
&& _shippingOptions.list.size() == 1) {
_shippingOptions.selectedId = _shippingOptions.list.front().id;
}
_savedInformation = _validatedInformation;
_updates.fire(ValidateFinished{});
}).fail([=](const MTP::Error &error) {
_validateRequestId = 0;
_updates.fire(Error{ Error::Type::Validate, error.type() });
}).send();
}
bool Form::validateInformationLocal(
const Ui::RequestedInformation &information) const {
if (const auto error = informationErrorLocal(information)) {
_updates.fire_copy(error);
return false;
}
return true;
}
Error Form::informationErrorLocal(
const Ui::RequestedInformation &information) const {
auto errors = QStringList();
const auto push = [&](const QString &id) {
errors.push_back(id);
};
if (_invoice.isShippingAddressRequested) {
if (information.shippingAddress.address1.isEmpty()) {
push(u"ADDRESS_STREET_LINE1_INVALID"_q);
}
if (information.shippingAddress.city.isEmpty()) {
push(u"ADDRESS_CITY_INVALID"_q);
}
if (information.shippingAddress.countryIso2.isEmpty()) {
push(u"ADDRESS_COUNTRY_INVALID"_q);
}
}
if (_invoice.isNameRequested && information.name.isEmpty()) {
push(u"REQ_INFO_NAME_INVALID"_q);
}
if (_invoice.isEmailRequested && information.email.isEmpty()) {
push(u"REQ_INFO_EMAIL_INVALID"_q);
}
if (_invoice.isPhoneRequested && information.phone.isEmpty()) {
push(u"REQ_INFO_PHONE_INVALID"_q);
}
if (!errors.isEmpty()) {
return Error{ Error::Type::Validate, errors.front() };
}
return Error();
}
void Form::validateCard(const Ui::UncheckedCardDetails &details) {
Expects(!v::is_null(_paymentMethod.native.data));
if (!validateCardLocal(details)) {
return;
}
const auto &native = _paymentMethod.native.data;
if (const auto stripe = std::get_if<StripePaymentMethod>(&native)) {
validateCard(*stripe, details);
} else {
Unexpected("Native payment provider in Form::validateCard.");
}
}
bool Form::validateCardLocal(const Ui::UncheckedCardDetails &details) const {
if (auto error = cardErrorLocal(details)) {
_updates.fire(std::move(error));
return false;
}
return true;
}
Error Form::cardErrorLocal(const Ui::UncheckedCardDetails &details) const {
using namespace Stripe;
auto errors = QStringList();
const auto push = [&](const QString &id) {
errors.push_back(id);
};
const auto kValid = ValidationState::Valid;
if (ValidateCard(details.number).state != kValid) {
push(u"LOCAL_CARD_NUMBER_INVALID"_q);
}
if (ValidateParsedExpireDate(
details.expireMonth,
details.expireYear
) != kValid) {
push(u"LOCAL_CARD_EXPIRE_DATE_INVALID"_q);
}
if (ValidateCvc(details.number, details.cvc).state != kValid) {
push(u"LOCAL_CARD_CVC_INVALID"_q);
}
if (_paymentMethod.ui.native.needCardholderName
&& details.cardholderName.isEmpty()) {
push(u"LOCAL_CARD_HOLDER_NAME_INVALID"_q);
}
if (_paymentMethod.ui.native.needCountry
&& details.addressCountry.isEmpty()) {
push(u"LOCAL_CARD_BILLING_COUNTRY_INVALID"_q);
}
if (_paymentMethod.ui.native.needZip
&& details.addressZip.isEmpty()) {
push(u"LOCAL_CARD_BILLING_ZIP_INVALID"_q);
}
if (!errors.isEmpty()) {
return Error{ Error::Type::Validate, errors.front() };
}
return Error();
}
void Form::validateCard(
const StripePaymentMethod &method,
const Ui::UncheckedCardDetails &details) {
Expects(!method.publishableKey.isEmpty());
if (_stripe) {
return;
}
auto configuration = Stripe::PaymentConfiguration{
.publishableKey = method.publishableKey,
.companyName = "Telegram",
};
_stripe = std::make_unique<Stripe::APIClient>(std::move(configuration));
auto card = Stripe::CardParams{
.number = details.number,
.expMonth = details.expireMonth,
.expYear = details.expireYear,
.cvc = details.cvc,
.name = details.cardholderName,
.addressZip = details.addressZip,
.addressCountry = details.addressCountry,
};
_stripe->createTokenWithCard(std::move(card), crl::guard(this, [=](
Stripe::Token token,
Stripe::Error error) {
_stripe = nullptr;
if (error) {
LOG(("Stripe Error %1: %2 (%3)"
).arg(int(error.code())
).arg(error.description()
).arg(error.message()));
_updates.fire(Error{ Error::Type::Stripe, error.description() });
} else {
setPaymentCredentials({
.title = CardTitle(token.card()),
.data = QJsonDocument(QJsonObject{
{ "type", "card" },
{ "id", token.tokenId() },
}).toJson(QJsonDocument::Compact),
.saveOnServer = false, // #TODO payments save
});
}
}));
}
void Form::setPaymentCredentials(const NewCredentials &credentials) {
Expects(!credentials.empty());
_paymentMethod.newCredentials = credentials;
refreshPaymentMethodDetails();
_updates.fire(PaymentMethodUpdate{});
}
void Form::setShippingOption(const QString &id) {
_shippingOptions.selectedId = id;
}
void Form::processShippingOptions(const QVector<MTPShippingOption> &data) {
_shippingOptions = Ui::ShippingOptions{ ranges::views::all(
data
) | ranges::views::transform([](const MTPShippingOption &option) {
return option.match([](const MTPDshippingOption &data) {
return Ui::ShippingOption{
.id = qs(data.vid()),
.title = qs(data.vtitle()),
.prices = ParsePrices(data.vprices()),
};
});
}) | ranges::to_vector };
}
} // namespace Payments

View File

@@ -0,0 +1,243 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "payments/ui/payments_panel_data.h"
#include "base/weak_ptr.h"
#include "mtproto/sender.h"
class Image;
namespace Stripe {
class APIClient;
} // namespace Stripe
namespace Main {
class Session;
} // namespace Main
namespace Data {
class PhotoMedia;
} // namespace Data
namespace Payments {
struct FormDetails {
QString url;
QString nativeProvider;
QByteArray nativeParamsJson;
UserId botId = 0;
UserId providerId = 0;
bool canSaveCredentials = false;
bool passwordMissing = false;
[[nodiscard]] bool valid() const {
return !url.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct ThumbnailLoadProcess {
std::shared_ptr<Data::PhotoMedia> view;
bool blurredSet = false;
rpl::lifetime lifetime;
};
struct SavedCredentials {
QString id;
QString title;
[[nodiscard]] bool valid() const {
return !id.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct NewCredentials {
QString title;
QByteArray data;
bool saveOnServer = false;
[[nodiscard]] bool empty() const {
return data.isEmpty();
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
};
struct StripePaymentMethod {
QString publishableKey;
};
struct NativePaymentMethod {
std::variant<
v::null_t,
StripePaymentMethod> data;
[[nodiscard]] bool valid() const {
return !v::is_null(data);
}
[[nodiscard]] explicit operator bool() const {
return valid();
}
};
struct PaymentMethod {
NativePaymentMethod native;
SavedCredentials savedCredentials;
NewCredentials newCredentials;
Ui::PaymentMethodDetails ui;
};
struct FormReady {};
struct ThumbnailUpdated {
QImage thumbnail;
};
struct ValidateFinished {};
struct PaymentMethodUpdate {};
struct VerificationNeeded {
QString url;
};
struct PaymentFinished {
MTPUpdates updates;
};
struct Error {
enum class Type {
None,
Form,
Validate,
Stripe,
Send,
};
Type type = Type::None;
QString id;
[[nodiscard]] bool empty() const {
return (type == Type::None);
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
};
struct FormUpdate : std::variant<
FormReady,
ThumbnailUpdated,
ValidateFinished,
PaymentMethodUpdate,
VerificationNeeded,
PaymentFinished,
Error> {
using variant::variant;
};
class Form final : public base::has_weak_ptr {
public:
Form(not_null<Main::Session*> session, FullMsgId itemId);
~Form();
[[nodiscard]] const Ui::Invoice &invoice() const {
return _invoice;
}
[[nodiscard]] const FormDetails &details() const {
return _details;
}
[[nodiscard]] const Ui::RequestedInformation &savedInformation() const {
return _savedInformation;
}
[[nodiscard]] const PaymentMethod &paymentMethod() const {
return _paymentMethod;
}
[[nodiscard]] const Ui::ShippingOptions &shippingOptions() const {
return _shippingOptions;
}
[[nodiscard]] rpl::producer<FormUpdate> updates() const {
return _updates.events();
}
void validateInformation(const Ui::RequestedInformation &information);
void validateCard(const Ui::UncheckedCardDetails &details);
void setPaymentCredentials(const NewCredentials &credentials);
void setShippingOption(const QString &id);
void submit();
private:
void fillInvoiceFromMessage();
void loadThumbnail(not_null<PhotoData*> photo);
[[nodiscard]] QImage prepareGoodThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const;
[[nodiscard]] QImage prepareBlurredThumbnail(
const std::shared_ptr<Data::PhotoMedia> &view) const;
[[nodiscard]] QImage prepareThumbnail(
not_null<const Image*> image,
bool blurred = false) const;
[[nodiscard]] QImage prepareEmptyThumbnail() const;
void requestForm();
void requestReceipt();
void processForm(const MTPDpayments_paymentForm &data);
void processReceipt(const MTPDpayments_paymentReceipt &data);
void processInvoice(const MTPDinvoice &data);
void processDetails(const MTPDpayments_paymentForm &data);
void processDetails(const MTPDpayments_paymentReceipt &data);
void processSavedInformation(const MTPDpaymentRequestedInfo &data);
void processSavedCredentials(
const MTPDpaymentSavedCredentialsCard &data);
void processShippingOptions(const QVector<MTPShippingOption> &data);
void fillPaymentMethodInformation();
void fillStripeNativeMethod();
void refreshPaymentMethodDetails();
[[nodiscard]] QString defaultPhone() const;
[[nodiscard]] QString defaultCountry() const;
void validateCard(
const StripePaymentMethod &method,
const Ui::UncheckedCardDetails &details);
bool validateInformationLocal(
const Ui::RequestedInformation &information) const;
[[nodiscard]] Error informationErrorLocal(
const Ui::RequestedInformation &information) const;
bool validateCardLocal(
const Ui::UncheckedCardDetails &details) const;
[[nodiscard]] Error cardErrorLocal(
const Ui::UncheckedCardDetails &details) const;
const not_null<Main::Session*> _session;
MTP::Sender _api;
FullMsgId _msgId;
FullMsgId _receiptMsgId;
Ui::Invoice _invoice;
std::unique_ptr<ThumbnailLoadProcess> _thumbnailLoadProcess;
FormDetails _details;
Ui::RequestedInformation _savedInformation;
PaymentMethod _paymentMethod;
Ui::RequestedInformation _validatedInformation;
mtpRequestId _validateRequestId = 0;
std::unique_ptr<Stripe::APIClient> _stripe;
Ui::ShippingOptions _shippingOptions;
QString _requestedInformationId;
rpl::event_stream<FormUpdate> _updates;
};
} // namespace Payments

View File

@@ -0,0 +1,18 @@
/*
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 Stripe {
enum class BillingAddressFields {
None,
Zip,
Full,
};
} // namespace Stripe

View File

@@ -0,0 +1,167 @@
/*
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 "stripe/stripe_api_client.h"
#include "stripe/stripe_error.h"
#include "stripe/stripe_token.h"
#include "stripe/stripe_form_encoder.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <crl/crl_on_main.h>
namespace Stripe {
namespace {
[[nodiscard]] QString APIURLBase() {
return "api.stripe.com/v1";
}
[[nodiscard]] QString TokenEndpoint() {
return "tokens";
}
[[nodiscard]] QString StripeAPIVersion() {
return "2015-10-12";
}
[[nodiscard]] QString SDKVersion() {
return "9.1.0";
}
[[nodiscard]] QString StripeUserAgentDetails() {
const auto details = QJsonObject{
{ "lang", "objective-c" },
{ "bindings_version", SDKVersion() },
};
return QString::fromUtf8(
QJsonDocument(details).toJson(QJsonDocument::Compact));
}
} // namespace
APIClient::APIClient(PaymentConfiguration configuration)
: _apiUrl("https://" + APIURLBase())
, _configuration(configuration) {
_additionalHttpHeaders = {
{ "X-Stripe-User-Agent", StripeUserAgentDetails() },
{ "Stripe-Version", StripeAPIVersion() },
{ "Authorization", "Bearer " + _configuration.publishableKey },
};
}
APIClient::~APIClient() {
const auto destroy = std::move(_old);
}
void APIClient::createTokenWithCard(
CardParams card,
TokenCompletionCallback completion) {
createTokenWithData(
FormEncoder::formEncodedDataForObject(MakeEncodable(card)),
std::move(completion));
}
void APIClient::createTokenWithData(
QByteArray data,
TokenCompletionCallback completion) {
const auto url = QUrl(_apiUrl + '/' + TokenEndpoint());
auto request = QNetworkRequest(url);
for (const auto &[name, value] : _additionalHttpHeaders) {
request.setRawHeader(name.toUtf8(), value.toUtf8());
}
destroyReplyDelayed(std::move(_reply));
_reply.reset(_manager.post(request, data));
const auto finish = [=](Token token, Error error) {
crl::on_main([
completion,
token = std::move(token),
error = std::move(error)
] {
completion(std::move(token), std::move(error));
});
};
const auto finishWithError = [=](Error error) {
finish(Token::Empty(), std::move(error));
};
const auto finishWithToken = [=](Token token) {
finish(std::move(token), Error::None());
};
QObject::connect(_reply.get(), &QNetworkReply::finished, [=] {
const auto replyError = int(_reply->error());
const auto replyErrorString = _reply->errorString();
const auto bytes = _reply->readAll();
destroyReplyDelayed(std::move(_reply));
auto parseError = QJsonParseError();
const auto document = QJsonDocument::fromJson(bytes, &parseError);
if (!bytes.isEmpty()) {
if (parseError.error != QJsonParseError::NoError) {
const auto code = int(parseError.error);
finishWithError({
Error::Code::JsonParse,
QString("InvalidJson%1").arg(code),
parseError.errorString(),
});
return;
} else if (!document.isObject()) {
finishWithError({
Error::Code::JsonFormat,
"InvalidJsonRoot",
"Not an object in JSON reply.",
});
return;
}
const auto object = document.object();
if (auto error = Error::DecodedObjectFromResponse(object)) {
finishWithError(std::move(error));
return;
}
}
if (replyError != QNetworkReply::NoError) {
finishWithError({
Error::Code::Network,
QString("RequestError%1").arg(replyError),
replyErrorString,
});
return;
}
auto token = Token::DecodedObjectFromAPIResponse(document.object());
if (!token) {
finishWithError({
Error::Code::JsonFormat,
"InvalidTokenJson",
"Could not parse token.",
});
}
finishWithToken(std::move(token));
});
}
void APIClient::destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply) {
if (!reply) {
return;
}
const auto raw = reply.get();
_old.push_back(std::move(reply));
QObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr);
raw->deleteLater();
QObject::connect(raw, &QObject::destroyed, [=] {
for (auto i = begin(_old); i != end(_old); ++i) {
if (i->get() == raw) {
i->release();
_old.erase(i);
break;
}
}
});
}
} // namespace Stripe

View File

@@ -0,0 +1,44 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "stripe/stripe_payment_configuration.h"
#include "stripe/stripe_card_params.h"
#include "stripe/stripe_callbacks.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtCore/QString>
#include <map>
namespace Stripe {
class APIClient final {
public:
explicit APIClient(PaymentConfiguration configuration);
~APIClient();
void createTokenWithCard(
CardParams card,
TokenCompletionCallback completion);
void createTokenWithData(
QByteArray data,
TokenCompletionCallback completion);
private:
void destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply);
QString _apiUrl;
PaymentConfiguration _configuration;
std::map<QString, QString> _additionalHttpHeaders;
QNetworkAccessManager _manager;
std::unique_ptr<QNetworkReply> _reply;
std::vector<std::unique_ptr<QNetworkReply>> _old;
};
} // namespace Stripe

View File

@@ -0,0 +1,19 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <functional>
namespace Stripe {
class Error;
class Token;
using TokenCompletionCallback = std::function<void(Token, Error)>;
} // namespace Stripe

View File

@@ -0,0 +1,188 @@
/*
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 "stripe/stripe_card.h"
#include "stripe/stripe_decode.h"
namespace Stripe {
namespace {
CardBrand BrandFromString(const QString &brand) {
if (brand == "visa") {
return CardBrand::Visa;
} else if (brand == "american express") {
return CardBrand::Amex;
} else if (brand == "mastercard") {
return CardBrand::MasterCard;
} else if (brand == "discover") {
return CardBrand::Discover;
} else if (brand == "jcb") {
return CardBrand::JCB;
} else if (brand == "diners club") {
return CardBrand::DinersClub;
} else {
return CardBrand::Unknown;
}
}
CardFundingType FundingFromString(const QString &funding) {
if (funding == "credit") {
return CardFundingType::Credit;
} else if (funding == "debit") {
return CardFundingType::Debit;
} else if (funding == "prepaid") {
return CardFundingType::Prepaid;
} else {
return CardFundingType::Other;
}
}
} // namespace
Card::Card(
QString id,
QString last4,
CardBrand brand,
quint32 expMonth,
quint32 expYear)
: _cardId(id)
, _last4(last4)
, _brand(brand)
, _expMonth(expMonth)
, _expYear(expYear) {
}
Card Card::Empty() {
return Card(QString(), QString(), CardBrand::Unknown, 0, 0);
}
Card Card::DecodedObjectFromAPIResponse(QJsonObject object) {
if (!ContainsFields(object, {
u"id",
u"last4",
u"brand",
u"exp_month",
u"exp_year"
})) {
return Card::Empty();
}
const auto string = [&](QStringView key) {
return object.value(key).toString();
};
const auto cardId = string(u"id");
const auto last4 = string(u"last4");
const auto brand = BrandFromString(string(u"brand").toLower());
const auto expMonth = object.value("exp_month").toInt();
const auto expYear = object.value("exp_year").toInt();
auto result = Card(cardId, last4, brand, expMonth, expYear);
result._name = string(u"name");
result._dynamicLast4 = string(u"dynamic_last4");
result._funding = FundingFromString(string(u"funding").toLower());
result._fingerprint = string(u"fingerprint");
result._country = string(u"country");
result._currency = string(u"currency");
result._addressLine1 = string(u"address_line1");
result._addressLine2 = string(u"address_line2");
result._addressCity = string(u"address_city");
result._addressState = string(u"address_state");
result._addressZip = string(u"address_zip");
result._addressCountry = string(u"address_country");
// TODO incomplete, not used.
//result._allResponseFields = object;
return result;
}
QString Card::cardId() const {
return _cardId;
}
QString Card::name() const {
return _name;
}
QString Card::last4() const {
return _last4;
}
QString Card::dynamicLast4() const {
return _dynamicLast4;
}
CardBrand Card::brand() const {
return _brand;
}
CardFundingType Card::funding() const {
return _funding;
}
QString Card::fingerprint() const {
return _fingerprint;
}
QString Card::country() const {
return _country;
}
QString Card::currency() const {
return _currency;
}
quint32 Card::expMonth() const {
return _expMonth;
}
quint32 Card::expYear() const {
return _expYear;
}
QString Card::addressLine1() const {
return _addressLine1;
}
QString Card::addressLine2() const {
return _addressLine2;
}
QString Card::addressCity() const {
return _addressCity;
}
QString Card::addressState() const {
return _addressState;
}
QString Card::addressZip() const {
return _addressZip;
}
QString Card::addressCountry() const {
return _addressCountry;
}
bool Card::empty() const {
return _cardId.isEmpty();
}
QString CardBrandToString(CardBrand brand) {
switch (brand) {
case CardBrand::Amex: return "American Express";
case CardBrand::DinersClub: return "Diners Club";
case CardBrand::Discover: return "Discover";
case CardBrand::JCB: return "JCB";
case CardBrand::MasterCard: return "MasterCard";
case CardBrand::Unknown: return "Unknown";
case CardBrand::Visa: return "Visa";
}
std::abort();
}
} // namespace Stripe

Some files were not shown because too many files have changed in this diff Show More