diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 95241634ca..105b633e7b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -825,6 +825,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org "lng_group_invite_create" = "Create an invite link"; "lng_group_invite_about" = "Telegram users will be able to join\nyour group by following this link."; +"lng_group_invite_about_channel" = "Telegram users will be able to join\nyour channel by following this link."; "lng_group_invite_create_new" = "Revoke invite link"; "lng_group_invite_about_new" = "Your previous link will be deactivated and we'll generate a new invite link for you."; "lng_group_invite_copied" = "Invite link copied to clipboard."; diff --git a/Telegram/SourceFiles/base/lambda_guard.h b/Telegram/SourceFiles/base/lambda_guard.h index da835b8e20..5c897bef94 100644 --- a/Telegram/SourceFiles/base/lambda_guard.h +++ b/Telegram/SourceFiles/base/lambda_guard.h @@ -78,7 +78,7 @@ public: Return operator()(OtherArgs &&...args) { return _guard ? _callable(std::forward(args)...) - : Return{}; + : Return(); } template < @@ -87,7 +87,7 @@ public: Return operator()(OtherArgs &&...args) const { return _guard ? _callable(std::forward(args)...) - : Return{}; + : Return(); } private: diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 2fef37d8e3..6fd7dba7ef 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -126,7 +126,7 @@ boxMediumSkip: 20px; boxButtonPadding: margins(8px, 12px, 13px, 12px); boxLayerButtonPadding: margins(8px, 8px, 8px, 8px); boxLabel: FlatLabel(defaultFlatLabel) { - width: 285px; + minWidth: 285px; align: align(topleft); style: boxLabelStyle; } @@ -159,7 +159,7 @@ cropMinSize: 20px; confirmInviteTitle: FlatLabel(defaultFlatLabel) { align: align(center); - width: 320px; + minWidth: 320px; maxHeight: 24px; textFg: windowBoldFg; style: TextStyle(defaultTextStyle) { @@ -170,7 +170,7 @@ confirmInviteTitle: FlatLabel(defaultFlatLabel) { } confirmInviteStatus: FlatLabel(boxLabel) { align: align(center); - width: 320px; + minWidth: 320px; maxHeight: 20px; textFg: windowSubTextFg; } @@ -183,13 +183,13 @@ confirmInviteUserPhotoSize: 56px; confirmInviteUserPhotoTop: 166px; confirmInviteUserName: FlatLabel(defaultFlatLabel) { align: align(center); - width: 66px; + minWidth: 66px; maxHeight: 20px; } confirmInviteUserNameTop: 227px; confirmPhoneAboutLabel: FlatLabel(defaultFlatLabel) { - width: 282px; + minWidth: 282px; } confirmPhoneCodeField: InputField(defaultInputField) { } @@ -199,7 +199,7 @@ revokePublicLinkStatusPalette: TextPalette(defaultTextPalette) { } aboutRevokePublicLabel: FlatLabel(defaultFlatLabel) { align: align(topleft); - width: 320px; + minWidth: 320px; } editBioCountdownLabel: FlatLabel(defaultFlatLabel) { style: boxTextStyle; @@ -394,7 +394,7 @@ notificationSampleSize: size(64px, 16px); membersAboutLimitPadding: margins(0px, 12px, 0px, 12px); membersAbout: FlatLabel(defaultFlatLabel) { - width: 332px; + minWidth: 332px; textFg: membersAboutLimitFg; align: align(top); style: boxLabelStyle; @@ -490,7 +490,7 @@ aboutVersionLink: LinkButton(defaultLinkButton) { aboutTextTop: 34px; aboutSkip: 14px; aboutLabel: FlatLabel(defaultFlatLabel) { - width: 330px; + minWidth: 330px; align: align(topleft); style: TextStyle(defaultTextStyle) { lineHeight: 22px; @@ -601,7 +601,7 @@ editPrivacyOptionMargin: margins(23px, 14px, 21px, 0px); editPrivacyPadding: margins(23px, 0px, 21px, 0px); editPrivacyWarningPadding: margins(23px, 14px, 21px, 0px); editPrivacyTitle: FlatLabel(defaultFlatLabel) { - width: 320px; + minWidth: 320px; textFg: boxTitleFg; maxHeight: 56px; style: TextStyle(defaultTextStyle) { @@ -612,7 +612,7 @@ editPrivacyTitle: FlatLabel(defaultFlatLabel) { } editPrivacyTitlePadding: margins(23px, 20px, 21px, 13px); editPrivacyLabel: FlatLabel(defaultFlatLabel) { - width: 320px; + minWidth: 320px; textFg: membersAboutLimitFg; style: defaultTextStyle; } @@ -624,13 +624,13 @@ changePhoneIcon: icon { { "phone_simcard_to", changePhoneSimcardTo, point(78px, 0px) } }; changePhoneDescription: FlatLabel(boxLabel) { - width: 332px; + minWidth: 332px; align: align(top); } changePhoneIconTop: 20px; changePhoneDescriptionTop: 96px; changePhoneLabel: FlatLabel(defaultFlatLabel) { - width: 275px; + minWidth: 275px; textFg: windowSubTextFg; } changePhoneError: FlatLabel(changePhoneLabel) { @@ -695,7 +695,7 @@ mutePhotoButton: PeerAvatarButton { photoSize: 40px; } muteChatTitle: FlatLabel(boxLabel) { - width: 235px; + minWidth: 235px; maxHeight: 20px; // block word wrap style: TextStyle(boxTextStyle) { font: font(boxFontSize semibold); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp new file mode 100644 index 0000000000..73ab6061fd --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -0,0 +1,970 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "boxes/peers/edit_peer_info_box.h" + +#include +#include +#include "info/profile/info_profile_button.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/widgets/input_fields.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/toast/toast.h" +#include "ui/special_buttons.h" +#include "boxes/confirm_box.h" +#include "boxes/photo_crop_box.h" +#include "boxes/add_contact_box.h" +#include "mtproto/sender.h" +#include "lang/lang_keys.h" +#include "core/file_utilities.h" +#include "mainwidget.h" +#include "apiwrap.h" +#include "application.h" +#include "auth_session.h" +#include "observer_peer.h" +#include "styles/style_boxes.h" +#include "styles/style_info.h" + +namespace { + +constexpr auto kUsernameCheckTimeout = TimeMs(200); + +class Controller + : private MTP::Sender + , private base::enable_weak_from_this { +public: + Controller( + not_null box, + not_null channel); + + object_ptr createContent(); + void setFocus(); + +private: + enum class Privacy { + Public, + Private, + }; + enum class Invites { + Everyone, + OnlyAdmins, + }; + enum class UsernameState { + Normal, + TooMany, + NotAvailable, + }; + struct Controls { + Ui::InputField *title = nullptr; + Ui::InputArea *description = nullptr; + Ui::NewAvatarButton *photo = nullptr; + rpl::lifetime initialPhotoImageWaiting; + + std::shared_ptr> privacy; + Ui::SlideWrap *usernameWrap = nullptr; + Ui::UsernameInput *username = nullptr; + base::unique_qptr usernameResult; + const style::FlatLabel *usernameResultStyle = nullptr; + + Ui::SlideWrap *createInviteLinkWrap = nullptr; + Ui::SlideWrap *editInviteLinkWrap = nullptr; + Ui::FlatLabel *inviteLink = nullptr; + + std::shared_ptr> invites; + Ui::Checkbox *signatures = nullptr; + }; + + base::lambda computeTitle() const; + object_ptr createPhotoAndTitleEdit(); + object_ptr createTitleEdit(); + object_ptr createPhotoEdit(); + object_ptr createDescriptionEdit(); + object_ptr createPrivaciesEdit(); + object_ptr createUsernameEdit(); + object_ptr createInviteLinkCreate(); + object_ptr createInviteLinkEdit(); + object_ptr createSignaturesEdit(); + object_ptr createInvitesEdit(); + object_ptr createDeleteButton(); + + void refreshInitialPhotoImage(); + void submitTitle(); + void submitDescription(); + void save(); + void deleteWithConfirmation(); + void choosePhotoDelayed(); + void choosePhoto(); + void suggestPhotoFile( + const FileDialog::OpenResult &result); + void suggestPhoto(const QImage &image); + void privacyChanged(Privacy value); + + void checkUsernameAvailability(); + void askUsernameRevoke(); + void usernameChanged(); + void showUsernameError(rpl::producer &&error); + void showUsernameGood(); + void showUsernameResult( + rpl::producer &&text, + not_null st); + + bool canEditInviteLink() const; + bool inviteLinkShown() const; + void refreshEditInviteLink(); + void refreshCreateInviteLink(); + void createInviteLink(); + void revokeInviteLink(); + void exportInviteLink(const QString &confirmation); + + not_null _box; + not_null _channel; + bool _isGroup; + + base::unique_qptr _wrap; + Controls _controls; + base::Timer _checkUsernameTimer; + mtpRequestId _checkUsernameRequestId = 0; + UsernameState _usernameState = UsernameState::Normal; + rpl::event_stream> _usernameResultTexts; + +}; + +Controller::Controller( + not_null box, + not_null channel) +: _box(box) +, _channel(channel) +, _isGroup(_channel->isMegagroup()) +, _checkUsernameTimer([this] { checkUsernameAvailability(); }) { + _box->setTitle(computeTitle()); + _box->addButton(langFactory(lng_settings_save), [this] { + save(); + }); + _box->addButton(langFactory(lng_cancel), [this] { + _box->closeBox(); + }); +} + +base::lambda Controller::computeTitle() const { + return langFactory(_isGroup + ? lng_edit_group + : lng_edit_channel_title); +} + +object_ptr Controller::createContent() { + auto result = object_ptr(_box); + _wrap.reset(result.data()); + _controls = Controls(); + + _wrap->add(createPhotoAndTitleEdit()); + _wrap->add(createDescriptionEdit()); + _wrap->add(createPrivaciesEdit()); + _wrap->add(createInviteLinkCreate()); + _wrap->add(createInviteLinkEdit()); + _wrap->add(createSignaturesEdit()); + _wrap->add(createInvitesEdit()); + _wrap->add(createDeleteButton()); + + _wrap->resizeToWidth(st::boxWideWidth); + + return result; +} + +void Controller::setFocus() { + if (_controls.title) { + _controls.title->setFocusFast(); + } +} + +object_ptr Controller::createPhotoAndTitleEdit() { + Expects(_wrap != nullptr); + + if (!_channel->canEditInformation()) { + return nullptr; + } + + auto result = object_ptr(_wrap); + auto container = result.data(); + + auto photoWrap = Ui::AttachParentChild( + container, + createPhotoEdit()); + auto titleEdit = Ui::AttachParentChild( + container, + createTitleEdit()); + photoWrap->heightValue() + | rpl::start_with_next([container](int height) { + container->resize(container->width(), height); + }, photoWrap->lifetime()); + container->widthValue() + | rpl::start_with_next([titleEdit](int width) { + auto left = st::editPeerPhotoMargins.left() + + st::editPeerPhotoSize; + titleEdit->resizeToWidth(width - left); + titleEdit->moveToLeft(left, 0, width); + }, titleEdit->lifetime()); + + return result; +} + +object_ptr Controller::createPhotoEdit() { + Expects(_wrap != nullptr); + + using PhotoWrap = Ui::PaddingWrap; + auto photoWrap = object_ptr( + _wrap, + object_ptr( + _wrap, + st::editPeerPhotoSize, + st::editPeerPhotoIconPosition), + st::editPeerPhotoMargins); + _controls.photo = photoWrap->entity(); + _controls.photo->addClickHandler([this] { choosePhotoDelayed(); }); + + _controls.initialPhotoImageWaiting = base::ObservableViewer( + Auth().downloaderTaskFinished()) + | rpl::start_with_next([=] { + refreshInitialPhotoImage(); + }); + refreshInitialPhotoImage(); + + return photoWrap; +} + +void Controller::refreshInitialPhotoImage() { + if (auto image = _channel->currentUserpic()) { + image->load(); + if (image->loaded()) { + _controls.photo->setImage(image->pixNoCache( + st::editPeerPhotoSize * cIntRetinaFactor(), + st::editPeerPhotoSize * cIntRetinaFactor(), + Images::Option::Smooth).toImage()); + _controls.initialPhotoImageWaiting.destroy(); + } + } else { + _controls.initialPhotoImageWaiting.destroy(); + } +} + +object_ptr Controller::createTitleEdit() { + Expects(_wrap != nullptr); + + auto result = object_ptr>( + _wrap, + object_ptr( + _wrap, + st::defaultInputField, + langFactory(_isGroup + ? lng_dlg_new_group_name + : lng_dlg_new_channel_name), + _channel->name), + st::editPeerTitleMargins); + + QObject::connect( + result->entity(), + &Ui::InputField::submitted, + [this] { submitTitle(); }); + + _controls.title = result->entity(); + return std::move(result); +} + +object_ptr Controller::createDescriptionEdit() { + Expects(_wrap != nullptr); + + auto result = object_ptr>( + _wrap, + object_ptr( + _wrap, + st::editPeerDescription, + langFactory(lng_create_group_description), + _channel->about()), + st::editPeerDescriptionMargins); + + QObject::connect( + result->entity(), + &Ui::InputArea::submitted, + [this] { submitDescription(); }); + + _controls.description = result->entity(); + return std::move(result); +} + +object_ptr Controller::createPrivaciesEdit() { + Expects(_wrap != nullptr); + + if (!_channel->canEditUsername()) { + return nullptr; + } + auto result = object_ptr>( + _wrap, + object_ptr(_wrap), + st::editPeerPrivaciesMargins); + auto container = result->entity(); + + _controls.privacy = std::make_shared>( + _channel->isPublic() ? Privacy::Public : Privacy::Private); + auto addButton = [&]( + Privacy value, + LangKey groupTextKey, + LangKey channelTextKey, + LangKey groupAboutKey, + LangKey channelAboutKey) { + container->add(object_ptr( + container, + st::editPeerPrivacyTopSkip)); + container->add(object_ptr>( + container, + _controls.privacy, + value, + lang(_isGroup ? groupTextKey : channelTextKey), + st::defaultBoxCheckbox)); + container->add(object_ptr>( + container, + object_ptr( + container, + Lang::Viewer(_isGroup ? groupAboutKey : channelAboutKey), + st::editPeerPrivacyLabel), + st::editPeerPrivacyLabelMargins)); + container->add(object_ptr( + container, + st::editPeerPrivacyBottomSkip)); + }; + addButton( + Privacy::Public, + lng_create_public_group_title, + lng_create_public_channel_title, + lng_create_public_group_about, + lng_create_public_channel_about); + addButton( + Privacy::Private, + lng_create_private_group_title, + lng_create_private_channel_title, + lng_create_private_group_about, + lng_create_private_channel_about); + container->add(createUsernameEdit()); + + _controls.privacy->setChangedCallback([this](Privacy value) { + privacyChanged(value); + }); + if (!_channel->isPublic()) { + checkUsernameAvailability(); + } + + return std::move(result); +} + +object_ptr Controller::createUsernameEdit() { + auto result = object_ptr>( + _wrap, + object_ptr(_wrap), + st::editPeerUsernameMargins); + _controls.usernameWrap = result.data(); + + auto container = result->entity(); + container->add(object_ptr( + container, + Lang::Viewer(lng_create_group_link), + st::editPeerSectionLabel)); + auto placeholder = container->add(object_ptr( + container)); + placeholder->setAttribute(Qt::WA_TransparentForMouseEvents); + _controls.username = Ui::AttachParentChild( + container, + object_ptr( + container, + st::setupChannelLink, + base::lambda(), + _channel->username, + true)); + _controls.username->heightValue() + | rpl::start_with_next([placeholder](int height) { + placeholder->resize(placeholder->width(), height); + }, placeholder->lifetime()); + placeholder->widthValue() + | rpl::start_with_next([this](int width) { + _controls.username->resize( + width, + _controls.username->height()); + }, placeholder->lifetime()); + _controls.username->move(placeholder->pos()); + + QObject::connect( + _controls.username, + &Ui::UsernameInput::changed, + [this] { usernameChanged(); }); + + auto shown = (_controls.privacy->value() == Privacy::Public); + result->toggle(shown, anim::type::instant); + + return std::move(result); +} + +void Controller::privacyChanged(Privacy value) { + auto toggleEditUsername = [&] { + _controls.usernameWrap->toggle( + (value == Privacy::Public), + anim::type::instant); + }; + auto refreshVisibilities = [&] { + // First we need to show everything, then hide anything. + // Otherwise the scroll position could jump up undesirably. + + if (value == Privacy::Public) { + toggleEditUsername(); + } + refreshCreateInviteLink(); + refreshEditInviteLink(); + if (value == Privacy::Public) { + _controls.usernameResult = nullptr; + checkUsernameAvailability(); + } else { + toggleEditUsername(); + } + }; + if (value == Privacy::Public) { + if (_usernameState == UsernameState::TooMany) { + askUsernameRevoke(); + return; + } else if (_usernameState == UsernameState::NotAvailable) { + _controls.privacy->setValue(Privacy::Private); + return; + } + refreshVisibilities(); + _controls.username->setDisplayFocused(true); + _controls.username->setFocus(); + } else { + request(base::take(_checkUsernameRequestId)).cancel(); + _checkUsernameTimer.cancel(); + refreshVisibilities(); + setFocus(); + } +} + +void Controller::checkUsernameAvailability() { + if (!_controls.username) { + return; + } + auto initial = (_controls.privacy->value() != Privacy::Public); + auto checking = initial + ? qsl(".bad.") + : _controls.username->getLastText().trimmed(); + if (checking.size() < MinUsernameLength) { + return; + } + if (_checkUsernameRequestId) { + request(_checkUsernameRequestId).cancel(); + } + _checkUsernameRequestId = request(MTPchannels_CheckUsername( + _channel->inputChannel, + MTP_string(checking) + )).done([=](const MTPBool &result) { + _checkUsernameRequestId = 0; + if (initial) { + return; + } + if (!mtpIsTrue(result) && checking != _channel->username) { + showUsernameError( + Lang::Viewer(lng_create_channel_link_occupied)); + } else { + showUsernameGood(); + } + }).fail([=](const RPCError &error) { + _checkUsernameRequestId = 0; + auto type = error.type(); + _usernameState = UsernameState::Normal; + if (type == qstr("CHANNEL_PUBLIC_GROUP_NA")) { + _usernameState = UsernameState::NotAvailable; + _controls.privacy->setValue(Privacy::Private); + } else if (rand() % 10 < 2 || type == qstr("CHANNELS_ADMIN_PUBLIC_TOO_MUCH")) { + _usernameState = UsernameState::TooMany; + if (_controls.privacy->value() == Privacy::Public) { + askUsernameRevoke(); + } + } else if (initial) { + if (_controls.privacy->value() == Privacy::Public) { + _controls.usernameResult = nullptr; + _controls.username->setFocus(); + } + } else if (type == qstr("USERNAME_INVALID")) { + showUsernameError( + Lang::Viewer(lng_create_channel_link_invalid)); + } else if (type == qstr("USERNAME_OCCUPIED") + && checking != _channel->username) { + showUsernameError( + Lang::Viewer(lng_create_channel_link_occupied)); + } + }).send(); +} + +void Controller::askUsernameRevoke() { + _controls.privacy->setValue(Privacy::Private); + auto revokeCallback = base::lambda_guarded(this, [this] { + _usernameState = UsernameState::Normal; + _controls.privacy->setValue(Privacy::Public); + checkUsernameAvailability(); + }); + Ui::show( + Box(std::move(revokeCallback)), + LayerOption::KeepOther); +} + +void Controller::usernameChanged() { + auto username = _controls.username->getLastText().trimmed(); + if (username.isEmpty()) { + _controls.usernameResult = nullptr; + _checkUsernameTimer.cancel(); + return; + } + auto bad = base::find_if(username, [](QChar ch) { + return (ch < 'A' || ch > 'Z') + && (ch < 'a' || ch > 'z') + && (ch < '0' || ch > '9') + && (ch != '_'); + }) != username.end(); + if (bad) { + showUsernameError( + Lang::Viewer(lng_create_channel_link_bad_symbols)); + } else if (username.size() < MinUsernameLength) { + showUsernameError( + Lang::Viewer(lng_create_channel_link_too_short)); + } else { + _controls.usernameResult = nullptr; + _checkUsernameTimer.callOnce(kUsernameCheckTimeout); + } +} + +void Controller::showUsernameError(rpl::producer &&error) { + showUsernameResult(std::move(error), &st::editPeerUsernameError); +} + +void Controller::showUsernameGood() { + showUsernameResult( + Lang::Viewer(lng_create_channel_link_available), + &st::editPeerUsernameGood); +} + +void Controller::showUsernameResult( + rpl::producer &&text, + not_null st) { + if (!_controls.usernameResult + || _controls.usernameResultStyle != st) { + _controls.usernameResultStyle = st; + _controls.usernameResult = base::make_unique_q( + _controls.usernameWrap, + _usernameResultTexts.events() | rpl::flatten_latest(), + *st); + auto label = _controls.usernameResult.get(); + label->show(); + label->widthValue() + | rpl::start_with_next([label] { + label->moveToRight( + st::editPeerUsernamePosition.x(), + st::editPeerUsernamePosition.y()); + }, label->lifetime()); + } + _usernameResultTexts.fire(std::move(text)); +} + +void Controller::createInviteLink() { + exportInviteLink(lang(_isGroup + ? lng_group_invite_about + : lng_group_invite_about_channel)); +} + +void Controller::revokeInviteLink() { + exportInviteLink(lang(lng_group_invite_about_new)); +} + +void Controller::exportInviteLink(const QString &confirmation) { + auto boxPointer = std::make_shared>(); + auto callback = base::lambda_guarded(this, [=] { + if (auto strong = *boxPointer) { + strong->closeBox(); + } + Auth().api().exportInviteLink(_channel); + }); + auto box = Box( + confirmation, + std::move(callback)); + *boxPointer = Ui::show(std::move(box), LayerOption::KeepOther); +} + +bool Controller::canEditInviteLink() const { + if (_channel->canEditUsername()) { + return true; + } + return (!_channel->isPublic() && _channel->canAddMembers()); +} + +bool Controller::inviteLinkShown() const { + return !_controls.privacy + || (_controls.privacy->value() == Privacy::Private); +} + +object_ptr Controller::createInviteLinkEdit() { + Expects(_wrap != nullptr); + + if (!canEditInviteLink()) { + return nullptr; + } + + auto result = object_ptr>( + _wrap, + object_ptr(_wrap), + st::editPeerInviteLinkMargins); + _controls.editInviteLinkWrap = result.data(); + + auto container = result->entity(); + container->add(object_ptr( + container, + Lang::Viewer(lng_profile_invite_link_section), + st::editPeerSectionLabel)); + container->add(object_ptr( + container, + st::editPeerInviteLinkSkip)); + + _controls.inviteLink = container->add(object_ptr( + container, + st::editPeerInviteLink)); + _controls.inviteLink->setSelectable(true); + _controls.inviteLink->setContextCopyText(QString()); + _controls.inviteLink->setBreakEverywhere(true); + _controls.inviteLink->setClickHandlerHook([this](auto&&...) { + Application::clipboard()->setText(_channel->inviteLink()); + Ui::Toast::Show(lang(lng_group_invite_copied)); + return false; + }); + + container->add(object_ptr( + container, + st::editPeerInviteLinkSkip)); + container->add(object_ptr( + container, + lang(lng_group_invite_create_new), + st::editPeerInviteLinkButton) + )->addClickHandler([this] { revokeInviteLink(); }); + + Notify::PeerUpdateValue( + _channel, + Notify::PeerUpdate::Flag::InviteLinkChanged) + | rpl::start_with_next([this] { + refreshEditInviteLink(); + }, _controls.editInviteLinkWrap->lifetime()); + + return std::move(result); +} + +void Controller::refreshEditInviteLink() { + auto link = _channel->inviteLink(); + auto text = TextWithEntities(); + if (!link.isEmpty()) { + text.text = link; + auto remove = qstr("https://"); + if (text.text.startsWith(remove)) { + text.text.remove(0, remove.size()); + } + text.entities.push_back(EntityInText( + EntityInTextCustomUrl, + 0, + text.text.size(), + link)); + } + _controls.inviteLink->setMarkedText(text); + + // Hack to expand FlatLabel width to naturalWidth again. + _controls.editInviteLinkWrap->resizeToWidth(st::boxWideWidth); + + _controls.editInviteLinkWrap->toggle( + inviteLinkShown() && !link.isEmpty(), + anim::type::instant); +} + +object_ptr Controller::createInviteLinkCreate() { + Expects(_wrap != nullptr); + + if (!canEditInviteLink()) { + return nullptr; + } + + auto result = object_ptr>( + _wrap, + object_ptr( + _wrap, + lang(lng_group_invite_create), + st::editPeerInviteLinkButton), + st::editPeerInviteLinkMargins); + result->entity()->addClickHandler([this] { + createInviteLink(); + }); + _controls.createInviteLinkWrap = result.data(); + + Notify::PeerUpdateValue( + _channel, + Notify::PeerUpdate::Flag::InviteLinkChanged) + | rpl::start_with_next([this] { + refreshCreateInviteLink(); + }, _controls.createInviteLinkWrap->lifetime()); + + return std::move(result); +} + +void Controller::refreshCreateInviteLink() { + auto link = _channel->inviteLink(); + _controls.createInviteLinkWrap->toggle( + inviteLinkShown() && link.isEmpty(), + anim::type::instant); +} + +object_ptr Controller::createSignaturesEdit() { + Expects(_wrap != nullptr); + + if (!_channel->canEditInformation() + || _channel->isMegagroup()) { + return nullptr; + } + auto result = object_ptr(_wrap); + auto container = result.data(); + container->add(object_ptr( + container, + st::defaultBoxCheckbox.margin.top())); + _controls.signatures = container->add( + object_ptr>( + container, + object_ptr( + container, + lang(lng_edit_sign_messages), + _channel->addsSignature(), + st::defaultBoxCheckbox), + st::editPeerSignaturesMargins))->entity(); + container->add(object_ptr( + container, + st::defaultBoxCheckbox.margin.bottom())); + return std::move(result); +} + +object_ptr Controller::createInvitesEdit() { + Expects(_wrap != nullptr); + + if (!_channel->canEditInformation() + || !_channel->isMegagroup()) { + return nullptr; + } + + auto result = object_ptr>( + _wrap, + object_ptr(_wrap), + st::editPeerInvitesMargins); + + auto container = result->entity(); + container->add(object_ptr( + container, + Lang::Viewer(lng_edit_group_who_invites), + st::editPeerSectionLabel)); + container->add(object_ptr( + container, + st::editPeerInvitesTopSkip)); + + _controls.invites = std::make_shared>( + _channel->anyoneCanAddMembers() + ? Invites::Everyone + : Invites::OnlyAdmins); + auto addButton = [&]( + Invites value, + LangKey textKey) { + container->add(object_ptr( + container, + st::editPeerInvitesSkip)); + container->add(object_ptr>( + container, + _controls.invites, + value, + lang(textKey), + st::defaultBoxCheckbox)); + }; + addButton( + Invites::Everyone, + lng_edit_group_invites_everybody); + addButton( + Invites::OnlyAdmins, + lng_edit_group_invites_only_admins); + + return std::move(result); +} + +object_ptr Controller::createDeleteButton() { + Expects(_wrap != nullptr); + + if (!_channel->canDelete()) { + return nullptr; + } + auto text = lang(_isGroup + ? lng_profile_delete_group + : lng_profile_delete_channel); + auto result = object_ptr>( + _wrap, + object_ptr( + _wrap, + text, + st::editPeerDeleteButton), + st::editPeerDeleteButtonMargins); + result->entity()->addClickHandler([this] { + deleteWithConfirmation(); + }); + return std::move(result); +} + +void Controller::submitTitle() { + Expects(_controls.title != nullptr); + Expects(_controls.description != nullptr); + + if (_controls.title->getLastText().isEmpty()) { + _controls.title->showError(); + } else { + _controls.description->setFocus(); + } +} + +void Controller::submitDescription() { + Expects(_controls.title != nullptr); + Expects(_controls.description != nullptr); + + if (_controls.title->getLastText().isEmpty()) { + _controls.title->showError(); + _controls.title->setFocus(); + } else { + save(); + } +} + +void Controller::save() { + Expects(_wrap != nullptr); + +} + +void Controller::deleteWithConfirmation() { + auto text = lang(_isGroup + ? lng_sure_delete_group + : lng_sure_delete_channel); + auto deleteCallback = [channel = _channel] { + Ui::hideLayer(); + Ui::showChatsList(); + if (auto chat = channel->migrateFrom()) { + App::main()->deleteAndExit(chat); + } + MTP::send( + MTPchannels_DeleteChannel(channel->inputChannel), + App::main()->rpcDone(&MainWidget::sentUpdatesReceived), + App::main()->rpcFail(&MainWidget::deleteChannelFailed)); + }; + Ui::show(Box( + text, + lang(lng_box_delete), + st::attentionBoxButton, + std::move(deleteCallback)), LayerOption::KeepOther); +} + +void Controller::choosePhotoDelayed() { + App::CallDelayed( + st::defaultRippleAnimation.hideDuration, + this, + [this] { choosePhoto(); }); +} + +void Controller::choosePhoto() { + auto handleChosenPhoto = base::lambda_guarded( + _controls.photo, + [this](auto &&result) { suggestPhotoFile(result); }); + + auto imgExtensions = cImgExtensions(); + auto filter = qsl("Image files (*") + imgExtensions.join(qsl(" *")) + qsl(");;") + FileDialog::AllFilesFilter(); + FileDialog::GetOpenPath( + lang(lng_choose_image), + filter, + std::move(handleChosenPhoto)); +} + +void Controller::suggestPhotoFile( + const FileDialog::OpenResult &result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + auto image = [&] { + if (!result.remoteContent.isEmpty()) { + return App::readImage(result.remoteContent); + } else if (!result.paths.isEmpty()) { + return App::readImage(result.paths.front()); + } + return QImage(); + }(); + suggestPhoto(image); +} + +void Controller::suggestPhoto(const QImage &image) { + auto badAspect = [](int a, int b) { + return (a >= 10 * b); + }; + if (image.isNull() + || badAspect(image.width(), image.height()) + || badAspect(image.height(), image.width())) { + Ui::show(Box(lang(lng_bad_photo))); + return; + } + + auto callback = [this](const QImage &cropped) { + _controls.photo->setImage(cropped); + }; + auto box = Ui::show( + Box(image, _channel), + LayerOption::KeepOther); + QObject::connect( + box, + &PhotoCropBox::ready, + base::lambda_guarded(_controls.photo, std::move(callback))); +} + +} // namespace + +EditPeerInfoBox::EditPeerInfoBox( + QWidget*, + not_null channel) +: _channel(channel) { +} + +void EditPeerInfoBox::prepare() { + auto controller = std::make_unique(this, _channel); + _focusRequests.events() + | rpl::start_with_next( + [c = controller.get()] { c->setFocus(); }, + lifetime()); + auto content = controller->createContent(); + content->heightValue() + | rpl::start_with_next([this](int height) { + setDimensions(st::boxWideWidth, height); + }, content->lifetime()); + setInnerWidget(object_ptr( + this, + std::move(content))); + Ui::AttachAsChild(this, std::move(controller)); +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h new file mode 100644 index 0000000000..545e03dd66 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h @@ -0,0 +1,41 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include +#include "boxes/peers/manage_peer_box.h" + +class EditPeerInfoBox : public BoxContent { +public: + EditPeerInfoBox(QWidget*, not_null channel); + + void setInnerFocus() override { + _focusRequests.fire({}); + } + +protected: + void prepare() override; + +private: + not_null _channel; + rpl::event_stream<> _focusRequests; + +}; diff --git a/Telegram/SourceFiles/boxes/peers/manage_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/manage_peer_box.cpp index 8cd0af7964..5fd40370de 100644 --- a/Telegram/SourceFiles/boxes/peers/manage_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/manage_peer_box.cpp @@ -22,6 +22,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include #include "lang/lang_keys.h" +#include "boxes/peers/edit_peer_info_box.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/labels.h" #include "history/history_admin_log_section.h" @@ -35,7 +36,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org namespace { -base::lambda ManageBoxTitle( +base::lambda ManagePeerTitle( not_null channel) { return langFactory(channel->isMegagroup() ? lng_manage_group_title @@ -83,6 +84,7 @@ void AddButtonWithCount( button, std::move(count), st::managePeerButtonLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); rpl::combine(button->widthValue(), label->widthValue()) | rpl::start_with_next([label](int outerWidth, int width) { label->moveToRight( @@ -118,9 +120,7 @@ void FillManageBox( Lang::Viewer(isGroup ? lng_manage_group_info : lng_manage_channel_info), - [=] { - - }, + [=] { Ui::show(Box(channel)); }, st::infoIconInformation); } if (HasRecentActions(channel)) { @@ -195,10 +195,14 @@ ManagePeerBox::ManagePeerBox( } bool ManagePeerBox::Available(not_null channel) { + // canViewMembers() is removed, because in supergroups you + // see them in profile and in channels only admins can see them. + // canViewAdmins() is removed, because in supergroups it is // always true and in channels it is equal to canViewBanned(). - return channel->canViewMembers() + return false +// || channel->canViewMembers() // || channel->canViewAdmins() || channel->canViewBanned() || channel->canEditInformation() @@ -208,7 +212,7 @@ bool ManagePeerBox::Available(not_null channel) { void ManagePeerBox::prepare() { _channel->updateFull(); - setTitle(ManageBoxTitle(_channel)); + setTitle(ManagePeerTitle(_channel)); addButton(langFactory(lng_cancel), [this] { closeBox(); }); setupContent(); diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index ecfec1bb47..07236e7834 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -95,7 +95,7 @@ callMuteRight: 8px; callNameTop: 15px; callName: FlatLabel(defaultFlatLabel) { - width: 260px; + minWidth: 260px; maxHeight: 30px; textFg: callNameFg; align: align(top); @@ -107,7 +107,7 @@ callName: FlatLabel(defaultFlatLabel) { } callStatusTop: 46px; callStatus: FlatLabel(defaultFlatLabel) { - width: 260px; + minWidth: 260px; maxHeight: 20px; textFg: callStatusFg; align: align(top); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index fc9e0dd858..954da83ad8 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -29,7 +29,7 @@ switchPmButton: RoundButton(defaultBoxButton) { textTop: 7px; } stickersRestrictedLabel: FlatLabel(defaultFlatLabel) { - width: 320px; + minWidth: 320px; align: align(center); textFg: noContactsColor; } diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 06c9991984..0a9dfd53d8 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -260,7 +260,6 @@ infoProfileStatusLeft: 109px; infoProfileStatusRight: 20px; infoProfileStatusTop: 58px; infoProfileStatusLabel: FlatLabel(defaultFlatLabel) { - width: 0px; maxHeight: 18px; textFg: windowSubTextFg; style: TextStyle(defaultTextStyle) { @@ -324,7 +323,6 @@ infoSharedMediaButtonIconPosition: point(20px, 3px); infoIconPosition: point(20px, 15px); infoLabeledOneLine: FlatLabel(defaultFlatLabel) { - width: 0px; // No need to set minWidth in one-line text. maxHeight: 20px; style: TextStyle(defaultTextStyle) { lineHeight: 19px; @@ -335,7 +333,7 @@ infoLabel: FlatLabel(infoLabeledOneLine) { textFg: windowSubTextFg; } infoLabeled: FlatLabel(infoLabeledOneLine) { - width: 180px; + minWidth: 180px; maxHeight: 0px; margin: margins(5px, 5px, 5px, 5px); } @@ -538,11 +536,58 @@ infoCommonGroupsList: PeerList(infoMembersList) { } managePeerButton: InfoProfileButton(infoProfileButton) { - padding: margins(76px, 10px, 76px, 8px); + padding: margins(76px, 12px, 76px, 10px); } -managePeerButtonIconPosition: infoSharedMediaButtonIconPosition; +managePeerButtonIconPosition: point(20px, 5px); managePeerButtonLabel: FlatLabel(defaultFlatLabel) { textFg: windowActiveTextFg; style: semiboldTextStyle; } managePeerButtonLabelPosition: point(25px, 10px); + +editPeerDeleteButtonMargins: margins(23px, 16px, 23px, 16px); +editPeerDeleteButton: sessionTerminateAllButton; +editPeerPhotoSize: newGroupPhotoSize; +editPeerPhotoIconPosition: newGroupPhotoIconPosition; +editPeerPhotoMargins: margins(23px, 16px, 23px, 8px); +editPeerTitle: defaultInputField; +editPeerTitleMargins: margins(27px, 21px, 23px, 8px); +editPeerDescription: newGroupDescription; +editPeerDescriptionMargins: margins(23px, 5px, 23px, 16px); +editPeerPrivaciesMargins: margins(23px, 10px, 23px, 0px); +editPeerPrivacyTopSkip: 10px; +editPeerPrivacyBottomSkip: 16px; +editPeerPrivacyLabel: FlatLabel(defaultFlatLabel) { + minWidth: 263px; + textFg: windowSubTextFg; +} +editPeerPrivacyLabelMargins: margins(34px, 0px, 0px, 0px); +editPeerSectionLabel: FlatLabel(boxTitle) { + style: TextStyle(defaultTextStyle) { + font: font(15px semibold); + linkFont: font(15px semibold); + linkFontOver: font(15px semibold underline); + } +} +editPeerUsername: setupChannelLink; +editPeerUsernameSkip: 8px; +editPeerInviteLink: FlatLabel(defaultFlatLabel) { + minWidth: 1px; // for break everywhere + style: boxTextStyle; +} +editPeerInviteLinkButton: boxLinkButton; +editPeerUsernameMargins: margins(0px, 10px, 0px, 0px); +editPeerUsernameGood: FlatLabel(defaultFlatLabel) { + textFg: boxTextFgGood; + style: boxTextStyle; +} +editPeerUsernameError: FlatLabel(editPeerUsernameGood) { + textFg: boxTextFgError; +} +editPeerUsernamePosition: point(0px, 10px); +editPeerInviteLinkSkip: 10px; +editPeerInviteLinkMargins: margins(23px, 10px, 14px, 0px); +editPeerSignaturesMargins: margins(23px, 16px, 23px, 8px); +editPeerInvitesMargins: margins(23px, 18px, 23px, 16px); +editPeerInvitesTopSkip: 10px; +editPeerInvitesSkip: 10px; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index edbbb179c4..f0b21ead57 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -235,8 +235,8 @@ object_ptr DetailsFiller::setupMuteToggle() { NotificationsEnabledValue(peer) )->addClickHandler([=] { App::main()->updateNotifySetting( - _peer, - _peer->isMuted() + peer, + peer->isMuted() ? NotifySettingSetNotify : NotifySettingSetMuted); }); diff --git a/Telegram/SourceFiles/profile/profile.style b/Telegram/SourceFiles/profile/profile.style index f21b350705..98afb49dc3 100644 --- a/Telegram/SourceFiles/profile/profile.style +++ b/Telegram/SourceFiles/profile/profile.style @@ -37,7 +37,7 @@ profileNameLeft: 26px; profileNameTop: 9px; profileNameLabel: FlatLabel(defaultFlatLabel) { margin: margins(10px, 5px, 10px, 5px); - width: 160px; + minWidth: 160px; maxHeight: 24px; textFg: windowBoldFg; style: TextStyle(defaultTextStyle) { @@ -98,15 +98,15 @@ profileBlockLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } profileBlockTextPart: FlatLabel(defaultFlatLabel) { - width: 180px; + minWidth: 180px; margin: margins(5px, 5px, 5px, 5px); } profileBlockOneLineTextPart: FlatLabel(profileBlockTextPart) { - width: 0px; // No need to set minWidth in one-line text. + minWidth: 0px; // No need to set minWidth in one-line text. maxHeight: 20px; } profileBioLabel: FlatLabel(profileBlockOneLineTextPart) { - width: 120px; + minWidth: 120px; maxHeight: 0px; } profileBlockOneLineSkip: 9px; @@ -116,7 +116,7 @@ profileEnableNotificationsTop: 7px; profileSettingsBlockSkip: 8px; profileInviteLinkText: FlatLabel(profileBlockTextPart) { - width: 1px; // Required for BreakEverywhere + minWidth: 1px; // Required for BreakEverywhere } profileLimitReachedSkip: 6px; @@ -135,7 +135,7 @@ profileMemberCreatorIconOver: icon {{ "profile_admin_star", profileAdminStarFgOv profileMemberAdminIcon: icon {{ "profile_admin_star", profileOtherAdminStarFg, point(4px, 3px) }}; profileMemberAdminIconOver: icon {{ "profile_admin_star", profileOtherAdminStarFgOver, point(4px, 3px) }}; profileLimitReachedLabel: FlatLabel(defaultFlatLabel) { - width: 180px; + minWidth: 180px; margin: margins(profileMemberPaddingLeft, 9px, profileMemberPaddingLeft, 6px); style: TextStyle(defaultTextStyle) { lineHeight: 19px; diff --git a/Telegram/SourceFiles/profile/profile_channel_controllers.cpp b/Telegram/SourceFiles/profile/profile_channel_controllers.cpp index fe08972d54..eee343b9f1 100644 --- a/Telegram/SourceFiles/profile/profile_channel_controllers.cpp +++ b/Telegram/SourceFiles/profile/profile_channel_controllers.cpp @@ -315,8 +315,10 @@ void ParticipantsBoxController::restoreState( loadMoreRows(); } PeerListController::restoreState(std::move(state)); - if (!_offset) { - setDescriptionText(QString()); + if (delegate()->peerListFullRowsCount() > 0) { + setNonEmptyDescription(); + } else if (_allLoaded) { + setDescriptionText(lang(lng_blocked_list_not_found)); } sortByOnline(); } @@ -442,11 +444,9 @@ void ParticipantsBoxController::loadMoreRows() { _loadRequestId = request(MTPchannels_GetParticipants(_channel->inputChannel, filter(), MTP_int(_offset), MTP_int(perPage))).done([this](const MTPchannels_ChannelParticipants &result) { Expects(result.type() == mtpc_channels_channelParticipants); + auto firstLoad = !_offset; _loadRequestId = 0; - if (!_offset) { - setDescriptionText((_role == Role::Restricted) ? lang(lng_group_blocked_list_about) : QString()); - } auto &participants = result.c_channels_channelParticipants(); App::feedUsers(participants.vusers); @@ -462,13 +462,26 @@ void ParticipantsBoxController::loadMoreRows() { }); } } - sortByOnline(); + if (delegate()->peerListFullRowsCount() > 0) { + sortByOnline(); + if (firstLoad) { + setNonEmptyDescription(); + } + } else if (_allLoaded) { + setDescriptionText(lang(lng_blocked_list_not_found)); + } delegate()->peerListRefreshRows(); }).fail([this](const RPCError &error) { _loadRequestId = 0; }).send(); } +void ParticipantsBoxController::setNonEmptyDescription() { + setDescriptionText((_role == Role::Kicked) + ? lang(lng_group_blocked_list_about) + : QString()); +} + bool ParticipantsBoxController::feedMegagroupLastParticipants() { if ((_role != Role::Members && _role != Role::Profile) || _offset > 0) { @@ -822,7 +835,8 @@ bool ParticipantsBoxController::removeRow(not_null user) { } else { delegate()->peerListRemoveRow(row); } - if (!delegate()->peerListFullRowsCount()) { + if (_role != Role::Kicked + && !delegate()->peerListFullRowsCount()) { setDescriptionText(lang(lng_blocked_list_not_found)); } return true; diff --git a/Telegram/SourceFiles/profile/profile_channel_controllers.h b/Telegram/SourceFiles/profile/profile_channel_controllers.h index 0c986f49f9..d0e4417e67 100644 --- a/Telegram/SourceFiles/profile/profile_channel_controllers.h +++ b/Telegram/SourceFiles/profile/profile_channel_controllers.h @@ -120,6 +120,7 @@ private: Role role, not_null additional); + void setNonEmptyDescription(); void setupSortByOnline(); void setupListChangeViewers(); void sortByOnlineDelayed(); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 03f321d840..c8a3addccc 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -49,7 +49,7 @@ settingsNameLeft: 26px; settingsNameTop: 9px; settingsNameLabel: FlatLabel(defaultFlatLabel) { margin: margins(10px, 5px, 10px, 5px); - width: 160px; + minWidth: 160px; maxHeight: 24px; textFg: windowBoldFg; style: TextStyle(defaultTextStyle) { @@ -98,12 +98,12 @@ settingsBlockLabel: FlatLabel(settingsPrimaryLabel) { textFg: windowSubTextFg; } settingsBlockOneLineTextPart: FlatLabel(settingsPrimaryLabel) { - width: 0px; // No need to set minWidth in one-line text. + minWidth: 0px; // No need to set minWidth in one-line text. margin: margins(5px, 5px, 5px, 5px); maxHeight: 20px; } settingsBioValue: FlatLabel(settingsBlockOneLineTextPart) { - width: 120px; + minWidth: 120px; maxHeight: 0px; } settingsSubSkip: 4px; diff --git a/Telegram/SourceFiles/ui/special_buttons.h b/Telegram/SourceFiles/ui/special_buttons.h index 5fe4f78318..26daae1ddc 100644 --- a/Telegram/SourceFiles/ui/special_buttons.h +++ b/Telegram/SourceFiles/ui/special_buttons.h @@ -168,6 +168,10 @@ public: void setImage(const QImage &image); + int naturalWidth() const override { + return height(); + } + protected: void paintEvent(QPaintEvent *e) override; diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index 39c95db127..27d1844cea 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -1744,7 +1744,12 @@ void FlatInput::onTextChange(const QString &text) { if (App::wnd()) App::wnd()->updateGlobalMenu(); } -InputArea::InputArea(QWidget *parent, const style::InputField &st, base::lambda placeholderFactory, const QString &val) : TWidget(parent) +InputArea::InputArea( + QWidget *parent, + const style::InputField &st, + base::lambda placeholderFactory, + const QString &val) +: RpWidget(parent) , _st(st) , _inner(this) , _oldtext(val) @@ -3272,7 +3277,12 @@ void InputField::setErrorShown(bool error) { } } -MaskedInputField::MaskedInputField(QWidget *parent, const style::InputField &st, base::lambda placeholderFactory, const QString &val) : TWidgetHelper(val, parent) +MaskedInputField::MaskedInputField( + QWidget *parent, + const style::InputField &st, + base::lambda placeholderFactory, + const QString &val) +: Parent(val, parent) , _st(st) , _oldtext(val) , _placeholderFactory(std::move(placeholderFactory)) { @@ -3350,15 +3360,18 @@ void MaskedInputField::onTouchTimer() { _touchRightButton = true; } -bool MaskedInputField::event(QEvent *e) { - if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) { - QTouchEvent *ev = static_cast(e); - if (ev->device()->type() == QTouchDevice::TouchScreen) { - touchEvent(ev); - return QLineEdit::event(e); +bool MaskedInputField::eventHook(QEvent *e) { + auto type = e->type(); + if (type == QEvent::TouchBegin + || type == QEvent::TouchUpdate + || type == QEvent::TouchEnd + || type == QEvent::TouchCancel) { + auto event = static_cast(e); + if (event->device()->type() == QTouchDevice::TouchScreen) { + touchEvent(event); } } - return QLineEdit::event(e); + return Parent::eventHook(e); } void MaskedInputField::touchEvent(QTouchEvent *e) { diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h index cfb77f7e05..a6fdd20e54 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.h +++ b/Telegram/SourceFiles/ui/widgets/input_fields.h @@ -324,11 +324,15 @@ enum class CtrlEnterSubmit { Both, }; -class InputArea : public TWidget, private base::Subscriber { +class InputArea : public RpWidget, private base::Subscriber { Q_OBJECT public: - InputArea(QWidget *parent, const style::InputField &st, base::lambda placeholderFactory = base::lambda(), const QString &val = QString()); + InputArea( + QWidget *parent, + const style::InputField &st, + base::lambda placeholderFactory = base::lambda(), + const QString &val = QString()); void showError(); @@ -687,9 +691,12 @@ private: bool _correcting = false; }; -class MaskedInputField : public TWidgetHelper, private base::Subscriber { +class MaskedInputField + : public RpWidgetWrap + , private base::Subscriber { Q_OBJECT + using Parent = RpWidgetWrap; public: MaskedInputField(QWidget *parent, const style::InputField &st, base::lambda placeholderFactory = base::lambda(), const QString &val = QString()); @@ -748,7 +755,7 @@ protected: void startBorderAnimation(); void startPlaceholderAnimation(); - bool event(QEvent *e) override; + bool eventHook(QEvent *e) override; void touchEvent(QTouchEvent *e); void paintEvent(QPaintEvent *e) override; void focusInEvent(QFocusEvent *e) override; diff --git a/Telegram/SourceFiles/ui/widgets/labels.cpp b/Telegram/SourceFiles/ui/widgets/labels.cpp index 623be2c41a..48769c5f75 100644 --- a/Telegram/SourceFiles/ui/widgets/labels.cpp +++ b/Telegram/SourceFiles/ui/widgets/labels.cpp @@ -139,7 +139,7 @@ void LabelSimple::paintEvent(QPaintEvent *e) { FlatLabel::FlatLabel(QWidget *parent, const style::FlatLabel &st) : RpWidget(parent) -, _text(st.width ? st.width : QFIXED_MAX) +, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _st(st) , _contextCopyText(lang(lng_context_copy_text)) { init(); @@ -151,7 +151,7 @@ FlatLabel::FlatLabel( InitType initType, const style::FlatLabel &st) : RpWidget(parent) -, _text(st.width ? st.width : QFIXED_MAX) +, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _st(st) , _contextCopyText(lang(lng_context_copy_text)) { if (initType == InitType::Rich) { @@ -167,7 +167,7 @@ FlatLabel::FlatLabel( rpl::producer &&text, const style::FlatLabel &st) : RpWidget(parent) -, _text(st.width ? st.width : QFIXED_MAX) +, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _st(st) , _contextCopyText(lang(lng_context_copy_text)) { textUpdated(); @@ -182,7 +182,7 @@ FlatLabel::FlatLabel( rpl::producer &&text, const style::FlatLabel &st) : RpWidget(parent) -, _text(st.width ? st.width : QFIXED_MAX) +, _text(st.minWidth ? st.minWidth : QFIXED_MAX) , _st(st) , _contextCopyText(lang(lng_context_copy_text)) { textUpdated(); @@ -259,7 +259,7 @@ QMargins FlatLabel::getMargins() const { int FlatLabel::countTextWidth() const { return _allowedWidth ? _allowedWidth - : (_st.width ? _st.width : _text.maxWidth()); + : (_st.minWidth ? _st.minWidth : _text.maxWidth()); } int FlatLabel::countTextHeight(int textWidth) { diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index 21fa464e69..5262003a77 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -29,7 +29,7 @@ LabelSimple { FlatLabel { margin: margins; - width: pixels; + minWidth: pixels; align: align; textFg: color; maxHeight: pixels; @@ -522,7 +522,7 @@ defaultLabelSimple: LabelSimple { } defaultFlatLabel: FlatLabel { - width: 0px; + minWidth: 0px; maxHeight: 0px; align: align(left); textFg: windowFg; diff --git a/Telegram/SourceFiles/ui/wrap/wrap.h b/Telegram/SourceFiles/ui/wrap/wrap.h index 807b57ff7c..eae83477ed 100644 --- a/Telegram/SourceFiles/ui/wrap/wrap.h +++ b/Telegram/SourceFiles/ui/wrap/wrap.h @@ -158,4 +158,42 @@ public: }; +class IgnoreMargins : public Wrap { + using Parent = Wrap; + +public: + IgnoreMargins(QWidget *parent, object_ptr &&child) + : Parent(parent, std::move(child)) { + if (auto weak = wrapped()) { + auto margins = weak->getMargins(); + resizeToWidth(weak->width() + - margins.left() + - margins.right()); + } + } + + QMargins getMargins() const override { + return QMargins(); + } + +protected: + int resizeGetHeight(int newWidth) override { + if (auto weak = wrapped()) { + weak->resizeToWidth(newWidth); + weak->moveToLeft(0, 0); + return weak->heightNoMargins(); + } + return height(); + } + +private: + void wrappedSizeUpdated(QSize size) override { + auto margins = wrapped()->getMargins(); + resize( + size.width() - margins.left() - margins.right(), + size.height() - margins.top() - margins.bottom()); + } + +}; + } // namespace Ui diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index eb13f292ca..d88e3f8bb9 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -34,6 +34,8 @@ <(src_loc)/base/virtual_method.h <(src_loc)/base/weak_unique_ptr.h <(src_loc)/base/zlib_help.h +<(src_loc)/boxes/peers/edit_peer_info_box.cpp +<(src_loc)/boxes/peers/edit_peer_info_box.h <(src_loc)/boxes/peers/manage_peer_box.cpp <(src_loc)/boxes/peers/manage_peer_box.h <(src_loc)/boxes/about_box.cpp