diff --git a/Telegram/Resources/icons/chat/reply_type_channel.png b/Telegram/Resources/icons/chat/reply_type_channel.png new file mode 100644 index 000000000..8aa9e90f8 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_channel.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_channel@2x.png b/Telegram/Resources/icons/chat/reply_type_channel@2x.png new file mode 100644 index 000000000..cb8bb8c2b Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_channel@2x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_channel@3x.png b/Telegram/Resources/icons/chat/reply_type_channel@3x.png new file mode 100644 index 000000000..e659c35ac Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_channel@3x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_group.png b/Telegram/Resources/icons/chat/reply_type_group.png new file mode 100644 index 000000000..b40ba4b81 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_group.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_group@2x.png b/Telegram/Resources/icons/chat/reply_type_group@2x.png new file mode 100644 index 000000000..8ad079fb3 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_group@2x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_group@3x.png b/Telegram/Resources/icons/chat/reply_type_group@3x.png new file mode 100644 index 000000000..11dd58c19 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_group@3x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_user.png b/Telegram/Resources/icons/chat/reply_type_user.png new file mode 100644 index 000000000..f2d6cf6fc Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_user.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_user@2x.png b/Telegram/Resources/icons/chat/reply_type_user@2x.png new file mode 100644 index 000000000..8ef54da95 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_user@2x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_user@3x.png b/Telegram/Resources/icons/chat/reply_type_user@3x.png new file mode 100644 index 000000000..67a947f36 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_user@3x.png differ diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 54b16fec6..88f6a0785 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -89,6 +89,10 @@ private: : FrameSizeFromTag(tag); } +[[nodiscard]] QString InternalPrefix() { + return u"internal:"_q; +} + } // namespace class CustomEmojiLoader final @@ -514,6 +518,9 @@ std::unique_ptr CustomEmojiManager::create( Fn update, SizeTag tag, int sizeOverride) { + if (data.startsWith(InternalPrefix())) { + return internal(data); + } const auto parsed = ParseCustomEmojiData(data); return parsed ? create(parsed, std::move(update), tag, sizeOverride) @@ -540,6 +547,18 @@ std::unique_ptr CustomEmojiManager::create( }); } +std::unique_ptr CustomEmojiManager::internal( + QStringView data) { + const auto index = data.mid(InternalPrefix().size()).toInt(); + Assert(index >= 0 && index < _internalEmoji.size()); + + auto &info = _internalEmoji[index]; + return std::make_unique( + data.toString(), + info.image, + info.textColor); +} + void CustomEmojiManager::resolve( QStringView data, not_null listener) { @@ -885,6 +904,34 @@ uint64 CustomEmojiManager::coloredSetId() const { return _coloredSetId; } +QString CustomEmojiManager::registerInternalEmoji( + QImage emoji, + bool textColor) { + _internalEmoji.push_back({ std::move(emoji), textColor }); + return InternalPrefix() + QString::number(_internalEmoji.size() - 1); +} + +QString CustomEmojiManager::registerInternalEmoji( + const style::icon &icon, + bool textColor) { + const auto i = _iconEmoji.find(&icon); + if (i != end(_iconEmoji)) { + return i->second; + } + auto image = QImage( + icon.size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + image.setDevicePixelRatio(style::DevicePixelRatio()); + auto p = QPainter(&image); + icon.paint(p, 0, 0, icon.width()); + p.end(); + + const auto result = registerInternalEmoji(std::move(image), textColor); + _iconEmoji.emplace(&icon, result); + return result; +} + int FrameSizeFromTag(SizeTag tag) { const auto emoji = EmojiSizeFromTag(tag); const auto factor = style::DevicePixelRatio(); diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index 4c79f8870..01eb1fd79 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -83,11 +83,22 @@ public: [[nodiscard]] Main::Session &session() const; [[nodiscard]] Session &owner() const; + [[nodiscard]] QString registerInternalEmoji( + QImage emoji, + bool textColor = true); + [[nodiscard]] QString registerInternalEmoji( + const style::icon &icon, + bool textColor = true); + [[nodiscard]] uint64 coloredSetId() const; private: static constexpr auto kSizeCount = int(SizeTag::kCount); + struct InternalEmojiData { + QImage image; + bool textColor = true; + }; struct RepaintBunch { crl::time when = 0; std::vector> instances; @@ -131,6 +142,8 @@ private: SizeTag tag, int sizeOverride, LoaderFactory factory); + [[nodiscard]] std::unique_ptr internal( + QStringView data); [[nodiscard]] static int SizeIndex(SizeTag tag); const not_null _owner; @@ -163,6 +176,9 @@ private: bool _repaintTimerScheduled = false; bool _requestSetsScheduled = false; + std::vector _internalEmoji; + base::flat_map, QString> _iconEmoji; + #if 0 // inject-to-on_main crl::time _repaintsLastAdded = 0; rpl::lifetime _repaintsLifetime; diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 5dfadd9f6..e331703ae 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -477,7 +477,6 @@ HistoryMessageReply &HistoryMessageReply::operator=( HistoryMessageReply::~HistoryMessageReply() { // clearData() should be called by holder. Expects(resolvedMessage.empty()); - Expects(originalVia == nullptr); } bool HistoryMessageReply::updateData( @@ -523,44 +522,42 @@ bool HistoryMessageReply::updateData( } } + const auto repaint = [=] { holder->customEmojiRepaint(); }; + const auto context = Core::MarkedTextContext{ + .session = &holder->history()->session(), + .customEmojiRepaint = repaint, + }; const auto external = this->external(); - if (resolvedMessage + _multiline = 0; + // #TODO !_fields.storyId && (external || !_fields.quote.empty()); + + const auto displaying = resolvedMessage || resolvedStory - || (external && (!_fields.messageId || force))) { - const auto repaint = [=] { holder->customEmojiRepaint(); }; - const auto context = Core::MarkedTextContext{ - .session = &holder->history()->session(), - .customEmojiRepaint = repaint, - }; - const auto text = !_fields.quote.empty() - ? _fields.quote - : resolvedMessage - ? resolvedMessage->inReplyText() - : resolvedStory - ? resolvedStory->inReplyText() - : TextWithEntities{ u"..."_q }; - _text.setMarkedText( - st::defaultTextStyle, - text, - Ui::DialogTextOptions(), - context); + || (external && (!_fields.messageId || force)); + _displaying = displaying ? 1 : 0; - updateName(holder); + const auto unavailable = !resolvedMessage + && !resolvedStory + && ((!_fields.storyId && !_fields.messageId) || force); + _unavailable = unavailable ? 1 : 0; + + const auto text = !_fields.quote.empty() + ? _fields.quote + : resolvedMessage + ? resolvedMessage->inReplyText() + : resolvedStory + ? resolvedStory->inReplyText() + : TextWithEntities{ u"..."_q }; + _text.setMarkedText( + st::defaultTextStyle, + text, + Ui::DialogTextOptions(), + context); + + updateName(holder); + + if (_displaying) { setLinkFrom(holder); - if (resolvedMessage - && !resolvedMessage->Has()) { - if (const auto bot = resolvedMessage->viaBot()) { - originalVia = std::make_unique(); - originalVia->create( - &holder->history()->owner(), - peerToUser(bot->id)); - } - } - - if (!resolvedMessage && !resolvedStory) { - _unavailable = 1; - } - const auto media = resolvedMessage ? resolvedMessage->media() : nullptr; @@ -642,7 +639,6 @@ void HistoryMessageReply::setTopMessageId(MsgId topMessageId) { } void HistoryMessageReply::clearData(not_null holder) { - originalVia = nullptr; if (resolvedMessage) { holder->history()->owner().unregisterDependentMessage( holder, @@ -658,6 +654,12 @@ void HistoryMessageReply::clearData(not_null holder) { _name.clear(); _text.clear(); _unavailable = 1; + _displaying = 0; + _expandable = 0; + if (_multiline) { + holder->history()->owner().requestItemResize(holder); + _multiline = 0; + } refreshReplyToMedia(); } @@ -691,9 +693,10 @@ PeerData *HistoryMessageReply::sender(not_null holder) const { } QString HistoryMessageReply::senderName( - not_null holder) const { + not_null holder, + bool shorten) const { if (const auto peer = sender(holder)) { - return senderName(peer); + return senderName(peer, shorten); } else if (!resolvedMessage) { return _fields.externalSenderName; } else if (holder->Has()) { @@ -708,11 +711,11 @@ QString HistoryMessageReply::senderName( return QString(); } -QString HistoryMessageReply::senderName(not_null peer) const { - if (const auto user = originalVia ? peer->asUser() : nullptr) { - return user->firstName; - } - return peer->name(); +QString HistoryMessageReply::senderName( + not_null peer, + bool shorten) const { + const auto user = shorten ? peer->asUser() : nullptr; + return user ? user->firstName : peer->name(); } bool HistoryMessageReply::isNameUpdated( @@ -729,47 +732,101 @@ bool HistoryMessageReply::isNameUpdated( void HistoryMessageReply::updateName( not_null holder, std::optional resolvedSender) const { - const auto peer = resolvedSender.value_or(sender(holder)); - const auto name = peer ? senderName(peer) : senderName(holder); + auto viaBotUsername = QString(); + if (resolvedMessage + && !resolvedMessage->Has()) { + if (const auto bot = resolvedMessage->viaBot()) { + viaBotUsername = bot->username(); + } + } + const auto sender = resolvedSender.value_or(this->sender(holder)); + const auto externalPeer = _fields.externalPeerId + ? holder->history()->owner().peer(_fields.externalPeerId).get() + : nullptr; + const auto groupNameAdded = (externalPeer && externalPeer != sender); + const auto shorten = !viaBotUsername.isEmpty() || groupNameAdded; + const auto name = sender + ? senderName(sender, shorten) + : senderName(holder, shorten); const auto hasPreview = (resolvedStory && resolvedStory->hasReplyPreview()) || (resolvedMessage && resolvedMessage->media() && resolvedMessage->media()->hasReplyPreview()); - const auto textLeft = hasPreview - ? (st::messageQuoteStyle.outline - + st::historyReplyPreviewMargin.left() - + st::historyReplyPreview - + st::historyReplyPreviewMargin.right()) - : st::historyReplyPadding.left(); - if (!name.isEmpty()) { - _name.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); - if (peer) { - _nameVersion = peer->nameVersion(); - } - const auto w = _name.maxWidth() - + (originalVia - ? (st::msgServiceFont->spacew + originalVia->maxWidth) - : 0) - + (_fields.quote.empty() - ? 0 - : st::messageTextStyle.blockquote.icon.width()); - _maxWidth = std::max( - w, - std::min(_text.maxWidth(), st::maxSignatureSize)) - + (_fields.storyId - ? (st::dialogsMiniReplyStory.skipText - + st::dialogsMiniReplyStory.icon.icon.width()) - : 0); - } else { - _maxWidth = st::msgDateFont->width(statePhrase()); + const auto previewSkip = st::messageQuoteStyle.outline + + st::historyReplyPreviewMargin.left() + + st::historyReplyPreview + + st::historyReplyPreviewMargin.right() + - st::historyReplyPadding.left(); + const auto peerIcon = [](PeerData *peer) { + return !peer + ? &st::historyReplyUser + : peer->isBroadcast() + ? &st::historyReplyChannel + : (peer->isChannel() || peer->isChat()) + ? &st::historyReplyGroup + : &st::historyReplyUser; + }; + const auto peerEmoji = [&](PeerData *peer) { + const auto owner = &holder->history()->owner(); + return Ui::Text::SingleCustomEmoji( + owner->customEmojiManager().registerInternalEmoji( + *peerIcon(peer))); + }; + auto nameFull = TextWithEntities(); + if (!groupNameAdded && !_fields.storyId) { + nameFull.append(peerEmoji(sender)); } - _maxWidth = textLeft + nameFull.append(name); + if (groupNameAdded) { + nameFull.append(peerEmoji(externalPeer)); + nameFull.append(externalPeer->name()); + } + if (!viaBotUsername.isEmpty()) { + nameFull.append(u" @"_q).append(viaBotUsername); + } + const auto context = Core::MarkedTextContext{ + .session = &holder->history()->session(), + .customEmojiRepaint = [] {}, + .customEmojiLoopLimit = 1, + }; + _name.setMarkedText( + st::fwdTextStyle, + nameFull, + Ui::NameTextOptions(), + context); + if (sender) { + _nameVersion = sender->nameVersion(); + } + const auto nameMaxWidth = previewSkip + + _name.maxWidth() + + (hasQuoteIcon() + ? st::messageTextStyle.blockquote.icon.width() + : 0); + const auto storySkip = _fields.storyId + ? (st::dialogsMiniReplyStory.skipText + + st::dialogsMiniReplyStory.icon.icon.width()) + : 0; + const auto optimalTextSize = _multiline + ? countMultilineOptimalSize(previewSkip) + : QSize( + (previewSkip + + storySkip + + std::min(_text.maxWidth(), st::maxSignatureSize)), + st::normalFont->height); + _maxWidth = std::max(nameMaxWidth, optimalTextSize.width()); + if (!_displaying) { + const auto phraseWidth = st::msgDateFont->width(statePhrase()); + _maxWidth = _unavailable + ? phraseWidth + : std::max(_maxWidth, phraseWidth); + } + _maxWidth = st::historyReplyPadding.left() + _maxWidth + st::historyReplyPadding.right(); _minHeight = st::historyReplyPadding.top() + st::msgServiceNameFont->height - + st::normalFont->height + + optimalTextSize.height() + st::historyReplyPadding.bottom(); } @@ -785,17 +842,11 @@ int HistoryMessageReply::resizeToWidth(int width) const { + st::historyReplyPreview + st::historyReplyPreviewMargin.right()) : st::historyReplyPadding.left(); - if (originalVia) { - originalVia->resize(width - - textLeft - - st::historyReplyPadding.right() - - _name.maxWidth() - - st::msgServiceFont->spacew); - } - if (width >= _maxWidth) { + if (width >= _maxWidth || !_multiline) { _height = _minHeight; return height(); } + // #TODO _height = _minHeight; return height(); } @@ -826,6 +877,15 @@ void HistoryMessageReply::storyRemoved( } } +bool HistoryMessageReply::hasQuoteIcon() const { + return _fields.manualQuote && !_fields.quote.empty(); +} + +QSize HistoryMessageReply::countMultilineOptimalSize( + int firstLineSkip) const { + return QSize(); // #TODO +} + void HistoryMessageReply::paint( Painter &p, not_null holder, @@ -839,7 +899,7 @@ void HistoryMessageReply::paint( y += st::historyReplyTop; const auto rect = QRect(x, y, w, _height); - const auto hasQuote = _fields.manualQuote && !_fields.quote.empty(); + const auto hasQuote = hasQuoteIcon(); const auto selected = context.selected(); const auto colorPeer = resolvedMessage ? resolvedMessage->displayFrom() @@ -969,10 +1029,6 @@ void HistoryMessageReply::paint( ? FromNameFg(context, colorIndexPlusOne - 1) : stm->msgServiceFg->c); _name.drawLeftElided(p, x + textLeft, y + st::historyReplyPadding.top(), w, w + 2 * x + 2 * textLeft); - if (originalVia && w > _name.maxWidth() + st::msgServiceFont->spacew) { - p.setFont(st::msgServiceFont); - p.drawText(x + textLeft + _name.maxWidth() + st::msgServiceFont->spacew, y + st::historyReplyPadding.top() + st::msgServiceFont->ascent, originalVia->text); - } p.setPen(inBubble ? stm->historyTextFg diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index c250e91fb..55c4e149b 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -276,8 +276,12 @@ struct HistoryMessageReply [[nodiscard]] bool external() const; [[nodiscard]] PeerData *sender(not_null holder) const; - [[nodiscard]] QString senderName(not_null holder) const; - [[nodiscard]] QString senderName(not_null peer) const; + [[nodiscard]] QString senderName( + not_null holder, + bool shorten) const; + [[nodiscard]] QString senderName( + not_null peer, + bool shorten) const; [[nodiscard]] bool isNameUpdated(not_null holder) const; void updateName( not_null holder, @@ -340,7 +344,6 @@ struct HistoryMessageReply WebPageId replyToWebPageId = 0; ReplyToMessagePointer resolvedMessage; ReplyToStoryPointer resolvedStory; - std::unique_ptr originalVia; std::unique_ptr spoiler; struct { @@ -349,6 +352,10 @@ struct HistoryMessageReply } ripple; private: + [[nodiscard]] bool hasQuoteIcon() const; + [[nodiscard]] QSize countMultilineOptimalSize( + int firstLineSkip) const; + ReplyFields _fields; ClickHandlerPtr _link; mutable Ui::Text::String _name; @@ -359,6 +366,9 @@ private: mutable int _height = 0; mutable int _nameVersion = 0; uint8 _unavailable : 1 = 0; + uint8 _displaying : 1 = 0; + uint8 _multiline : 1 = 0; + uint8 _expandable : 1 = 0; }; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index f5a6d655d..833702101 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -771,13 +771,9 @@ QSize Message::performCountOptimalSize() { accumulate_max(maxWidth, namew); } if (reply) { - auto replyw = st::msgPadding.left() + const auto replyw = st::msgPadding.left() + reply->maxWidth() + st::msgPadding.right(); - if (reply->originalVia) { - replyw += st::msgServiceFont->spacew - + reply->originalVia->maxWidth; - } accumulate_max(maxWidth, replyw); } if (entry) { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 70018cad1..d4f189ac0 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -32,6 +32,12 @@ historyReplyBottom: 2px; historyReplyPreview: 32px; historyReplyPreviewMargin: margins(7px, 4px, 4px, 4px); historyReplyPadding: margins(11px, 2px, 6px, 2px); +historyReplyUser: icon {{ "chat/reply_type_user", windowFg }}; +historyReplyUserPadding: margins(0px, 4px, 4px, 0px); +historyReplyGroup: icon {{ "chat/reply_type_group", windowFg }}; +historyReplyGroupPadding: margins(0px, 4px, 4px, 0px); +historyReplyChannel: icon {{ "chat/reply_type_channel", windowFg }}; +historyReplyChannelPadding: margins(0px, 5px, 4px, 0px); msgReplyPadding: margins(6px, 6px, 11px, 6px); msgReplyBarPos: point(1px, 0px);