diff --git a/Telegram/Resources/icons/upload_chat_photo.png b/Telegram/Resources/icons/upload_chat_photo.png new file mode 100644 index 0000000000..c17cfb6547 Binary files /dev/null and b/Telegram/Resources/icons/upload_chat_photo.png differ diff --git a/Telegram/Resources/icons/upload_chat_photo@2x.png b/Telegram/Resources/icons/upload_chat_photo@2x.png new file mode 100644 index 0000000000..43d87f3f15 Binary files /dev/null and b/Telegram/Resources/icons/upload_chat_photo@2x.png differ diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 0e20edf8e4..bc20797a69 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -114,7 +114,6 @@ private: object_ptr createInvitesEdit(); object_ptr createDeleteButton(); - void refreshInitialPhotoImage(); void submitTitle(); void submitDescription(); void deleteWithConfirmation(); @@ -264,31 +263,9 @@ object_ptr Controller::createPhotoEdit() { st::editPeerPhotoMargins); _controls.photo = photoWrap->entity(); - _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); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 4d03403c21..2f26b0abbd 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -243,7 +243,6 @@ Cover::Cover( _name->setContextCopyText(lang(lng_profile_copy_fullname)); _status->setAttribute(Qt::WA_TransparentForMouseEvents); - initUserpicButton(); initViewers(); setupChildGeometry(); } @@ -275,10 +274,6 @@ Cover *Cover::setOnlineCount(rpl::producer &&count) { void Cover::initViewers() { using Flag = Notify::PeerUpdate::Flag; - Notify::PeerUpdateValue(_peer, Flag::PhotoChanged) - | rpl::start_with_next( - [this] { refreshUserpicLink(); }, - lifetime()); Notify::PeerUpdateValue(_peer, Flag::NameChanged) | rpl::start_with_next( [this] { refreshNameText(); }, @@ -288,12 +283,30 @@ void Cover::initViewers() { | rpl::start_with_next( [this] { refreshStatusText(); }, lifetime()); + if (!_peer->isUser()) { + Notify::PeerUpdateValue(_peer, + Flag::ChannelRightsChanged | Flag::ChatCanEdit) + | rpl::start_with_next( + [this] { refreshUploadPhotoOverlay(); }, + lifetime()); + } VerifiedValue(_peer) | rpl::start_with_next( [this](bool verified) { setVerified(verified); }, lifetime()); } +void Cover::refreshUploadPhotoOverlay() { + _userpic->switchChangePhotoOverlay([&] { + if (auto chat = _peer->asChat()) { + return chat->canEdit(); + } else if (auto channel = _peer->asChannel()) { + return channel->canEditInformation(); + } + return false; + }()); +} + void Cover::setVerified(bool verified) { if ((_verifiedCheck != nullptr) == verified) { return; @@ -313,29 +326,6 @@ void Cover::setVerified(bool verified) { refreshNameGeometry(width()); } -void Cover::initUserpicButton() { - _userpic->setClickedCallback([this] { - auto hasPhoto = (_peer->photoId != 0); - auto knownPhoto = (_peer->photoId != UnknownPeerPhotoId); - if (hasPhoto && knownPhoto) { - if (auto photo = App::photo(_peer->photoId)) { - if (photo->date) { - Messenger::Instance().showPhoto(photo, _peer); - } - } - } - }); -} - -void Cover::refreshUserpicLink() { - auto hasPhoto = (_peer->photoId != 0); - auto knownPhoto = (_peer->photoId != UnknownPeerPhotoId); - _userpic->setPointerCursor(hasPhoto && knownPhoto); - if (!knownPhoto) { - Auth().api().requestFullPeer(_peer); - } -} - void Cover::refreshNameText() { _name->setText(App::peerName(_peer)); refreshNameGeometry(width()); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.h b/Telegram/SourceFiles/info/profile/info_profile_cover.h index 302fb83ffd..3da087dd8b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.h +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.h @@ -79,12 +79,11 @@ public: private: void setupChildGeometry(); void initViewers(); - void initUserpicButton(); - void refreshUserpicLink(); void refreshNameText(); void refreshStatusText(); void refreshNameGeometry(int newWidth); void refreshStatusGeometry(int newWidth); + void refreshUploadPhotoOverlay(); void setVerified(bool verified); not_null _peer; diff --git a/Telegram/SourceFiles/ui/abstract_button.h b/Telegram/SourceFiles/ui/abstract_button.h index be353ff19c..0bf184cb41 100644 --- a/Telegram/SourceFiles/ui/abstract_button.h +++ b/Telegram/SourceFiles/ui/abstract_button.h @@ -83,6 +83,7 @@ protected: Down = (1 << 1), Disabled = (1 << 2), }; + friend constexpr bool is_flag_type(StateFlag) { return true; }; using State = base::flags; State state() const { diff --git a/Telegram/SourceFiles/ui/special_buttons.cpp b/Telegram/SourceFiles/ui/special_buttons.cpp index b0fe830c7b..e4e9ee87da 100644 --- a/Telegram/SourceFiles/ui/special_buttons.cpp +++ b/Telegram/SourceFiles/ui/special_buttons.cpp @@ -52,6 +52,74 @@ QPixmap CreateSquarePixmap(int width, Callback &&paintCallback) { return App::pixmapFromImageInPlace(std::move(image)); }; +template +void SuggestPhoto( + const QImage &image, + PeerId peerForCrop, + Callback &&callback) { + 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)), + LayerOption::KeepOther); + return; + } + + auto box = Ui::show( + Box(image, peerForCrop), + LayerOption::KeepOther); + box->ready() + | rpl::start_with_next( + std::forward(callback), + box->lifetime()); +} + +template +void SuggestPhotoFile( + const FileDialog::OpenResult &result, + PeerId peerForCrop, + Callback &&callback) { + 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, + peerForCrop, + std::forward(callback)); +} + +template +void ShowChoosePhotoBox(PeerId peerForCrop, Callback &&callback) { + auto imgExtensions = cImgExtensions(); + auto filter = qsl("Image files (*") + + imgExtensions.join(qsl(" *")) + + qsl(");;") + + FileDialog::AllFilesFilter(); + auto handleChosenPhoto = [ + peerForCrop, + callback = std::forward(callback) + ](auto &&result) mutable { + SuggestPhotoFile(result, peerForCrop, std::move(callback)); + }; + FileDialog::GetOpenPath( + lang(lng_choose_image), + filter, + std::move(handleChosenPhoto)); +} + } // namespace HistoryDownButton::HistoryDownButton(QWidget *parent, const style::TwoIconButton &st) : RippleButton(parent, st.ripple) @@ -387,63 +455,33 @@ void UserpicButton::setClickHandlerByRole() { } void UserpicButton::changePhotoLazy() { - auto imgExtensions = cImgExtensions(); - auto filter = qsl("Image files (*") - + imgExtensions.join(qsl(" *")) - + qsl(");;") - + FileDialog::AllFilesFilter(); - auto handleChosenPhoto = base::lambda_guarded( + auto callback = base::lambda_guarded( this, - [this](auto &&result) { suggestPhotoFile(result); }); - FileDialog::GetOpenPath( - lang(lng_choose_image), - filter, - std::move(handleChosenPhoto)); + [this](QImage &&image) { setImage(std::move(image)); }); + ShowChoosePhotoBox(_peerForCrop, std::move(callback)); } -void UserpicButton::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 UserpicButton::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)), - LayerOption::KeepOther); - return; - } - - auto box = Ui::show( - Box(image, _peerForCrop), - LayerOption::KeepOther); - box->ready() - | rpl::start_with_next([this](QImage &&image) { - setImage(std::move(image)); - }, box->lifetime()); +void UserpicButton::uploadNewPeerPhoto() { + auto callback = base::lambda_guarded( + this, + [this](QImage &&image) { + Messenger::Instance().uploadProfilePhoto( + std::move(image), + _peer->id + ); + }); + ShowChoosePhotoBox(_peerForCrop, std::move(callback)); } void UserpicButton::openPeerPhoto() { Expects(_peer != nullptr); Expects(_controller != nullptr); + if (_changeOverlayEnabled && _cursorInChangeOverlay) { + uploadNewPeerPhoto(); + return; + } + auto id = _peer->photoId; if (!id || id == UnknownPeerPhotoId) { return; @@ -524,6 +562,47 @@ void UserpicButton::paintEvent(QPaintEvent *e) { photoTop + iconTop, width()); } + } else if (_changeOverlayEnabled) { + auto current = _changeOverlayShown.current( + ms, + (isOver() || isDown()) ? 1. : 0.); + auto barHeight = anim::interpolate( + 0, + _st.uploadHeight, + current); + if (barHeight > 0) { + auto barLeft = photoLeft; + auto barTop = photoTop + _st.photoSize - barHeight; + auto rect = QRect( + barLeft, + barTop, + _st.photoSize, + barHeight); + p.setClipRect(rect); + { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(_st.uploadBg); + p.drawEllipse( + photoLeft, + photoTop, + _st.photoSize, + _st.photoSize); + } + auto iconLeft = (_st.uploadIconPosition.x() < 0) + ? (_st.photoSize - _st.uploadIcon.width()) / 2 + : _st.uploadIconPosition.x(); + auto iconTop = (_st.uploadIconPosition.y() < 0) + ? (_st.uploadHeight - _st.uploadIcon.height()) / 2 + : _st.uploadIconPosition.y(); + if (iconTop < barHeight) { + _st.uploadIcon.paint( + p, + barLeft + iconLeft, + barTop + iconTop, + width()); + } + } } } @@ -552,8 +631,8 @@ QPoint UserpicButton::prepareRippleStartPosition() const { void UserpicButton::processPeerPhoto() { Expects(_peer != nullptr); - bool hasPhoto = (_peer->photoId && _peer->photoId != UnknownPeerPhotoId); - setCursor(hasPhoto ? style::cur_pointer : style::cur_default); + auto hasPhoto = (_peer->photoId + && _peer->photoId != UnknownPeerPhotoId); _waiting = !_peer->userpicLoaded(); if (_waiting) { _peer->loadUserpic(true); @@ -561,10 +640,52 @@ void UserpicButton::processPeerPhoto() { if (_role == Role::OpenPhoto) { auto id = _peer->photoId; if (id == UnknownPeerPhotoId) { - _peer->updateFull(); + _peer->updateFullForced(); } - auto canOpen = (id != 0 && id != UnknownPeerPhotoId); - setCursor(canOpen ? style::cur_pointer : style::cur_default); + _canOpenPhoto = (id != 0 && id != UnknownPeerPhotoId); + updateCursor(); + } +} + +void UserpicButton::updateCursor() { + Expects(_role == Role::OpenPhoto); + + auto pointer = _canOpenPhoto + || (_changeOverlayEnabled && _cursorInChangeOverlay); + setPointerCursor(pointer); +} + +void UserpicButton::mouseMoveEvent(QMouseEvent *e) { + RippleButton::mouseMoveEvent(e); + if (_role == Role::OpenPhoto) { + updateCursorInChangeOverlay(e->pos()); + } +} + +void UserpicButton::updateCursorInChangeOverlay(QPoint localPos) { + auto photoPosition = countPhotoPosition(); + auto overlayRect = QRect( + photoPosition.x(), + photoPosition.y() + _st.photoSize - _st.uploadHeight, + _st.photoSize, + _st.uploadHeight); + auto inOverlay = overlayRect.contains(localPos); + setCursorInChangeOverlay(inOverlay); +} + +void UserpicButton::leaveEventHook(QEvent *e) { + if (_role == Role::OpenPhoto) { + setCursorInChangeOverlay(false); + } + return RippleButton::leaveEventHook(e); +} + +void UserpicButton::setCursorInChangeOverlay(bool inOverlay) { + Expects(_role == Role::OpenPhoto); + + if (_cursorInChangeOverlay != inOverlay) { + _cursorInChangeOverlay = inOverlay; + updateCursor(); } } @@ -606,7 +727,45 @@ void UserpicButton::startAnimation() { } void UserpicButton::switchChangePhotoOverlay(bool enabled) { + Expects(_role == Role::OpenPhoto); + if (_changeOverlayEnabled != enabled) { + _changeOverlayEnabled = enabled; + if (enabled) { + if (isOver()) { + startChangeOverlayAnimation(); + } + updateCursorInChangeOverlay( + mapFromGlobal(QCursor::pos())); + } else { + _changeOverlayShown.finish(); + update(); + } + } +} + +void UserpicButton::startChangeOverlayAnimation() { + auto over = isOver() || isDown(); + _changeOverlayShown.start( + [this] { update(); }, + over ? 0. : 1., + over ? 1. : 0., + st::slideWrapDuration); + update(); +} + +void UserpicButton::onStateChanged( + State was, + StateChangeSource source) { + RippleButton::onStateChanged(was, source); + if (_changeOverlayEnabled) { + auto mask = (StateFlag::Over | StateFlag::Down); + auto wasOver = (was & mask) != 0; + auto nowOver = (state() & mask) != 0; + if (wasOver != nowOver) { + startChangeOverlayAnimation(); + } + } } void UserpicButton::setImage(QImage &&image) { diff --git a/Telegram/SourceFiles/ui/special_buttons.h b/Telegram/SourceFiles/ui/special_buttons.h index 3ace412697..f32e09edd4 100644 --- a/Telegram/SourceFiles/ui/special_buttons.h +++ b/Telegram/SourceFiles/ui/special_buttons.h @@ -30,10 +30,6 @@ namespace Window { class Controller; } // namespace Window -namespace FileDialog { -struct OpenResult; -} // namespace FileDialog - namespace Ui { class HistoryDownButton : public RippleButton { @@ -180,7 +176,11 @@ public: } protected: - void paintEvent(QPaintEvent *e); + void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void leaveEventHook(QEvent *e) override; + + void onStateChanged(State was, StateChangeSource source) override; QImage prepareRippleMask() const override; QPoint prepareRippleStartPosition() const override; @@ -195,14 +195,16 @@ private: void startNewPhotoShowing(); void prepareUserpicPixmap(); QPoint countPhotoPosition() const; + void startChangeOverlayAnimation(); + void updateCursorInChangeOverlay(QPoint localPos); + void setCursorInChangeOverlay(bool inOverlay); + void updateCursor(); void grabOldUserpic(); void setClickHandlerByRole(); void openPeerPhoto(); void changePhotoLazy(); - void suggestPhotoFile( - const FileDialog::OpenResult &result); - void suggestPhoto(const QImage &image); + void uploadNewPeerPhoto(); const style::UserpicButton &_st; Window::Controller *_controller = nullptr; @@ -218,6 +220,11 @@ private: Animation _a_appearance; QImage _result; + bool _canOpenPhoto = false; + bool _cursorInChangeOverlay = false; + bool _changeOverlayEnabled = false; + Animation _changeOverlayShown; + }; } // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index 55df525d36..41b4bbcb94 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -500,6 +500,10 @@ UserpicButton { changeIcon: icon; changeIconPosition: point; duration: int; + uploadHeight: pixels; + uploadBg: color; + uploadIcon: icon; + uploadIconPosition: point; } InfoProfileButton { @@ -981,6 +985,7 @@ defaultImportantTooltipLabel: FlatLabel(defaultFlatLabel) { } defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }}; +defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }}; defaultUserpicButton: UserpicButton { size: size(76px, 76px); photoSize: 76px; @@ -989,6 +994,10 @@ defaultUserpicButton: UserpicButton { changeIcon: defaultChangeUserpicIcon; changeIconPosition: point(23px, 25px); duration: 500; + uploadHeight: 24px; + uploadBg: msgDateImgBgOver; + uploadIcon: defaultUploadUserpicIcon; + uploadIconPosition: point(-1px, 1px); } historyToDownBelow: icon { diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index c487347418..ed6112ba5e 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -199,7 +199,7 @@ void Controller::resizeForThirdSection() { auto extendBy = qMax( minimalThreeColumnWidth() - layout.bodyWidth, - st::columnMinimalWidthThird); + countThirdColumnWidthFromRatio(layout.bodyWidth)); auto newBodyWidth = layout.bodyWidth + extendBy; auto currentRatio = Auth().data().dialogsWidthRatio(); Auth().data().setDialogsWidthRatio(