diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 636909863..ea6c5f2e8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4724,6 +4724,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boosts_prepaid_giveaway_status#one" = "{count} subscription {duration}"; "lng_boosts_prepaid_giveaway_status#other" = "{count} subscriptions {duration}"; +"lng_contact_add" = "Add"; +"lng_contact_send_message" = "message"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 7361373df..3e43cd47a 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1253,7 +1253,7 @@ const SharedContact *MediaContact::sharedContact() const { } TextWithEntities MediaContact::notificationText() const { - return tr::lng_in_dlg_contact(tr::now, Ui::Text::WithEntities); + return Ui::Text::Colorized(tr::lng_in_dlg_contact(tr::now)); } QString MediaContact::pinnedTextSubstring() const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index 7b0ac35b9..908bf1c0c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -7,28 +7,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_contact.h" -#include "core/click_handler_types.h" // ClickHandlerContext -#include "lang/lang_keys.h" -#include "layout/layout_selection.h" -#include "mainwindow.h" #include "boxes/add_contact_box.h" -#include "history/history_item_components.h" -#include "history/history_item.h" -#include "history/history.h" -#include "history/view/history_view_element.h" -#include "history/view/history_view_cursor_state.h" -#include "window/window_session_controller.h" -#include "ui/empty_userpic.h" -#include "ui/chat/chat_style.h" -#include "ui/text/format_values.h" // Ui::FormatPhone -#include "ui/text/text_options.h" -#include "ui/painter.h" +#include "core/click_handler_types.h" // ClickHandlerContext #include "data/data_session.h" #include "data/data_user.h" -#include "data/data_media_types.h" -#include "data/data_cloud_file.h" +#include "history/history.h" +#include "history/history_item_components.h" +#include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_reply.h" +#include "history/view/media/history_view_media_common.h" +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "ui/chat/chat_style.h" +#include "ui/empty_userpic.h" +#include "ui/painter.h" +#include "ui/power_saving.h" +#include "ui/rect.h" +#include "ui/text/format_values.h" // Ui::FormatPhone +#include "ui/text/text_options.h" +#include "window/window_session_controller.h" namespace HistoryView { namespace { @@ -81,17 +80,32 @@ Contact::Contact( const QString &last, const QString &phone) : Media(parent) -, _userId(userId) -, _fname(first) -, _lname(last) -, _phone(Ui::FormatPhone(phone)) { +, _st(st::historyPagePreview) +, _pixh(st::contactsPhotoSize) +, _userId(userId) { history()->owner().registerContactView(userId, parent); - _name.setText( - st::semiboldTextStyle, - tr::lng_full_name(tr::now, lt_first_name, first, lt_last_name, last).trimmed(), - Ui::NameTextOptions()); - _phonew = st::normalFont->width(_phone); + _nameLine.setText( + st::webPageTitleStyle, + tr::lng_full_name( + tr::now, + lt_first_name, + first, + lt_last_name, + last).trimmed(), + Ui::WebpageTextTitleOptions()); + + _phoneLine.setText( + st::webPageDescriptionStyle, + Ui::FormatPhone(QString(phone).replace(QChar('+'), QString())), + Ui::WebpageTextTitleOptions()); + +#if 0 // No info. + _infoLine.setText( + st::webPageDescriptionStyle, + phone, + Ui::WebpageTextTitleOptions()); +#endif } Contact::~Contact() { @@ -111,121 +125,300 @@ void Contact::updateSharedContactUserId(UserId userId) { } QSize Contact::countOptimalSize() { - const auto item = _parent->data(); - auto maxWidth = st::msgFileMinWidth; - _contact = _userId - ? item->history()->owner().userLoaded(_userId) + ? _parent->data()->history()->owner().userLoaded(_userId) : nullptr; if (_contact) { _contact->loadUserpic(); } else { - const auto full = _name.toString(); + const auto full = _nameLine.toString(); _photoEmpty = std::make_unique( Ui::EmptyUserpic::UserpicColor(Data::DecideColorIndex(_userId ? peerFromUser(_userId) : Data::FakePeerIdForJustName(full))), full); } - if (_contact && _contact->isContact()) { - _linkl = SendMessageClickHandler(_contact); - _link = tr::lng_profile_send_message(tr::now).toUpper(); - } else if (_userId) { - _linkl = AddContactClickHandler(_parent->data()); - _link = tr::lng_profile_add_contact(tr::now).toUpper(); - } - _linkw = _link.isEmpty() ? 0 : st::semiboldFont->width(_link); - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - - const auto tleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto tright = st.padding.right(); - if (_userId) { - accumulate_max(maxWidth, tleft + _phonew + tright); + _buttons.clear(); + if (_contact) { + const auto message = tr::lng_contact_send_message(tr::now).toUpper(); + _buttons.push_back({ + message, + st::semiboldFont->width(message), + SendMessageClickHandler(_contact), + }); + if (!_contact->isContact()) { + const auto add = tr::lng_contact_add(tr::now).toUpper(); + _buttons.push_back({ + add, + st::semiboldFont->width(add), + AddContactClickHandler(_parent->data()), + }); + } + _mainButton.link = _buttons.front().link; } else { - accumulate_max(maxWidth, tleft + _phonew + _parent->skipBlockWidth() + st::msgPadding.right()); +#if 0 // Can't view contact. + const auto view = tr::lng_profile_add_contact(tr::now).toUpper(); + _buttons.push_back({ + view, + st::semiboldFont->width(view), + AddContactClickHandler(_parent->data()), + }); +#endif + _mainButton.link = nullptr; } - accumulate_max(maxWidth, tleft + _name.maxWidth() + tright); - accumulate_min(maxWidth, st::msgMaxWidth); - auto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom(); - if (_parent->bottomInfoIsWide()) { - minHeight += st::msgDateFont->height - st::msgDateDelta.y(); + const auto padding = inBubblePadding() + innerMargin(); + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left(); + const auto lineHeight = UnitedLineHeight(); + + auto maxWidth = _parent->skipBlockWidth(); + auto minHeight = 0; + + auto textMinHeight = 0; + if (!_nameLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _nameLine.maxWidth()); + textMinHeight += 1 * lineHeight; } - if (!isBubbleTop()) { - minHeight -= st::msgFileTopMinus; + if (!_phoneLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _phoneLine.maxWidth()); + textMinHeight += 1 * lineHeight; } + if (!_infoLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _infoLine.maxWidth()); + textMinHeight += std::min(_infoLine.minHeight(), 1 * lineHeight); + } + minHeight = std::max(textMinHeight, st::contactsPhotoSize); + + if (!_buttons.empty()) { + auto buttonsWidth = rect::m::sum::h(st::historyPageButtonPadding); + for (const auto &button : _buttons) { + buttonsWidth += button.width; + } + accumulate_max(maxWidth, buttonsWidth); + } + maxWidth += rect::m::sum::h(padding); + minHeight += rect::m::sum::v(padding); + return { maxWidth, minHeight }; } void Contact::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; - auto paintw = width(); + if (width() < rect::m::sum::h(st::msgPadding) + 1) { + return; + } + const auto st = context.st; + const auto sti = context.imageStyle(); const auto stm = context.messageStyle(); - accumulate_min(paintw, maxWidth()); + const auto bubble = st::msgPadding; + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + auto tshift = inner.top(); - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus; - const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto nametop = st.nameTop - topMinus; - const auto nameright = st.padding.right(); - const auto statustop = st.statusTop - topMinus; - const auto linkshift = st::msgDateFont->height / 2; - const auto linktop = st.linkTop - topMinus - linkshift; - if (_userId) { - QRect rthumb(style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, paintw)); - if (_contact) { - const auto was = !_userpic.null(); - _contact->paintUserpic(p, _userpic, rthumb.x(), rthumb.y(), st.thumbSize); - if (!was && !_userpic.null()) { - history()->owner().registerHeavyViewPart(_parent); + const auto selected = context.selected(); + const auto view = parent(); + const auto colorIndex = _contact + ? _contact->colorIndex() + : Data::DecideColorIndex( + Data::FakePeerIdForJustName(_nameLine.toString())); + const auto cache = context.outbg + ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() + : st->coloredReplyCache(selected, colorIndex).get(); + const auto backgroundEmojiId = _contact + ? _contact->backgroundEmojiId() + : DocumentId(); + const auto backgroundEmoji = backgroundEmojiId + ? st->backgroundEmojiData(backgroundEmojiId).get() + : nullptr; + const auto backgroundEmojiCache = backgroundEmoji + ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( + selected, + context.outbg, + true, + colorIndex + 1)] + : nullptr; + Ui::Text::ValidateQuotePaintCache(*cache, _st); + Ui::Text::FillQuotePaint(p, outer, *cache, _st); + if (backgroundEmoji) { + ValidateBackgroundEmoji( + backgroundEmojiId, + backgroundEmoji, + backgroundEmojiCache, + cache, + view); + if (!backgroundEmojiCache->frames[0].isNull()) { + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto r = outer + - QMargins(0, 0, 0, rect::bottom(outer) - end); + FillBackgroundEmoji(p, r, false, *backgroundEmojiCache); + } + } + + if (_mainButton.ripple) { + _mainButton.ripple->paint( + p, + outer.x(), + outer.y(), + width(), + &cache->bg); + if (_mainButton.ripple->empty()) { + _mainButton.ripple = nullptr; + } + } + + { + const auto left = inner.left(); + const auto top = tshift; + if (_userId) { + if (_contact) { + const auto was = !_userpic.null(); + _contact->paintUserpic(p, _userpic, left, top, _pixh); + if (!was && !_userpic.null()) { + history()->owner().registerHeavyViewPart(_parent); + } + } else { + _photoEmpty->paintCircle(p, left, top, _pixh, _pixh); } } else { - _photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize); + _photoEmpty->paintCircle(p, left, top, _pixh, _pixh); } if (context.selected()) { - PainterHighQualityEnabler hq(p); + auto hq = PainterHighQualityEnabler(p); p.setBrush(p.textPalette().selectOverlay); p.setPen(Qt::NoPen); - p.drawEllipse(rthumb); + p.drawEllipse(left, top, _pixh, _pixh); } - - bool over = ClickHandler::showAsActive(_linkl); - p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont); - p.setPen(stm->msgFileThumbLinkFg); - p.drawTextLeft(nameleft, linktop, paintw, _link, _linkw); - } else { - _photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize); } - const auto namewidth = paintw - nameleft - nameright; - p.setFont(st::semiboldFont); - p.setPen(stm->historyFileNameFg); - _name.drawLeftElided(p, nameleft, nametop, namewidth, paintw); + const auto lineHeight = UnitedLineHeight(); + const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left(); + const auto lineWidth = rect::right(inner) - lineLeft; - p.setFont(st::normalFont); - p.setPen(stm->mediaFg); - p.drawTextLeft(nameleft, statustop, paintw, _phone); + { + p.setPen(cache->icon); + p.setTextPalette(context.outbg + ? stm->semiboldPalette + : st->coloredTextPalette(selected, colorIndex)); + + const auto endskip = _nameLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _nameLine.drawLeftElided( + p, + lineLeft, + tshift, + lineWidth, + width(), + 1, + style::al_left, + 0, + -1, + endskip, + false, + context.selection); + tshift += lineHeight; + + p.setTextPalette(stm->textPalette); + } + p.setPen(stm->historyTextFg); + { + tshift += st::lineWidth * 3; // Additional skip. + const auto endskip = _phoneLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _phoneLine.drawLeftElided( + p, + lineLeft, + tshift, + lineWidth, + width(), + 1, + style::al_left, + 0, + -1, + endskip, + false, + toTitleSelection(context.selection)); + tshift += 1 * lineHeight; + } + if (!_infoLine.isEmpty()) { + tshift += st::lineWidth * 3; // Additional skip. + const auto endskip = _infoLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _parent->prepareCustomEmojiPaint(p, context, _infoLine); + _infoLine.draw(p, { + .position = { lineLeft, tshift }, + .outerWidth = width(), + .availableWidth = lineWidth, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + .selection = toDescriptionSelection(context.selection), + .elisionHeight = (1 * lineHeight), + .elisionRemoveFromEnd = endskip, + }); + tshift += (1 * lineHeight); + } + + if (!_buttons.empty()) { + p.setFont(st::semiboldFont); + p.setPen(cache->icon); + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto line = st::historyPageButtonLine; + auto color = cache->icon; + color.setAlphaF(color.alphaF() * 0.3); + const auto top = end + st::historyPageButtonPadding.top(); + const auto buttonWidth = inner.width() / float64(_buttons.size()); + p.fillRect(inner.x(), end, inner.width(), line, color); + for (auto i = 0; i < _buttons.size(); i++) { + const auto &button = _buttons[i]; + const auto left = inner.x() + i * buttonWidth; + if (button.ripple) { + button.ripple->paint(p, left, end, buttonWidth, &cache->bg); + if (button.ripple->empty()) { + _buttons[i].ripple = nullptr; + } + } + p.drawText( + left + (buttonWidth - button.width) / 2, + top + st::semiboldFont->ascent, + button.text); + } + } } TextState Contact::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); - if (_userId) { - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus; - const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto linkshift = st::msgDateFont->height / 2; - const auto linktop = st.linkTop - topMinus - linkshift; - if (style::rtlrect(nameleft, linktop, _linkw, st::semiboldFont->height, width()).contains(point)) { - result.link = _linkl; - return result; + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + + _lastPoint = point; + + if (_buttons.size() > 1) { + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto line = st::historyPageButtonLine; + const auto bWidth = inner.width() / float64(_buttons.size()); + const auto bHeight = rect::bottom(outer) - end; + for (auto i = 0; i < _buttons.size(); i++) { + const auto left = inner.x() + i * bWidth; + if (QRectF(left, end, bWidth, bHeight).contains(point)) { + result.link = _buttons[i].link; + return result; + } } } - if (QRect(0, 0, width(), height()).contains(point) && _contact) { - result.link = _contact->openLink(); + if (outer.contains(point)) { + result.link = _mainButton.link; return result; } return result; @@ -239,4 +432,100 @@ bool Contact::hasHeavyPart() const { return !_userpic.null(); } +void Contact::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + const auto end = rect::bottom(inner) + _st.padding.bottom(); + if ((_lastPoint.y() < end) || (_buttons.size() <= 1)) { + if (p != _mainButton.link) { + return; + } + if (pressed) { + if (!_mainButton.ripple) { + const auto owner = &parent()->history()->owner(); + _mainButton.ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + outer.size(), + _st.radius), + [=] { owner->requestViewRepaint(parent()); }); + } + _mainButton.ripple->add(_lastPoint - outer.topLeft()); + } else if (_mainButton.ripple) { + _mainButton.ripple->lastStop(); + } + return; + } else if (_buttons.empty()) { + return; + } + const auto bWidth = inner.width() / float64(_buttons.size()); + const auto bHeight = rect::bottom(outer) - end; + for (auto i = 0; i < _buttons.size(); i++) { + const auto &button = _buttons[i]; + if (p != button.link) { + continue; + } + if (pressed) { + if (!button.ripple) { + const auto owner = &parent()->history()->owner(); + + _buttons[i].ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::MaskByDrawer( + QSize(bWidth, bHeight), + false, + [=](QPainter &p) { + p.drawRect(0, 0, bWidth, bHeight); + }), + [=] { owner->requestViewRepaint(parent()); }); + } + button.ripple->add(_lastPoint + - QPoint(inner.x() + i * bWidth, end)); + } else if (button.ripple) { + button.ripple->lastStop(); + } + } +} + +QMargins Contact::inBubblePadding() const { + return { + st::msgPadding.left(), + isBubbleTop() ? st::msgPadding.left() : 0, + st::msgPadding.right(), + isBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0 + }; +} + +QMargins Contact::innerMargin() const { + const auto button = _buttons.empty() ? 0 : st::historyPageButtonHeight; + return _st.padding + QMargins(0, 0, 0, button); +} + +int Contact::bottomInfoPadding() const { + if (!isBubbleBottom()) { + return 0; + } + + auto result = st::msgDateFont->height; + + // We use padding greater than st::msgPadding.bottom() in the + // bottom of the bubble so that the left line looks pretty. + // but if we have bottom skip because of the info display + // we don't need that additional padding so we replace it + // back with st::msgPadding.bottom() instead of left(). + result += st::msgPadding.bottom() - st::msgPadding.left(); + return result; +} + +TextSelection Contact::toTitleSelection(TextSelection selection) const { + return UnshiftItemSelection(selection, _nameLine); +} + +TextSelection Contact::toDescriptionSelection(TextSelection selection) const { + return UnshiftItemSelection(toTitleSelection(selection), _phoneLine); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.h b/Telegram/SourceFiles/history/view/media/history_view_contact.h index ecca595f2..2dd665b94 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.h +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.h @@ -12,11 +12,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class EmptyUserpic; +class RippleAnimation; } // namespace Ui namespace HistoryView { -class Contact : public Media { +class Contact final : public Media { public: Contact( not_null parent, @@ -29,7 +30,8 @@ public: void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; - bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { + bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override { return true; } bool dragItemByHandler(const ClickHandlerPtr &p) const override { @@ -43,16 +45,6 @@ public: return false; } - const QString &fname() const { - return _fname; - } - const QString &lname() const { - return _lname; - } - const QString &phone() const { - return _phone; - } - // Should be called only by Data::Session. void updateSharedContactUserId(UserId userId) override; @@ -62,18 +54,40 @@ public: private: QSize countOptimalSize() override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, bool pressed) override; + + [[nodiscard]] QMargins inBubblePadding() const; + [[nodiscard]] QMargins innerMargin() const; + [[nodiscard]] int bottomInfoPadding() const; + + [[nodiscard]] TextSelection toTitleSelection( + TextSelection selection) const; + [[nodiscard]] TextSelection toDescriptionSelection( + TextSelection selection) const; + + const style::QuoteStyle &_st; + const int _pixh; + UserId _userId = 0; UserData *_contact = nullptr; - int _phonew = 0; - QString _fname, _lname, _phone; - Ui::Text::String _name; + Ui::Text::String _nameLine; + Ui::Text::String _phoneLine; + Ui::Text::String _infoLine; + + struct Button { + QString text; + int width = 0; + ClickHandlerPtr link; + mutable std::unique_ptr ripple; + }; + std::vector