diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f63aaa046..49ba8d5c1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -936,6 +936,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org "lng_stickers_group_from_featured" = "Choose from trending stickers"; "lng_in_dlg_photo" = "Photo"; +"lng_in_dlg_album" = "Album"; "lng_in_dlg_video" = "Video"; "lng_in_dlg_audio_file" = "Audio file"; "lng_in_dlg_contact" = "Contact"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index cdbbb24c7..adbbf4d16 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -1608,18 +1608,18 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &msgs } if (!v) return; - QMap msgsIds; // copied from feedMsgs - for (int32 i = 0, l = v->size(); i < l; ++i) { - const auto &msg(v->at(i)); - switch (msg.type()) { - case mtpc_message: msgsIds.insert((uint64(uint32(msg.c_message().vid.v)) << 32) | uint64(i), i); break; - case mtpc_messageEmpty: msgsIds.insert((uint64(uint32(msg.c_messageEmpty().vid.v)) << 32) | uint64(i), i); break; - case mtpc_messageService: msgsIds.insert((uint64(uint32(msg.c_messageService().vid.v)) << 32) | uint64(i), i); break; - } + + auto indices = base::flat_map(); // copied from feedMsgs + for (auto i = 0, l = v->size(); i != l; ++i) { + const auto msgId = idFromMessage(v->at(i)); + indices.emplace((uint64(uint32(msgId)) << 32) | uint64(i), i); } - for_const (auto msgId, msgsIds) { - if (auto item = App::histories().addNewMessage(v->at(msgId), NewMessageExisting)) { + for (const auto [position, index] : indices) { + const auto item = App::histories().addNewMessage( + v->at(index), + NewMessageExisting); + if (item) { item->setPendingInitDimensions(); } } diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index 4d535917c..fbe6a9e19 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -1105,29 +1105,23 @@ namespace { } void feedMsgs(const QVector &msgs, NewMessageType type) { - QMap msgsIds; - for (int32 i = 0, l = msgs.size(); i < l; ++i) { - const auto &msg(msgs.at(i)); - switch (msg.type()) { - case mtpc_message: { - const auto &d(msg.c_message()); - bool needToAdd = true; + auto indices = base::flat_map(); + for (int i = 0, l = msgs.size(); i != l; ++i) { + const auto &msg = msgs[i]; + if (msg.type() == mtpc_message) { + const auto &data = msg.c_message(); if (type == NewMessageUnread) { // new message, index my forwarded messages to links overview - if (checkEntitiesAndViewsUpdate(d)) { // already in blocks + if (checkEntitiesAndViewsUpdate(data)) { // already in blocks LOG(("Skipping message, because it is already in blocks!")); - needToAdd = false; + continue; } } - if (needToAdd) { - msgsIds.insert((uint64(uint32(d.vid.v)) << 32) | uint64(i), i); - } - } break; - case mtpc_messageEmpty: msgsIds.insert((uint64(uint32(msg.c_messageEmpty().vid.v)) << 32) | uint64(i), i); break; - case mtpc_messageService: msgsIds.insert((uint64(uint32(msg.c_messageService().vid.v)) << 32) | uint64(i), i); break; } + const auto msgId = idFromMessage(msg); + indices.emplace((uint64(uint32(msgId)) << 32) | uint64(i), i); } - for (QMap::const_iterator i = msgsIds.cbegin(), e = msgsIds.cend(); i != e; ++i) { - histories().addNewMessage(msgs.at(i.value()), type); + for (const auto [position, index] : indices) { + histories().addNewMessage(msgs[index], type); } } @@ -2671,7 +2665,9 @@ namespace { p.setBrush(p.textPalette().selectOverlay); p.drawEllipse(rect); } else { - auto overlayCorners = (radius == ImageRoundRadius::Small) ? SelectedOverlaySmallCorners : SelectedOverlayLargeCorners; + auto overlayCorners = (radius == ImageRoundRadius::Small) + ? SelectedOverlaySmallCorners + : SelectedOverlayLargeCorners; auto overlayParts = RectPart::Full | RectPart::None; if (radius == ImageRoundRadius::Large) { complexAdjustRect(corners, rect, overlayParts); diff --git a/Telegram/SourceFiles/data/data_shared_media.cpp b/Telegram/SourceFiles/data/data_shared_media.cpp index 1ae21917e..767c54f42 100644 --- a/Telegram/SourceFiles/data/data_shared_media.cpp +++ b/Telegram/SourceFiles/data/data_shared_media.cpp @@ -291,7 +291,7 @@ base::optional SharedMediaWithLastSlice::IsLastIsolated( | [](HistoryItem *item) { return item ? item->getMedia() : nullptr; } | [](HistoryMedia *media) { return (media && media->type() == MediaTypePhoto) - ? static_cast(media)->photo() + ? static_cast(media)->photo().get() : nullptr; } | [](PhotoData *photo) { return photo ? photo->id : 0; } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 4749e219c..6f8c54add 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -796,12 +796,7 @@ void Histories::checkSelfDestructItems() { } HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, bool detachExistingItem) { - auto msgId = MsgId(0); - switch (msg.type()) { - case mtpc_messageEmpty: msgId = msg.c_messageEmpty().vid.v; break; - case mtpc_message: msgId = msg.c_message().vid.v; break; - case mtpc_messageService: msgId = msg.c_messageService().vid.v; break; - } + const auto msgId = idFromMessage(msg); if (!msgId) return nullptr; auto result = App::histItemById(channelId(), msgId); @@ -810,7 +805,10 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, result->detach(); } if (msg.type() == mtpc_message) { - result->updateMedia(msg.c_message().has_media() ? (&msg.c_message().vmedia) : 0); + const auto media = msg.c_message().has_media() + ? &msg.c_message().vmedia + : nullptr; + result->updateMedia(media); if (applyServiceAction) { App::checkSavedGif(result); } @@ -1094,23 +1092,23 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, return result; } -HistoryItem *History::createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *msg) { +not_null History::createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *msg) { return HistoryMessage::create(this, id, flags, date, from, postAuthor, msg); } -HistoryItem *History::createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { +not_null History::createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, postAuthor, doc, caption, markup); } -HistoryItem *History::createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) { +not_null History::createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) { return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, postAuthor, photo, caption, markup); } -HistoryItem *History::createItemGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup) { +not_null History::createItemGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup) { return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, postAuthor, game, markup); } -HistoryItem *History::addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags, bool newMsg) { +not_null History::addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags, bool newMsg) { auto message = HistoryService::PreparedText { text }; return addNewItem(HistoryService::create(this, msgId, date, message, flags), newMsg); } @@ -1147,19 +1145,19 @@ HistoryItem *History::addToHistory(const MTPMessage &msg) { return createItem(msg, false, false); } -HistoryItem *History::addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *item) { +not_null History::addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *item) { return addNewItem(createItemForwarded(id, flags, date, from, postAuthor, item), true); } -HistoryItem *History::addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { +not_null History::addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { return addNewItem(createItemDocument(id, flags, viaBotId, replyTo, date, from, postAuthor, doc, caption, markup), true); } -HistoryItem *History::addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) { +not_null History::addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) { return addNewItem(createItemPhoto(id, flags, viaBotId, replyTo, date, from, postAuthor, photo, caption, markup), true); } -HistoryItem *History::addNewGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup) { +not_null History::addNewGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup) { return addNewItem(createItemGame(id, flags, viaBotId, replyTo, date, from, postAuthor, game, markup), true); } @@ -1251,10 +1249,15 @@ void History::addUnreadMentionsSlice(const MTPmessages_Messages &result) { Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::UnreadMentionsChanged); } -HistoryItem *History::addNewItem(HistoryItem *adding, bool newMsg) { +not_null History::addNewItem(not_null adding, bool newMsg) { Expects(!isBuildingFrontBlock()); addItemToBlock(adding); + const auto [groupFrom, groupTill] = recountGroupingFromTill(adding); + if (groupFrom != groupTill || groupFrom->groupId()) { + recountGrouping(groupFrom, groupTill); + } + setLastMessage(adding); if (newMsg) { newItemAdded(adding); @@ -1434,8 +1437,7 @@ HistoryBlock *History::prepareBlockForAddingItem() { return result; }; -void History::addItemToBlock(HistoryItem *item) { - Expects(item != nullptr); +void History::addItemToBlock(not_null item) { Expects(item->detached()); auto block = prepareBlockForAddingItem(); @@ -1528,6 +1530,9 @@ void History::addOlderSlice(const QVector &slice) { return; } + auto firstAdded = (HistoryItem*)nullptr; + auto lastAdded = (HistoryItem*)nullptr; + auto logged = QStringList(); logged.push_back(QString::number(minMsgId())); logged.push_back(QString::number(maxMsgId())); @@ -1539,9 +1544,12 @@ void History::addOlderSlice(const QVector &slice) { for (auto i = slice.cend(), e = slice.cbegin(); i != e;) { --i; - auto adding = createItem(*i, false, true); + const auto adding = createItem(*i, false, true); if (!adding) continue; + if (!firstAdded) firstAdded = adding; + lastAdded = adding; + if (minAdded < 0 || minAdded > adding->id) { minAdded = adding->id; } @@ -1638,6 +1646,11 @@ void History::addOlderSlice(const QVector &slice) { CrashReports::ClearAnnotation("old_minmaxwas_minmaxadd"); + if (lastAdded) { + const auto [from, till] = recountGroupingFromTill(lastAdded); + recountGrouping(firstAdded, till); + } + if (isChannel()) { asChannelHistory()->checkJoinedMessage(); asChannelHistory()->checkMaxReadMessageDate(); @@ -1655,6 +1668,9 @@ void History::addNewerSlice(const QVector &slice) { } } + auto firstAdded = (HistoryItem*)nullptr; + auto lastAdded = (HistoryItem*)nullptr; + Assert(!isBuildingFrontBlock()); if (!slice.isEmpty()) { auto logged = QStringList(); @@ -1665,12 +1681,14 @@ void History::addNewerSlice(const QVector &slice) { auto maxAdded = -1; std::vector medias[Storage::kSharedMediaTypeCount]; - auto atLeastOneAdded = false; for (auto i = slice.cend(), e = slice.cbegin(); i != e;) { --i; - auto adding = createItem(*i, false, true); + const auto adding = createItem(*i, false, true); if (!adding) continue; + if (!firstAdded) firstAdded = adding; + lastAdded = adding; + if (minAdded < 0 || minAdded > adding->id) { minAdded = adding->id; } @@ -1679,7 +1697,6 @@ void History::addNewerSlice(const QVector &slice) { } addItemToBlock(adding); - atLeastOneAdded = true; if (auto types = adding->sharedMediaTypes()) { for (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) { auto type = static_cast(i); @@ -1696,7 +1713,7 @@ void History::addNewerSlice(const QVector &slice) { logged.push_back(QString::number(maxAdded)); CrashReports::SetAnnotation("new_minmaxwas_minmaxadd", logged.join(";")); - if (!atLeastOneAdded) { + if (!firstAdded) { newLoaded = true; setLastMessage(lastAvailableMessage()); } @@ -1709,6 +1726,11 @@ void History::addNewerSlice(const QVector &slice) { checkAddAllToUnreadMentions(); } + if (firstAdded) { + const auto [from, till] = recountGroupingFromTill(firstAdded); + recountGrouping(from, lastAdded); + } + if (isChannel()) asChannelHistory()->checkJoinedMessage(); checkLastMsg(); } @@ -2007,7 +2029,7 @@ void History::destroyUnreadBar() { } } -HistoryItem *History::addNewInTheMiddle(HistoryItem *newItem, int32 blockIndex, int32 itemIndex) { +not_null History::addNewInTheMiddle(not_null newItem, int32 blockIndex, int32 itemIndex) { Expects(blockIndex >= 0); Expects(blockIndex < blocks.size()); Expects(itemIndex >= 0); @@ -2029,9 +2051,126 @@ HistoryItem *History::addNewInTheMiddle(HistoryItem *newItem, int32 blockIndex, newItem->nextItemChanged(); } + const auto [groupFrom, groupTill] = recountGroupingFromTill(newItem); + if (groupFrom != groupTill || groupFrom->groupId()) { + recountGrouping(groupFrom, groupTill); + } + return newItem; } +HistoryItem *History::findNextItem(not_null item) const { + Expects(!item->detached()); + + const auto nextBlockIndex = item->block()->indexInHistory() + 1; + const auto nextItemIndex = item->indexInBlock() + 1; + if (nextItemIndex < int(item->block()->items.size())) { + return item->block()->items[nextItemIndex]; + } else if (nextBlockIndex < int(blocks.size())) { + return blocks[nextBlockIndex]->items.front(); + } + return nullptr; +} + +HistoryItem *History::findPreviousItem(not_null item) const { + Expects(!item->detached()); + + const auto blockIndex = item->block()->indexInHistory(); + const auto itemIndex = item->indexInBlock(); + if (itemIndex > 0) { + return item->block()->items[itemIndex - 1]; + } else if (blockIndex > 0) { + return blocks[blockIndex - 1]->items.back(); + } + return nullptr; +} + +not_null History::findGroupFirst( + not_null item) const { + const auto group = item->Get(); + Assert(group != nullptr); + Assert(group->leader != nullptr); + + const auto leaderGroup = (group->leader == item) + ? group + : group->leader->Get(); + Assert(leaderGroup != nullptr); + + return leaderGroup->others.empty() + ? group->leader + : leaderGroup->others.front().get(); +} + +not_null History::findGroupLast( + not_null item) const { + const auto group = item->Get(); + Assert(group != nullptr); + + return group->leader; +} + +auto History::recountGroupingFromTill(not_null item) +-> std::pair, not_null> { + const auto recountFromItem = [&] { + if (const auto prev = findPreviousItem(item)) { + if (prev->groupId()) { + return findGroupFirst(prev); + } + } + return item; + }(); + if (recountFromItem == item && !item->groupId()) { + return { item, item }; + } + const auto recountTillItem = [&] { + if (const auto next = findNextItem(item)) { + if (next->groupId()) { + return findGroupLast(next); + } + } + return item; + }(); + return { recountFromItem, recountTillItem }; +} + +void History::recountGrouping( + not_null from, + not_null till) { + Expects(!from->detached()); + Expects(!till->detached()); + + from->validateGroupId(); + auto others = std::vector>(); + auto currentGroupId = from->groupId(); + auto prev = from; + while (prev != till) { + auto item = findNextItem(prev); + item->validateGroupId(); + const auto groupId = item->groupId(); + if (currentGroupId) { + if (groupId == currentGroupId) { + others.push_back(prev); + } else { + for (const auto other : others) { + other->makeGroupMember(prev); + } + prev->makeGroupLeader(base::take(others)); + currentGroupId = groupId; + } + } else if (groupId) { + currentGroupId = groupId; + } + prev = item; + } + + if (currentGroupId) { + for (const auto other : others) { + other->makeGroupMember(prev); + } + till->makeGroupLeader(base::take(others)); + } +} + void History::startBuildingFrontBlock(int expectedItemsCount) { Assert(!isBuildingFrontBlock()); Assert(expectedItemsCount > 0); @@ -2471,7 +2610,7 @@ void History::setPinnedIndex(int pinnedIndex) { void History::changeMsgId(MsgId oldId, MsgId newId) { } -void History::removeBlock(HistoryBlock *block) { +void History::removeBlock(not_null block) { Expects(block->items.empty()); if (_buildingFrontBlock && block == _buildingFrontBlock->block) { @@ -2522,9 +2661,21 @@ void HistoryBlock::clear(bool leaveItems) { } } -void HistoryBlock::removeItem(HistoryItem *item) { +void HistoryBlock::removeItem(not_null item) { Expects(item->block() == this); + auto [groupFrom, groupTill] = _history->recountGroupingFromTill(item); + const auto groupHistory = _history; + const auto needGroupRecount = (groupFrom != groupTill); + if (needGroupRecount) { + if (groupFrom == item) { + groupFrom = groupHistory->findNextItem(groupFrom); + } + if (groupTill == item) { + groupTill = groupHistory->findPreviousItem(groupTill); + } + } + auto blockIndex = indexInHistory(); auto itemIndex = item->indexInBlock(); if (_history->showFrom == item) { @@ -2558,4 +2709,8 @@ void HistoryBlock::removeItem(HistoryItem *item) { if (items.empty()) { delete this; } + + if (needGroupRecount) { + groupHistory->recountGrouping(groupFrom, groupTill); + } } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 1ee8a690f..c3b6fadce 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -136,6 +136,7 @@ enum HistoryMediaType { MediaTypeVoiceFile, MediaTypeGame, MediaTypeInvoice, + MediaTypeGrouped, MediaTypeCount }; @@ -217,13 +218,13 @@ public: virtual ~History(); - HistoryItem *addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags = 0, bool newMsg = true); HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type); HistoryItem *addToHistory(const MTPMessage &msg); - HistoryItem *addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *item); - HistoryItem *addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); - HistoryItem *addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); - HistoryItem *addNewGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup); + not_null addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags = 0, bool newMsg = true); + not_null addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *item); + not_null addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); + not_null addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); + not_null addNewGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup); // Used only internally and for channel admin log. HistoryItem *createItem(const MTPMessage &msg, bool applyServiceAction, bool detachExistingItem); @@ -475,17 +476,17 @@ protected: // this method just removes a block from the blocks list // when the last item from this block was detached and // calls the required previousItemChanged() - void removeBlock(HistoryBlock *block); + void removeBlock(not_null block); void clearBlocks(bool leaveItems); - HistoryItem *createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *msg); - HistoryItem *createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); - HistoryItem *createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); - HistoryItem *createItemGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup); + not_null createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *msg); + not_null createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); + not_null createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); + not_null createItemGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup); - HistoryItem *addNewItem(HistoryItem *adding, bool newMsg); - HistoryItem *addNewInTheMiddle(HistoryItem *newItem, int32 blockIndex, int32 itemIndex); + not_null addNewItem(not_null adding, bool newMsg); + not_null addNewInTheMiddle(not_null newItem, int32 blockIndex, int32 itemIndex); // All this methods add a new item to the first or last block // depending on if we are in isBuildingFronBlock() state. @@ -493,7 +494,7 @@ protected: // Adds the item to the back or front block, depending on // isBuildingFrontBlock(), creating the block if necessary. - void addItemToBlock(HistoryItem *item); + void addItemToBlock(not_null item); // Usually all new items are added to the last block. // Only when we scroll up and add a new slice to the @@ -517,6 +518,18 @@ private: void clearSendAction(not_null from); + HistoryItem *findPreviousItem(not_null item) const; + HistoryItem *findNextItem(not_null item) const; + not_null findGroupFirst( + not_null item) const; + not_null findGroupLast( + not_null item) const; + auto recountGroupingFromTill(not_null item) + -> std::pair, not_null>; + void recountGrouping( + not_null from, + not_null till); + enum class Flag { f_has_pending_resized_items = (1 << 0), f_pending_resize = (1 << 1), @@ -624,7 +637,7 @@ public: ~HistoryBlock() { clear(); } - void removeItem(HistoryItem *item); + void removeItem(not_null item); int resizeGetHeight(int newWidth, bool resizeAllItems); int y() const { diff --git a/Telegram/SourceFiles/history/history.style b/Telegram/SourceFiles/history/history.style index 2fb07d036..67a5f4eed 100644 --- a/Telegram/SourceFiles/history/history.style +++ b/Telegram/SourceFiles/history/history.style @@ -461,3 +461,9 @@ historyFastShareIcon: icon {{ "fast_share", msgServiceFg, point(4px, 3px)}}; historyGoToOriginalIcon: icon {{ "title_back-flip_horizontal", msgServiceFg, point(8px, 7px) }}; historySavedFont: font(semibold 14px); + +historyGroupWidthMax: maxMediaSize; +historyGroupWidthMin: minPhotoSize; +historyGroupSkip: 4px; +historyGroupRadialSize: 44px; +historyGroupRadialLine: 3px; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index b80c4d5d2..7e710ca5d 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -24,6 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "mainwidget.h" #include "history/history_service_layout.h" #include "history/history_media_types.h" +#include "history/history_media_grouped.h" #include "history/history_message.h" #include "media/media_clip_reader.h" #include "styles/style_dialogs.h" @@ -563,7 +564,8 @@ HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default; HistoryMediaPtr::HistoryMediaPtr() = default; -HistoryMediaPtr::HistoryMediaPtr(std::unique_ptr pointer) : _pointer(std::move(pointer)) { +HistoryMediaPtr::HistoryMediaPtr(std::unique_ptr pointer) +: _pointer(std::move(pointer)) { if (_pointer) { _pointer->attachToParent(); } @@ -768,6 +770,11 @@ void HistoryItem::detach() { void HistoryItem::detachFast() { _block = nullptr; _indexInBlock = -1; + + validateGroupId(); + if (groupId()) { + makeGroupLeader({}); + } } Storage::SharedMediaTypesMask HistoryItem::sharedMediaTypes() const { @@ -1116,6 +1123,88 @@ void HistoryItem::setUnreadBarFreezed() { } } +bool HistoryItem::groupIdValidityChanged() { + if (Has()) { + if (_media && _media->canBeGrouped()) { + return false; + } + RemoveComponents(HistoryMessageGroup::Bit()); + setPendingInitDimensions(); + return true; + } + return false; +} + +void HistoryItem::makeGroupMember(not_null leader) { + Expects(leader != this); + + const auto group = Get(); + Assert(group != nullptr); + if (group->leader == this) { + if (auto single = _media ? _media->takeLastFromGroup() : nullptr) { + _media = std::move(single); + } + _flags |= MTPDmessage_ClientFlag::f_hidden_by_group; + setPendingInitDimensions(); + + group->leader = leader; + base::take(group->others); + } else if (group->leader != leader) { + group->leader = leader; + } + + Ensures(isHiddenByGroup()); + Ensures(group->others.empty()); +} + +void HistoryItem::makeGroupLeader( + std::vector> &&others) { + const auto group = Get(); + Assert(group != nullptr); + + if (group->leader != this) { + group->leader = this; + _flags &= ~MTPDmessage_ClientFlag::f_hidden_by_group; + setPendingInitDimensions(); + } + group->others = std::move(others); + if (!_media || !_media->applyGroup(group->others)) { + resetGroupMedia(group->others); + } + + Ensures(!isHiddenByGroup()); +} + +void HistoryItem::resetGroupMedia( + const std::vector> &others) { + if (!others.empty()) { + _media = std::make_unique(this, others); + } else if (_media) { + _media = _media->takeLastFromGroup(); + } + setPendingInitDimensions(); +} + +int HistoryItem::marginTop() const { + auto result = 0; + if (!isHiddenByGroup()) { + if (isAttachedToPrevious()) { + result += st::msgMarginTopAttached; + } else { + result += st::msgMargin.top(); + } + } + result += displayedDateHeight(); + if (const auto unreadbar = Get()) { + result += unreadbar->height(); + } + return result; +} + +int HistoryItem::marginBottom() const { + return isHiddenByGroup() ? 0 : st::msgMargin.bottom(); +} + void HistoryItem::clipCallback(Media::Clip::Notification notification) { using namespace Media::Clip; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 712428d51..5cbb5c67c 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -22,6 +22,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "base/runtime_composer.h" #include "base/flags.h" +#include "base/value_ordering.h" namespace base { template @@ -435,6 +436,34 @@ struct HistoryMessageUnreadBar : public RuntimeComponent(value); + } + + explicit operator bool() const { + return value != None; + } + + friend inline Type value_ordering_helper(MessageGroupId value) { + return value.value; + } + +}; +struct HistoryMessageGroup : public RuntimeComponent { + MessageGroupId groupId = MessageGroupId::None; + HistoryItem *leader = nullptr; + std::vector> others; +}; + class HistoryWebPage; // Special type of Component for the channel actions log. @@ -899,22 +928,8 @@ public: } return 0; } - int marginTop() const { - int result = 0; - if (isAttachedToPrevious()) { - result += st::msgMarginTopAttached; - } else { - result += st::msgMargin.top(); - } - result += displayedDateHeight(); - if (auto unreadbar = Get()) { - result += unreadbar->height(); - } - return result; - } - int marginBottom() const { - return st::msgMargin.bottom(); - } + int marginTop() const; + int marginBottom() const; bool isAttachedToPrevious() const { return _flags & MTPDmessage_ClientFlag::f_attach_to_previous; } @@ -932,6 +947,23 @@ public: bool isEmpty() const { return _text.isEmpty() && !_media && !Has(); } + bool isHiddenByGroup() const { + return _flags & MTPDmessage_ClientFlag::f_hidden_by_group; + } + + MessageGroupId groupId() const { + if (const auto group = Get()) { + return group->groupId; + } + return MessageGroupId::None; + } + bool groupIdValidityChanged(); + void validateGroupId() { + // Just ignore the result. + groupIdValidityChanged(); + } + void makeGroupMember(not_null leader); + void makeGroupLeader(std::vector> &&others); int width() const { return _width; @@ -1070,6 +1102,8 @@ protected: HistoryMediaPtr _media; private: + void resetGroupMedia(const std::vector> &others); + int _y = 0; int _width = 0; diff --git a/Telegram/SourceFiles/history/history_media.h b/Telegram/SourceFiles/history/history_media.h index 16caa592a..a5554992f 100644 --- a/Telegram/SourceFiles/history/history_media.h +++ b/Telegram/SourceFiles/history/history_media.h @@ -61,7 +61,9 @@ public: } virtual bool isDisplayed() const { - return true; + return !_parent->isHiddenByGroup(); + } + virtual void updateNeedBubbleState() { } virtual bool isAboveMessage() const { return false; @@ -132,7 +134,8 @@ public: virtual bool uploading() const { return false; } - virtual std::unique_ptr clone(HistoryItem *newParent) const = 0; + virtual std::unique_ptr clone( + not_null newParent) const = 0; virtual DocumentData *getDocument() { return nullptr; @@ -155,10 +158,40 @@ public: virtual void attachToParent() { } - virtual void detachFromParent() { } + virtual bool canBeGrouped() const { + return false; + } + virtual QSize sizeForGrouping() const { + Unexpected("Grouping method call."); + } + virtual void drawGrouped( + Painter &p, + const QRect &clip, + TextSelection selection, + TimeMs ms, + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const { + Unexpected("Grouping method call."); + } + virtual HistoryTextState getStateGrouped( + const QRect &geometry, + QPoint point, + HistoryStateRequest request) const { + Unexpected("Grouping method call."); + } + virtual std::unique_ptr takeLastFromGroup() { + return nullptr; + } + virtual bool applyGroup( + const std::vector> &others) { + return others.empty(); + } + virtual void updateSentMedia(const MTPMessageMedia &media) { } diff --git a/Telegram/SourceFiles/history/history_media_grouped.cpp b/Telegram/SourceFiles/history/history_media_grouped.cpp new file mode 100644 index 000000000..831ac3fc7 --- /dev/null +++ b/Telegram/SourceFiles/history/history_media_grouped.cpp @@ -0,0 +1,333 @@ +/* +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 "history/history_media_grouped.h" + +#include "history/history_media_types.h" +#include "history/history_message.h" +#include "storage/storage_shared_media.h" +#include "lang/lang_keys.h" +#include "ui/grouped_layout.h" +#include "styles/style_history.h" + +namespace { + +RectParts GetCornersFromSides(RectParts sides) { + const auto convert = [&]( + RectPart side1, + RectPart side2, + RectPart corner) { + return ((sides & side1) && (sides & side2)) + ? corner + : RectPart::None; + }; + return RectPart::None + | convert(RectPart::Top, RectPart::Left, RectPart::TopLeft) + | convert(RectPart::Top, RectPart::Right, RectPart::TopRight) + | convert(RectPart::Bottom, RectPart::Left, RectPart::BottomLeft) + | convert(RectPart::Bottom, RectPart::Right, RectPart::BottomRight); +} + +} // namespace + +HistoryGroupedMedia::Element::Element(not_null item) +: item(item) { +} + +HistoryGroupedMedia::HistoryGroupedMedia( + not_null parent, + const std::vector> &others) +: HistoryMedia(parent) { + const auto result = applyGroup(others); + + Ensures(result); +} + +void HistoryGroupedMedia::initDimensions() { + std::vector sizes; + sizes.reserve(_elements.size()); + for (const auto &element : _elements) { + const auto &media = element.content; + media->initDimensions(); + sizes.push_back(media->sizeForGrouping()); + } + + const auto layout = Data::LayoutMediaGroup( + sizes, + st::historyGroupWidthMax, + st::historyGroupWidthMin, + st::historyGroupSkip); + Assert(layout.size() == _elements.size()); + + _maxw = _minh = 0; + for (auto i = 0, count = int(layout.size()); i != count; ++i) { + const auto &item = layout[i]; + accumulate_max(_maxw, item.geometry.x() + item.geometry.width()); + accumulate_max(_minh, item.geometry.y() + item.geometry.height()); + _elements[i].initialGeometry = item.geometry; + _elements[i].sides = item.sides; + } +} + +int HistoryGroupedMedia::resizeGetHeight(int width) { + _width = width; + _height = 0; + if (_width < st::historyGroupWidthMin) { + return _height; + } + + const auto initialSpacing = st::historyGroupSkip; + const auto factor = width / float64(st::historyGroupWidthMax); + const auto scale = [&](int value) { + return int(std::round(value * factor)); + }; + const auto spacing = scale(initialSpacing); + for (auto &element : _elements) { + const auto sides = element.sides; + const auto initialGeometry = element.initialGeometry; + const auto needRightSkip = !(sides & RectPart::Right); + const auto needBottomSkip = !(sides & RectPart::Bottom); + const auto initialLeft = initialGeometry.x(); + const auto initialTop = initialGeometry.y(); + const auto initialRight = initialLeft + + initialGeometry.width() + + (needRightSkip ? initialSpacing : 0); + const auto initialBottom = initialTop + + initialGeometry.height() + + (needBottomSkip ? initialSpacing : 0); + const auto left = scale(initialLeft); + const auto top = scale(initialTop); + const auto width = scale(initialRight) + - left + - (needRightSkip ? spacing : 0); + const auto height = scale(initialBottom) + - top + - (needBottomSkip ? spacing : 0); + element.geometry = QRect(left, top, width, height); + + accumulate_max(_height, top + height); + } + return _height; +} + +void HistoryGroupedMedia::draw( + Painter &p, + const QRect &clip, + TextSelection selection, + TimeMs ms) const { + for (const auto &element : _elements) { + auto corners = GetCornersFromSides(element.sides); + if (!isBubbleTop()) { + corners &= ~(RectPart::TopLeft | RectPart::TopRight); + } + if (!isBubbleBottom() || !_caption.isEmpty()) { + corners &= ~(RectPart::BottomLeft | RectPart::BottomRight); + } + element.content->drawGrouped( + p, + clip, + selection, + ms, + element.geometry, + corners, + &element.cacheKey, + &element.cache); + } +} + +HistoryTextState HistoryGroupedMedia::getState( + QPoint point, + HistoryStateRequest request) const { + for (const auto &element : _elements) { + if (element.geometry.contains(point)) { + return element.content->getStateGrouped( + element.geometry, + point, + request); + } + } + return HistoryTextState(); +} + +bool HistoryGroupedMedia::toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const { + for (const auto &element : _elements) { + if (element.content->toggleSelectionByHandlerClick(p)) { + return true; + } + } + return false; +} + +bool HistoryGroupedMedia::dragItemByHandler(const ClickHandlerPtr &p) const { + for (const auto &element : _elements) { + if (element.content->dragItemByHandler(p)) { + return true; + } + } + return false; +} + +TextSelection HistoryGroupedMedia::adjustSelection( + TextSelection selection, + TextSelectType type) const { + return _caption.adjustSelection(selection, type); +} + +TextWithEntities HistoryGroupedMedia::selectedText( + TextSelection selection) const { + return WithCaptionSelectedText( + lang(lng_in_dlg_album), + _caption, + selection); +} + +void HistoryGroupedMedia::clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) { + for (const auto &element : _elements) { + element.content->clickHandlerActiveChanged(p, active); + } +} + +void HistoryGroupedMedia::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + for (const auto &element : _elements) { + element.content->clickHandlerPressedChanged(p, pressed); + } +} + +void HistoryGroupedMedia::attachToParent() { + for (const auto &element : _elements) { + element.content->attachToParent(); + } +} + +void HistoryGroupedMedia::detachFromParent() { + for (const auto &element : _elements) { + if (element.content) { + element.content->detachFromParent(); + } + } +} + +std::unique_ptr HistoryGroupedMedia::takeLastFromGroup() { + return std::move(_elements.back().content); +} + +bool HistoryGroupedMedia::applyGroup( + const std::vector> &others) { + if (others.empty()) { + return false; + } + const auto pushElement = [&](not_null item) { + const auto media = item->getMedia(); + Assert(media != nullptr && media->canBeGrouped()); + + _elements.push_back(Element(item)); + _elements.back().content = item->getMedia()->clone(_parent); + }; + if (_elements.empty()) { + pushElement(_parent); + } else if (validateGroupElements(others)) { + return true; + } + + // We're updating other elements, so we just need to preserve the main. + auto mainElement = std::move(_elements.back()); + _elements.erase(_elements.begin(), _elements.end()); + _elements.reserve(others.size() + 1); + for (const auto item : others) { + pushElement(item); + } + _elements.push_back(std::move(mainElement)); + _parent->setPendingInitDimensions(); + return true; +} + +bool HistoryGroupedMedia::validateGroupElements( + const std::vector> &others) const { + if (_elements.size() != others.size() + 1) { + return false; + } + for (auto i = 0, count = int(others.size()); i != count; ++i) { + if (_elements[i].item != others[i]) { + return false; + } + } + return true; +} + +not_null HistoryGroupedMedia::main() const { + Expects(!_elements.empty()); + + return _elements.back().content.get(); +} + +bool HistoryGroupedMedia::hasReplyPreview() const { + return main()->hasReplyPreview(); +} + +ImagePtr HistoryGroupedMedia::replyPreview() { + return main()->replyPreview(); +} + +Storage::SharedMediaTypesMask HistoryGroupedMedia::sharedMediaTypes() const { + return main()->sharedMediaTypes(); +} + +void HistoryGroupedMedia::updateNeedBubbleState() { + auto captionText = [&] { + for (const auto &element : _elements) { + auto result = element.content->getCaption(); + if (!result.text.isEmpty()) { + return result; + } + } + return TextWithEntities(); + }(); + _caption.setText( + st::messageTextStyle, + captionText.text + _parent->skipBlock(), + itemTextNoMonoOptions(_parent)); + _needBubble = computeNeedBubble(); +} + +bool HistoryGroupedMedia::needsBubble() const { + return _needBubble; +} + +bool HistoryGroupedMedia::computeNeedBubble() const { + if (!_caption.isEmpty()) { + return true; + } + for (const auto &element : _elements) { + if (const auto message = element.item->toHistoryMessage()) { + if (message->viaBot() + || message->Has() + || message->Has() + || message->displayFromName()) { + return true; + } + } + } + return false; +} diff --git a/Telegram/SourceFiles/history/history_media_grouped.h b/Telegram/SourceFiles/history/history_media_grouped.h new file mode 100644 index 000000000..f1025d78f --- /dev/null +++ b/Telegram/SourceFiles/history/history_media_grouped.h @@ -0,0 +1,126 @@ +/* +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 "history/history_media.h" +#include "data/data_document.h" +#include "data/data_photo.h" + +class HistoryGroupedMedia : public HistoryMedia { +public: + HistoryGroupedMedia( + not_null parent, + const std::vector> &others); + + HistoryMediaType type() const override { + return MediaTypeGrouped; + } + std::unique_ptr clone( + not_null newParent) const override { + Unexpected("Clone HistoryGroupedMedia."); + } + + void initDimensions() override; + int resizeGetHeight(int width) override; + + void draw( + Painter &p, + const QRect &clip, + TextSelection selection, + TimeMs ms) const override; + HistoryTextState getState( + QPoint point, + HistoryStateRequest request) const override; + + bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override; + bool dragItemByHandler(const ClickHandlerPtr &p) const override; + + [[nodiscard]] TextSelection adjustSelection( + TextSelection selection, + TextSelectType type) const override; + uint16 fullSelectionLength() const override { + return _caption.length(); + } + bool hasTextForCopy() const override { + return !_caption.isEmpty(); + } + + TextWithEntities selectedText(TextSelection selection) const override; + + void clickHandlerActiveChanged( + const ClickHandlerPtr &p, + bool active) override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) override; + + void attachToParent() override; + void detachFromParent() override; + std::unique_ptr takeLastFromGroup() override; + bool applyGroup( + const std::vector> &others) override; + + bool hasReplyPreview() const override; + ImagePtr replyPreview() override; + + Storage::SharedMediaTypesMask sharedMediaTypes() const override; + bool canBeGrouped() const override { + return true; + } + + bool skipBubbleTail() const override { + return isBubbleBottom() && _caption.isEmpty(); + } + void updateNeedBubbleState() override; + bool needsBubble() const override; + bool customInfoLayout() const override { + return _caption.isEmpty(); + } + bool allowsFastShare() const override { + return true; + } + +private: + struct Element { + Element(not_null item); + + not_null item; + std::unique_ptr content; + + RectParts sides = RectPart::None; + QRect initialGeometry; + QRect geometry; + mutable uint64 cacheKey = 0; + mutable QPixmap cache; + + }; + + bool computeNeedBubble() const; + not_null main() const; + bool validateGroupElements( + const std::vector> &others) const; + + Text _caption; + std::vector _elements; + bool _needBubble = false; + +}; diff --git a/Telegram/SourceFiles/history/history_media_types.cpp b/Telegram/SourceFiles/history/history_media_types.cpp index f7d5ac618..56bfcf2b2 100644 --- a/Telegram/SourceFiles/history/history_media_types.cpp +++ b/Telegram/SourceFiles/history/history_media_types.cpp @@ -95,14 +95,6 @@ bool needReSetInlineResultDocument(const MTPMessageMedia &media, DocumentData *e return true; } -} // namespace - -void HistoryInitMedia() { - initTextOptions(); -} - -namespace { - int32 documentMaxStatusWidth(DocumentData *document) { int32 result = st::normalFont->width(formatDownloadText(document->size, document->size)); if (const auto song = document->song()) { @@ -125,7 +117,43 @@ int32 gifMaxStatusWidth(DocumentData *document) { return result; } -TextWithEntities captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) { +QSize CountPixSizeForSize(QSize original, QSize geometry) { + const auto width = geometry.width(); + const auto height = geometry.height(); + auto tw = original.width(); + auto th = original.height(); + if (tw * height > th * width) { + if (tw * height < 2 * th * width) { + tw = (height * tw) / th; + th = height; + } else if (tw < width) { + th = (width * th) / tw; + tw = width; + } + } else { + if (th * width < 2 * tw * height) { + th = (width * th) / tw; + tw = width; + } else if (tw > 0 && th < height) { + tw = (height * tw) / th; + th = height; + } + } + if (tw < 1) tw = 1; + if (th < 1) th = 1; + return { tw, th }; +} + +} // namespace + +void HistoryInitMedia() { + initTextOptions(); +} + +TextWithEntities WithCaptionSelectedText( + const QString &attachType, + const Text &caption, + TextSelection selection) { if (selection != FullSelection) { return caption.originalTextWithEntities(selection, ExpandLinksAll); } @@ -143,7 +171,9 @@ TextWithEntities captionedSelectedText(const QString &attachType, const Text &ca return result; } -QString captionedNotificationText(const QString &attachType, const Text &caption) { +QString WithCaptionNotificationText( + const QString &attachType, + const Text &caption) { if (caption.isEmpty()) { return attachType; } @@ -153,7 +183,9 @@ QString captionedNotificationText(const QString &attachType, const Text &caption return lng_dialogs_text_media(lt_media_part, attachTypeWrapped, lt_caption, captionText); } -QString captionedInDialogsText(const QString &attachType, const Text &caption) { +QString WithCaptionDialogsText( + const QString &attachType, + const Text &caption) { if (caption.isEmpty()) { return textcmdLink(1, TextUtilities::Clean(attachType)); } @@ -163,8 +195,6 @@ QString captionedInDialogsText(const QString &attachType, const Text &caption) { return lng_dialogs_text_media(lt_media_part, attachTypeWrapped, lt_caption, captionText); } -} // namespace - void HistoryFileMedia::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (p == _savel || p == _cancell) { if (active && !dataLoaded()) { @@ -184,7 +214,10 @@ void HistoryFileMedia::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool Auth().data().requestItemRepaint(_parent); } -void HistoryFileMedia::setLinks(ClickHandlerPtr &&openl, ClickHandlerPtr &&savel, ClickHandlerPtr &&cancell) { +void HistoryFileMedia::setLinks( + ClickHandlerPtr &&openl, + ClickHandlerPtr &&savel, + ClickHandlerPtr &&cancell) { _openl = std::move(openl); _savel = std::move(savel); _cancell = std::move(cancell); @@ -232,33 +265,59 @@ void HistoryFileMedia::checkAnimationFinished() const { HistoryFileMedia::~HistoryFileMedia() = default; -HistoryPhoto::HistoryPhoto(not_null parent, not_null photo, const QString &caption) : HistoryFileMedia(parent) +HistoryPhoto::HistoryPhoto( + not_null parent, + not_null photo, + const QString &caption) +: HistoryFileMedia(parent) , _data(photo) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { - setLinks(MakeShared(_data), MakeShared(_data), MakeShared(_data)); + setLinks( + MakeShared(_data), + MakeShared(_data), + MakeShared(_data)); if (!caption.isEmpty()) { _caption.setText(st::messageTextStyle, caption + _parent->skipBlock(), itemTextNoMonoOptions(_parent)); } init(); } -HistoryPhoto::HistoryPhoto(not_null parent, not_null chat, not_null photo, int32 width) : HistoryFileMedia(parent) +HistoryPhoto::HistoryPhoto( + not_null parent, + not_null chat, + not_null photo, + int width) +: HistoryFileMedia(parent) , _data(photo) { - setLinks(MakeShared(_data, chat), MakeShared(_data, chat), MakeShared(_data, chat)); + setLinks( + MakeShared(_data, chat), + MakeShared(_data, chat), + MakeShared(_data, chat)); _width = width; init(); } -HistoryPhoto::HistoryPhoto(not_null parent, not_null chat, const MTPDphoto &photo, int32 width) : HistoryPhoto(parent, chat, App::feedPhoto(photo), width) { +HistoryPhoto::HistoryPhoto( + not_null parent, + not_null chat, + const MTPDphoto &photo, + int width) +: HistoryPhoto(parent, chat, App::feedPhoto(photo), width) { } -HistoryPhoto::HistoryPhoto(not_null parent, const HistoryPhoto &other) : HistoryFileMedia(parent) +HistoryPhoto::HistoryPhoto( + not_null parent, + const HistoryPhoto &other) +: HistoryFileMedia(parent) , _data(other._data) , _pixw(other._pixw) , _pixh(other._pixh) , _caption(other._caption) { - setLinks(MakeShared(_data), MakeShared(_data), MakeShared(_data)); + setLinks( + MakeShared(_data), + MakeShared(_data), + MakeShared(_data)); init(); } @@ -378,7 +437,6 @@ void HistoryPhoto::draw(Painter &p, const QRect &r, TextSelection selection, Tim bool radial = isRadialAnimation(ms); auto rthumb = rtlrect(skipx, skipy, width, height, _width); - QPixmap pix; if (_parent->toHistoryMessage()) { if (bubble) { skipx = st::mediaPadding.left(); @@ -400,21 +458,17 @@ void HistoryPhoto::draw(Painter &p, const QRect &r, TextSelection selection, Tim auto roundRadius = inWebPage ? ImageRoundRadius::Small : ImageRoundRadius::Large; auto roundCorners = inWebPage ? ImageRoundCorner::All : ((isBubbleTop() ? (ImageRoundCorner::TopLeft | ImageRoundCorner::TopRight) : ImageRoundCorner::None) | ((isBubbleBottom() && _caption.isEmpty()) ? (ImageRoundCorner::BottomLeft | ImageRoundCorner::BottomRight) : ImageRoundCorner::None)); - if (loaded) { - pix = _data->full->pixSingle(_pixw, _pixh, width, height, roundRadius, roundCorners); - } else { - pix = _data->thumb->pixBlurredSingle(_pixw, _pixh, width, height, roundRadius, roundCorners); - } + const auto pix = loaded + ? _data->full->pixSingle(_pixw, _pixh, width, height, roundRadius, roundCorners) + : _data->thumb->pixBlurredSingle(_pixw, _pixh, width, height, roundRadius, roundCorners); p.drawPixmap(rthumb.topLeft(), pix); if (selected) { App::complexOverlayRect(p, rthumb, roundRadius, roundCorners); } } else { - if (loaded) { - pix = _data->full->pixCircled(_pixw, _pixh); - } else { - pix = _data->thumb->pixBlurredCircled(_pixw, _pixh); - } + const auto pix = loaded + ? _data->full->pixCircled(_pixw, _pixh) + : _data->thumb->pixBlurredCircled(_pixw, _pixh); p.drawPixmap(rthumb.topLeft(), pix); } if (radial || (!loaded && !_data->loading())) { @@ -534,6 +588,163 @@ HistoryTextState HistoryPhoto::getState(QPoint point, HistoryStateRequest reques return result; } +QSize HistoryPhoto::sizeForGrouping() const { + const auto width = convertScale(_data->full->width()); + const auto height = convertScale(_data->full->height()); + return { std::max(width, 1), std::max(height, 1) }; +} + +void HistoryPhoto::drawGrouped( + Painter &p, + const QRect &clip, + TextSelection selection, + TimeMs ms, + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const { + _data->automaticLoad(_parent); + + validateGroupedCache(geometry, corners, cacheKey, cache); + + const auto selected = (selection == FullSelection); + const auto loaded = _data->loaded(); + const auto displayLoading = _data->displayLoading(); + const auto bubble = _parent->hasBubble(); + + if (displayLoading) { + ensureAnimation(); + if (!_animation->radial.animating()) { + _animation->radial.start(_data->progress()); + } + } + const auto radial = isRadialAnimation(ms); + + if (!bubble) { +// App::roundShadow(p, 0, 0, width, height, selected ? st::msgInShadowSelected : st::msgInShadow, selected ? InSelectedShadowCorners : InShadowCorners); + } + p.drawPixmap(geometry.topLeft(), *cache); + if (selected) { + const auto roundRadius = ImageRoundRadius::Large; + const auto roundCorners = ImageRoundCorner::None + | ((corners & RectPart::TopLeft) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None) + | ((corners & RectPart::TopRight) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None) + | ((corners & RectPart::BottomLeft) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None) + | ((corners & RectPart::BottomRight) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None); + App::complexOverlayRect(p, geometry, roundRadius, roundCorners); + } + + if (radial || (!loaded && !_data->loading())) { + const auto radialOpacity = (radial && loaded && !_data->uploading()) + ? _animation->radial.opacity() + : 1.; + const auto radialSize = st::historyGroupRadialSize; + const auto inner = QRect( + geometry.x() + (geometry.width() - radialSize) / 2, + geometry.y() + (geometry.height() - radialSize) / 2, + radialSize, + radialSize); + p.setPen(Qt::NoPen); + if (selected) { + p.setBrush(st::msgDateImgBgSelected); + } else if (isThumbAnimation(ms)) { + auto over = _animation->a_thumbOver.current(); + p.setBrush(anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, over)); + } else { + auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); + p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg); + } + + p.setOpacity(radialOpacity * p.opacity()); + + { + PainterHighQualityEnabler hq(p); + p.drawEllipse(inner); + } + + p.setOpacity(radialOpacity); + auto icon = ([radial, this, selected]() -> const style::icon*{ + if (radial || _data->loading()) { + auto delayed = _data->full->toDelayedStorageImage(); + if (!delayed || !delayed->location().isNull()) { + return &(selected ? st::historyFileThumbCancelSelected : st::historyFileThumbCancel); + } + return nullptr; + } + return &(selected ? st::historyFileThumbDownloadSelected : st::historyFileThumbDownload); + })(); + if (icon) { + icon->paintInCenter(p, inner); + } + p.setOpacity(1); + if (radial) { + const auto line = st::historyGroupRadialLine; + const auto rinner = inner.marginsRemoved({ line, line, line, line }); + const auto color = selected + ? st::historyFileThumbRadialFgSelected + : st::historyFileThumbRadialFg; + _animation->radial.draw(p, rinner, line, color); + } + } +} + +HistoryTextState HistoryPhoto::getStateGrouped( + const QRect &geometry, + QPoint point, + HistoryStateRequest request) const { + if (!geometry.contains(point)) { + return {}; + } + const auto delayed = _data->full->toDelayedStorageImage(); + return _data->uploading() + ? _cancell + : _data->loaded() + ? _openl + : _data->loading() + ? ((!delayed || !delayed->location().isNull()) + ? _cancell + : ClickHandlerPtr()) + : _savel; +} + +void HistoryPhoto::validateGroupedCache( + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const { + using Option = Images::Option; + const auto loaded = _data->loaded(); + const auto loadLevel = loaded ? 2 : _data->thumb->loaded() ? 1 : 0; + const auto width = geometry.width(); + const auto height = geometry.height(); + const auto options = Option::Smooth + | Option::RoundedLarge + | (loaded ? Option::None : Option::Blurred) + | ((corners & RectPart::TopLeft) ? Option::RoundedTopLeft : Option::None) + | ((corners & RectPart::TopRight) ? Option::RoundedTopRight : Option::None) + | ((corners & RectPart::BottomLeft) ? Option::RoundedBottomLeft : Option::None) + | ((corners & RectPart::BottomRight) ? Option::RoundedBottomRight : Option::None); + const auto key = (uint64(width) << 48) + | (uint64(height) << 32) + | (uint64(options) << 16) + | (uint64(loadLevel)); + if (*cacheKey == key) { + return; + } + + const auto originalWidth = convertScale(_data->full->width()); + const auto originalHeight = convertScale(_data->full->height()); + const auto pixSize = CountPixSizeForSize( + { originalWidth, originalHeight }, + { width, height }); + const auto pixWidth = pixSize.width(); + const auto pixHeight = pixSize.height(); + const auto &image = loaded ? _data->full : _data->thumb; + + *cacheKey = key; + *cache = image->pixNoCache(pixWidth, pixHeight, options, width, height); +} + void HistoryPhoto::updateSentMedia(const MTPMessageMedia &media) { if (media.type() == mtpc_messageMediaPhoto) { auto &mediaPhoto = media.c_messageMediaPhoto(); @@ -614,15 +825,18 @@ void HistoryPhoto::detachFromParent() { } QString HistoryPhoto::notificationText() const { - return captionedNotificationText(lang(lng_in_dlg_photo), _caption); + return WithCaptionNotificationText(lang(lng_in_dlg_photo), _caption); } QString HistoryPhoto::inDialogsText() const { - return captionedInDialogsText(lang(lng_in_dlg_photo), _caption); + return WithCaptionDialogsText(lang(lng_in_dlg_photo), _caption); } TextWithEntities HistoryPhoto::selectedText(TextSelection selection) const { - return captionedSelectedText(lang(lng_in_dlg_photo), _caption, selection); + return WithCaptionSelectedText( + lang(lng_in_dlg_photo), + _caption, + selection); } bool HistoryPhoto::needsBubble() const { @@ -649,7 +863,11 @@ ImagePtr HistoryPhoto::replyPreview() { return _data->makeReplyPreview(); } -HistoryVideo::HistoryVideo(not_null parent, DocumentData *document, const QString &caption) : HistoryFileMedia(parent) +HistoryVideo::HistoryVideo( + not_null parent, + not_null document, + const QString &caption) +: HistoryFileMedia(parent) , _data(document) , _thumbw(1) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { @@ -664,7 +882,10 @@ HistoryVideo::HistoryVideo(not_null parent, DocumentData *document _data->thumb->load(); } -HistoryVideo::HistoryVideo(not_null parent, const HistoryVideo &other) : HistoryFileMedia(parent) +HistoryVideo::HistoryVideo( + not_null parent, + const HistoryVideo &other) +: HistoryFileMedia(parent) , _data(other._data) , _thumbw(other._thumbw) , _caption(other._caption) { @@ -864,10 +1085,11 @@ void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, Tim } HistoryTextState HistoryVideo::getState(QPoint point, HistoryStateRequest request) const { + if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) { + return {}; + } + HistoryTextState result; - - if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return result; - bool loaded = _data->loaded(); int32 skipx = 0, skipy = 0, width = _width, height = _height; @@ -914,20 +1136,176 @@ HistoryTextState HistoryVideo::getState(QPoint point, HistoryStateRequest reques return result; } +QSize HistoryVideo::sizeForGrouping() const { + const auto width = convertScale(_data->thumb->width()); + const auto height = convertScale(_data->thumb->height()); + return { std::max(width, 1), std::max(height, 1) }; +} + +void HistoryVideo::drawGrouped( + Painter &p, + const QRect &clip, + TextSelection selection, + TimeMs ms, + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const { + _data->automaticLoad(_parent); + + validateGroupedCache(geometry, corners, cacheKey, cache); + + const auto selected = (selection == FullSelection); + const auto loaded = _data->loaded(); + const auto displayLoading = _data->displayLoading(); + const auto bubble = _parent->hasBubble(); + + if (displayLoading) { + ensureAnimation(); + if (!_animation->radial.animating()) { + _animation->radial.start(_data->progress()); + } + } + const auto radial = isRadialAnimation(ms); + + if (!bubble) { +// App::roundShadow(p, 0, 0, width, height, selected ? st::msgInShadowSelected : st::msgInShadow, selected ? InSelectedShadowCorners : InShadowCorners); + } + p.drawPixmap(geometry.topLeft(), *cache); + if (selected) { + const auto roundRadius = ImageRoundRadius::Large; + const auto roundCorners = ImageRoundCorner::None + | ((corners & RectPart::TopLeft) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None) + | ((corners & RectPart::TopRight) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None) + | ((corners & RectPart::BottomLeft) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None) + | ((corners & RectPart::BottomRight) ? ImageRoundCorner::TopLeft : ImageRoundCorner::None); + App::complexOverlayRect(p, geometry, roundRadius, roundCorners); + } + + const auto radialOpacity = (radial && loaded && !_data->uploading()) + ? _animation->radial.opacity() + : 1.; + const auto radialSize = st::historyGroupRadialSize; + const auto inner = QRect( + geometry.x() + (geometry.width() - radialSize) / 2, + geometry.y() + (geometry.height() - radialSize) / 2, + radialSize, + radialSize); + p.setPen(Qt::NoPen); + if (selected) { + p.setBrush(st::msgDateImgBgSelected); + } else if (isThumbAnimation(ms)) { + auto over = _animation->a_thumbOver.current(); + p.setBrush(anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, over)); + } else { + auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); + p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg); + } + + p.setOpacity(radialOpacity * p.opacity()); + + { + PainterHighQualityEnabler hq(p); + p.drawEllipse(inner); + } + + p.setOpacity(radialOpacity); + auto icon = ([this, radial, selected, loaded]() -> const style::icon * { + if (loaded && !radial) { + return &(selected ? st::historyFileThumbPlaySelected : st::historyFileThumbPlay); + } else if (radial || _data->loading()) { + if (_parent->id > 0 || _data->uploading()) { + return &(selected ? st::historyFileThumbCancelSelected : st::historyFileThumbCancel); + } + return nullptr; + } + return &(selected ? st::historyFileThumbDownloadSelected : st::historyFileThumbDownload); + })(); + if (icon) { + icon->paintInCenter(p, inner); + } + p.setOpacity(1); + if (radial) { + const auto line = st::historyGroupRadialLine; + const auto rinner = inner.marginsRemoved({ line, line, line, line }); + const auto color = selected + ? st::historyFileThumbRadialFgSelected + : st::historyFileThumbRadialFg; + _animation->radial.draw(p, rinner, line, color); + } +} + +HistoryTextState HistoryVideo::getStateGrouped( + const QRect &geometry, + QPoint point, + HistoryStateRequest request) const { + if (!geometry.contains(point)) { + return {}; + } + return _data->uploading() + ? _cancell + : _data->loaded() + ? _openl + : _data->loading() + ? _cancell + : _savel; +} + +void HistoryVideo::validateGroupedCache( + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const { + using Option = Images::Option; + const auto loaded = _data->thumb->loaded(); + const auto loadLevel = loaded ? 1 : 0; + const auto width = geometry.width(); + const auto height = geometry.height(); + const auto options = Option::Smooth + | Option::RoundedLarge + | Option::Blurred + | ((corners & RectPart::TopLeft) ? Option::RoundedTopLeft : Option::None) + | ((corners & RectPart::TopRight) ? Option::RoundedTopRight : Option::None) + | ((corners & RectPart::BottomLeft) ? Option::RoundedBottomLeft : Option::None) + | ((corners & RectPart::BottomRight) ? Option::RoundedBottomRight : Option::None); + const auto key = (uint64(width) << 48) + | (uint64(height) << 32) + | (uint64(options) << 16) + | (uint64(loadLevel)); + if (*cacheKey == key) { + return; + } + + const auto originalWidth = convertScale(_data->thumb->width()); + const auto originalHeight = convertScale(_data->thumb->height()); + const auto pixSize = CountPixSizeForSize( + { originalWidth, originalHeight }, + { width, height }); + const auto pixWidth = pixSize.width(); + const auto pixHeight = pixSize.height(); + const auto &image = _data->thumb; + + *cacheKey = key; + *cache = image->pixNoCache(pixWidth, pixHeight, options, width, height); +} + void HistoryVideo::setStatusSize(int32 newSize) const { HistoryFileMedia::setStatusSize(newSize, _data->size, _data->duration(), 0); } QString HistoryVideo::notificationText() const { - return captionedNotificationText(lang(lng_in_dlg_video), _caption); + return WithCaptionNotificationText(lang(lng_in_dlg_video), _caption); } QString HistoryVideo::inDialogsText() const { - return captionedInDialogsText(lang(lng_in_dlg_video), _caption); + return WithCaptionDialogsText(lang(lng_in_dlg_video), _caption); } TextWithEntities HistoryVideo::selectedText(TextSelection selection) const { - return captionedSelectedText(lang(lng_in_dlg_video), _caption, selection); + return WithCaptionSelectedText( + lang(lng_in_dlg_video), + _caption, + selection); } bool HistoryVideo::needsBubble() const { @@ -1036,7 +1414,11 @@ void HistoryDocumentVoice::stopSeeking() { Media::Player::instance()->stopSeeking(AudioMsgId::Type::Voice); } -HistoryDocument::HistoryDocument(not_null parent, DocumentData *document, const QString &caption) : HistoryFileMedia(parent) +HistoryDocument::HistoryDocument( + not_null parent, + not_null document, + const QString &caption) +: HistoryFileMedia(parent) , _data(document) { createComponents(!caption.isEmpty()); if (auto named = Get()) { @@ -1056,7 +1438,6 @@ HistoryDocument::HistoryDocument( not_null parent, const HistoryDocument &other) : HistoryFileMedia(parent) -, RuntimeComposer() , _data(other._data) { auto captioned = other.Get(); createComponents(captioned != 0); @@ -1537,7 +1918,9 @@ void HistoryDocument::updatePressed(QPoint point) { QString HistoryDocument::notificationText() const { QString result; buildStringRepresentation([&result](const QString &type, const QString &fileName, const Text &caption) { - result = captionedNotificationText(fileName.isEmpty() ? type : fileName, caption); + result = WithCaptionNotificationText( + fileName.isEmpty() ? type : fileName, + caption); }); return result; } @@ -1545,7 +1928,9 @@ QString HistoryDocument::notificationText() const { QString HistoryDocument::inDialogsText() const { QString result; buildStringRepresentation([&result](const QString &type, const QString &fileName, const Text &caption) { - result = captionedInDialogsText(fileName.isEmpty() ? type : fileName, caption); + result = WithCaptionDialogsText( + fileName.isEmpty() ? type : fileName, + caption); }); return result; } @@ -1557,7 +1942,7 @@ TextWithEntities HistoryDocument::selectedText(TextSelection selection) const { if (!fileName.isEmpty()) { fullType.append(qstr(" : ")).append(fileName); } - result = captionedSelectedText(fullType, caption, selection); + result = WithCaptionSelectedText(fullType, caption, selection); }); return result; } @@ -1771,7 +2156,11 @@ ImagePtr HistoryDocument::replyPreview() { return _data->makeReplyPreview(); } -HistoryGif::HistoryGif(not_null parent, DocumentData *document, const QString &caption) : HistoryFileMedia(parent) +HistoryGif::HistoryGif( + not_null parent, + not_null document, + const QString &caption) +: HistoryFileMedia(parent) , _data(document) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { setDocumentLinks(_data, true); @@ -1785,7 +2174,10 @@ HistoryGif::HistoryGif(not_null parent, DocumentData *document, co _data->thumb->load(); } -HistoryGif::HistoryGif(not_null parent, const HistoryGif &other) : HistoryFileMedia(parent) +HistoryGif::HistoryGif( + not_null parent, + const HistoryGif &other) +: HistoryFileMedia(parent) , _data(other._data) , _thumbw(other._thumbw) , _thumbh(other._thumbh) @@ -2365,15 +2757,15 @@ HistoryTextState HistoryGif::getState(QPoint point, HistoryStateRequest request) } QString HistoryGif::notificationText() const { - return captionedNotificationText(mediaTypeString(), _caption); + return WithCaptionNotificationText(mediaTypeString(), _caption); } QString HistoryGif::inDialogsText() const { - return captionedInDialogsText(mediaTypeString(), _caption); + return WithCaptionDialogsText(mediaTypeString(), _caption); } TextWithEntities HistoryGif::selectedText(TextSelection selection) const { - return captionedSelectedText(mediaTypeString(), _caption, selection); + return WithCaptionSelectedText(mediaTypeString(), _caption, selection); } bool HistoryGif::needsBubble() const { @@ -2600,7 +2992,10 @@ bool HistoryGif::dataLoaded() const { return (!_parent || _parent->id > 0) ? _data->loaded() : false; } -HistorySticker::HistorySticker(not_null parent, DocumentData *document) : HistoryMedia(parent) +HistorySticker::HistorySticker( + not_null parent, + not_null document) +: HistoryMedia(parent) , _data(document) , _emoji(_data->sticker()->alt) { _data->thumb->load(); @@ -3769,13 +4164,19 @@ int HistoryWebPage::bottomInfoPadding() const { return result; } -HistoryGame::HistoryGame(not_null parent, GameData *data) : HistoryMedia(parent) +HistoryGame::HistoryGame( + not_null parent, + not_null data) +: HistoryMedia(parent) , _data(data) , _title(st::msgMinWidth - st::webPageLeft) , _description(st::msgMinWidth - st::webPageLeft) { } -HistoryGame::HistoryGame(not_null parent, const HistoryGame &other) : HistoryMedia(parent) +HistoryGame::HistoryGame( + not_null parent, + const HistoryGame &other) +: HistoryMedia(parent) , _data(other._data) , _attach(other._attach ? other._attach->clone(parent) : nullptr) , _title(other._title) @@ -4799,11 +5200,11 @@ TextSelection HistoryLocation::adjustSelection(TextSelection selection, TextSele } QString HistoryLocation::notificationText() const { - return captionedNotificationText(lang(lng_maps_point), _title); + return WithCaptionNotificationText(lang(lng_maps_point), _title); } QString HistoryLocation::inDialogsText() const { - return captionedInDialogsText(lang(lng_maps_point), _title); + return WithCaptionDialogsText(lang(lng_maps_point), _title); } TextWithEntities HistoryLocation::selectedText(TextSelection selection) const { diff --git a/Telegram/SourceFiles/history/history_media_types.h b/Telegram/SourceFiles/history/history_media_types.h index 108029dc7..e6f078573 100644 --- a/Telegram/SourceFiles/history/history_media_types.h +++ b/Telegram/SourceFiles/history/history_media_types.h @@ -38,6 +38,16 @@ class EmptyUserpic; } // namespace Ui void HistoryInitMedia(); +TextWithEntities WithCaptionSelectedText( + const QString &attachType, + const Text &caption, + TextSelection selection); +QString WithCaptionNotificationText( + const QString &attachType, + const Text &caption); +QString WithCaptionDialogsText( + const QString &attachType, + const Text &caption); class HistoryFileMedia : public HistoryMedia { public: @@ -129,23 +139,35 @@ protected: class HistoryPhoto : public HistoryFileMedia { public: - HistoryPhoto(not_null parent, not_null photo, const QString &caption); - HistoryPhoto(not_null parent, not_null chat, not_null photo, int width); - HistoryPhoto(not_null parent, not_null chat, const MTPDphoto &photo, int width); + HistoryPhoto( + not_null parent, + not_null photo, + const QString &caption); + HistoryPhoto( + not_null parent, + not_null chat, + not_null photo, + int width); + HistoryPhoto( + not_null parent, + not_null chat, + const MTPDphoto &photo, + int width); HistoryPhoto(not_null parent, const HistoryPhoto &other); void init(); HistoryMediaType type() const override { return MediaTypePhoto; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } void initDimensions() override; int resizeGetHeight(int width) override; - void draw(Painter &p, const QRect &r, TextSelection selection, TimeMs ms) const override; + void draw(Painter &p, const QRect &clip, TextSelection selection, TimeMs ms) const override; HistoryTextState getState(QPoint point, HistoryStateRequest request) const override; [[nodiscard]] TextSelection adjustSelection( @@ -166,10 +188,28 @@ public: Storage::SharedMediaTypesMask sharedMediaTypes() const override; - PhotoData *photo() const { + not_null photo() const { return _data; } + bool canBeGrouped() const override { + return true; + } + QSize sizeForGrouping() const override; + void drawGrouped( + Painter &p, + const QRect &clip, + TextSelection selection, + TimeMs ms, + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const override; + HistoryTextState getStateGrouped( + const QRect &geometry, + QPoint point, + HistoryStateRequest request) const override; + void updateSentMedia(const MTPMessageMedia &media) override; bool needReSetInlineResultMedia(const MTPMessageMedia &media) override; @@ -210,6 +250,12 @@ protected: } private: + void validateGroupedCache( + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const; + not_null _data; int16 _pixw = 1; int16 _pixh = 1; @@ -219,12 +265,17 @@ private: class HistoryVideo : public HistoryFileMedia { public: - HistoryVideo(not_null parent, DocumentData *document, const QString &caption); + HistoryVideo( + not_null parent, + not_null document, + const QString &caption); HistoryVideo(not_null parent, const HistoryVideo &other); + HistoryMediaType type() const override { return MediaTypeVideo; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } @@ -256,6 +307,24 @@ public: return _data; } + bool canBeGrouped() const override { + return true; + } + QSize sizeForGrouping() const override; + void drawGrouped( + Painter &p, + const QRect &clip, + TextSelection selection, + TimeMs ms, + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const override; + HistoryTextState getStateGrouped( + const QRect &geometry, + QPoint point, + HistoryStateRequest request) const override; + bool uploading() const override { return _data->uploading(); } @@ -297,13 +366,18 @@ protected: } private: + void validateGroupedCache( + const QRect &geometry, + RectParts corners, + not_null cacheKey, + not_null cache) const; + void setStatusSize(int32 newSize) const; + void updateStatusText() const; + not_null _data; int32 _thumbw; Text _caption; - void setStatusSize(int32 newSize) const; - void updateStatusText() const; - }; struct HistoryDocumentThumbed : public RuntimeComponent { @@ -370,8 +444,14 @@ private: class HistoryDocument : public HistoryFileMedia, public RuntimeComposer { public: - HistoryDocument(not_null parent, DocumentData *document, const QString &caption); - HistoryDocument(not_null parent, const HistoryDocument &other); + HistoryDocument( + not_null parent, + not_null document, + const QString &caption); + HistoryDocument( + not_null parent, + const HistoryDocument &other); + HistoryMediaType type() const override { return _data->isVoiceMessage() ? MediaTypeVoiceFile @@ -379,7 +459,8 @@ public: ? MediaTypeMusicFile : MediaTypeFile); } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } @@ -488,12 +569,17 @@ private: class HistoryGif : public HistoryFileMedia { public: - HistoryGif(not_null parent, DocumentData *document, const QString &caption); + HistoryGif( + not_null parent, + not_null document, + const QString &caption); HistoryGif(not_null parent, const HistoryGif &other); + HistoryMediaType type() const override { return MediaTypeGif; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } @@ -602,11 +688,15 @@ private: class HistorySticker : public HistoryMedia { public: - HistorySticker(not_null parent, DocumentData *document); + HistorySticker( + not_null parent, + not_null document); + HistoryMediaType type() const override { return MediaTypeSticker; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, _data); } @@ -671,11 +761,18 @@ private: class HistoryContact : public HistoryMedia { public: - HistoryContact(not_null parent, int32 userId, const QString &first, const QString &last, const QString &phone); + HistoryContact( + not_null parent, + int32 userId, + const QString &first, + const QString &last, + const QString &phone); + HistoryMediaType type() const override { return MediaTypeContact; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, _userId, _fname, _lname, _phone); } @@ -735,11 +832,15 @@ private: class HistoryCall : public HistoryMedia { public: - HistoryCall(not_null parent, const MTPDmessageActionPhoneCall &call); + HistoryCall( + not_null parent, + const MTPDmessageActionPhoneCall &call); + HistoryMediaType type() const override { return MediaTypeCall; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { Unexpected("Clone HistoryCall."); } @@ -790,12 +891,18 @@ private: class HistoryWebPage : public HistoryMedia { public: - HistoryWebPage(not_null parent, not_null data); - HistoryWebPage(not_null parent, const HistoryWebPage &other); + HistoryWebPage( + not_null parent, + not_null data); + HistoryWebPage( + not_null parent, + const HistoryWebPage &other); + HistoryMediaType type() const override { return MediaTypeWebPage; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } @@ -898,12 +1005,14 @@ private: class HistoryGame : public HistoryMedia { public: - HistoryGame(not_null parent, GameData *data); + HistoryGame(not_null parent, not_null data); HistoryGame(not_null parent, const HistoryGame &other); + HistoryMediaType type() const override { return MediaTypeGame; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } @@ -962,7 +1071,7 @@ public: } ImagePtr replyPreview() override; - GameData *game() { + not_null game() { return _data; } @@ -993,7 +1102,7 @@ private: QMargins inBubblePadding() const; int bottomInfoPadding() const; - GameData *_data; + not_null _data; ClickHandlerPtr _openl; std::unique_ptr _attach; @@ -1007,12 +1116,18 @@ private: class HistoryInvoice : public HistoryMedia { public: - HistoryInvoice(not_null parent, const MTPDmessageMediaInvoice &data); - HistoryInvoice(not_null parent, const HistoryInvoice &other); + HistoryInvoice( + not_null parent, + const MTPDmessageMediaInvoice &data); + HistoryInvoice( + not_null parent, + const HistoryInvoice &other); + HistoryMediaType type() const override { return MediaTypeInvoice; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } @@ -1103,12 +1218,20 @@ struct LocationData; class HistoryLocation : public HistoryMedia { public: - HistoryLocation(not_null parent, const LocationCoords &coords, const QString &title = QString(), const QString &description = QString()); - HistoryLocation(not_null parent, const HistoryLocation &other); + HistoryLocation( + not_null parent, + const LocationCoords &coords, + const QString &title = QString(), + const QString &description = QString()); + HistoryLocation( + not_null parent, + const HistoryLocation &other); + HistoryMediaType type() const override { return MediaTypeLocation; } - std::unique_ptr clone(HistoryItem *newParent) const override { + std::unique_ptr clone( + not_null newParent) const override { return std::make_unique(newParent, *this); } diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index 7bb74d86c..3a8c61a1c 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -630,7 +630,9 @@ int HistoryMessage::KeyboardStyle::minButtonWidth(HistoryMessageReplyMarkup::But return result; } -HistoryMessage::HistoryMessage(not_null history, const MTPDmessage &msg) +HistoryMessage::HistoryMessage( + not_null history, + const MTPDmessage &msg) : HistoryItem(history, msg.vid.v, msg.vflags.v, ::date(msg.vdate), msg.has_from_id() ? msg.vfrom_id.v : 0) { CreateConfig config; @@ -655,6 +657,9 @@ HistoryMessage::HistoryMessage(not_null history, const MTPDmessage &ms if (msg.has_reply_markup()) config.mtpMarkup = &msg.vreply_markup; if (msg.has_edit_date()) config.editDate = ::date(msg.vedit_date); if (msg.has_post_author()) config.author = qs(msg.vpost_author); + if (msg.has_grouped_id()) { + config.groupId = MessageGroupId::FromRaw(msg.vgrouped_id.v); + } createComponents(config); @@ -665,7 +670,9 @@ HistoryMessage::HistoryMessage(not_null history, const MTPDmessage &ms setText({ text, entities }); } -HistoryMessage::HistoryMessage(not_null history, const MTPDmessageService &msg) +HistoryMessage::HistoryMessage( + not_null history, + const MTPDmessageService &msg) : HistoryItem(history, msg.vid.v, mtpCastFlags(msg.vflags.v), ::date(msg.vdate), msg.has_from_id() ? msg.vfrom_id.v : 0) { CreateConfig config; @@ -755,22 +762,53 @@ HistoryMessage::HistoryMessage( setText(fwd->originalText()); } -HistoryMessage::HistoryMessage(not_null history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, const TextWithEntities &textWithEntities) +HistoryMessage::HistoryMessage( + not_null history, + MsgId id, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + const TextWithEntities &textWithEntities) : HistoryItem(history, id, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, postAuthor, MTPnullMarkup); setText(textWithEntities); } -HistoryMessage::HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) +HistoryMessage::HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null document, + const QString &caption, + const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, postAuthor, markup); - initMediaFromDocument(doc, caption); + initMediaFromDocument(document, caption); setText(TextWithEntities()); } -HistoryMessage::HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) +HistoryMessage::HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null photo, + const QString &caption, + const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, postAuthor, markup); @@ -778,7 +816,17 @@ HistoryMessage::HistoryMessage(not_null history, MsgId msgId, MTPDmess setText(TextWithEntities()); } -HistoryMessage::HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup) +HistoryMessage::HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null game, + const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, postAuthor, markup); @@ -786,7 +834,12 @@ HistoryMessage::HistoryMessage(not_null history, MsgId msgId, MTPDmess setText(TextWithEntities()); } -void HistoryMessage::createComponentsHelper(MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, const QString &postAuthor, const MTPReplyMarkup &markup) { +void HistoryMessage::createComponentsHelper( + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + const QString &postAuthor, + const MTPReplyMarkup &markup) { CreateConfig config; if (flags & MTPDmessage::Flag::f_via_bot_id) config.viaBotId = viaBotId; @@ -818,6 +871,7 @@ void HistoryMessage::updateMediaInBubbleState() { return; } + _media->updateNeedBubbleState(); if (!drawBubble()) { _media->setInBubbleState(MediaInBubbleState::None); return; @@ -960,10 +1014,13 @@ void HistoryMessage::createComponents(const CreateConfig &config) { } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } + if (config.groupId) { + mask |= HistoryMessageGroup::Bit(); + } UpdateComponents(mask); - if (auto reply = Get()) { + if (const auto reply = Get()) { reply->replyToMsgId = config.replyTo; if (!reply->updateData(this)) { Auth().api().requestMessageData( @@ -972,21 +1029,21 @@ void HistoryMessage::createComponents(const CreateConfig &config) { HistoryDependentItemCallback(fullId())); } } - if (auto via = Get()) { + if (const auto via = Get()) { via->create(config.viaBotId); } - if (auto views = Get()) { + if (const auto views = Get()) { views->_views = config.viewsCount; } - if (auto edited = Get()) { + if (const auto edited = Get()) { edited->create(config.editDate, date.toString(cTimeFormat())); - if (auto msgsigned = Get()) { + if (const auto msgsigned = Get()) { msgsigned->create(config.author, edited->_edited.originalText()); } - } else if (auto msgsigned = Get()) { + } else if (const auto msgsigned = Get()) { msgsigned->create(config.author, date.toString(cTimeFormat())); } - if (auto forwarded = Get()) { + if (const auto forwarded = Get()) { forwarded->_originalDate = config.originalDate; forwarded->_originalSender = App::peer(config.senderOriginal); forwarded->_originalId = config.originalId; @@ -994,7 +1051,7 @@ void HistoryMessage::createComponents(const CreateConfig &config) { forwarded->_savedFromPeer = App::peerLoaded(config.savedFromPeer); forwarded->_savedFromMsgId = config.savedFromMsgId; } - if (auto markup = Get()) { + if (const auto markup = Get()) { if (config.mtpMarkup) { markup->create(*config.mtpMarkup); } else if (config.inlineMarkup) { @@ -1004,6 +1061,10 @@ void HistoryMessage::createComponents(const CreateConfig &config) { _flags |= MTPDmessage_ClientFlag::f_has_switch_inline_button; } } + if (const auto group = Get()) { + group->groupId = config.groupId; + group->leader = this; + } initTime(); _fromNameVersion = displayFrom()->nameVersion; } @@ -1240,14 +1301,16 @@ void HistoryMessage::initDimensions() { } else if (_media) { _media->initDimensions(); _maxw = _media->maxWidth(); - _minh = _media->minHeight(); + _minh = _media->isDisplayed() ? _media->minHeight() : 0; } else { _maxw = st::msgMinWidth; _minh = 0; } - if (auto markup = inlineReplyMarkup()) { + if (const auto markup = inlineReplyMarkup()) { if (!markup->inlineKeyboard) { - markup->inlineKeyboard = std::make_unique(this, std::make_unique(st::msgBotKbButton)); + markup->inlineKeyboard = std::make_unique( + this, + std::make_unique(st::msgBotKbButton)); } // if we have a text bubble we can resize it to fit the keyboard @@ -1259,7 +1322,9 @@ void HistoryMessage::initDimensions() { } bool HistoryMessage::drawBubble() const { - if (Has()) { + if (isHiddenByGroup()) { + return false; + } else if (Has()) { return true; } return _media ? (!emptyText() || _media->needsBubble()) : !isEmpty(); @@ -1397,7 +1462,9 @@ bool HistoryMessage::displayForwardedFrom() const { void HistoryMessage::updateMedia(const MTPMessageMedia *media) { auto setMediaAllowed = [](HistoryMediaType type) { - return (type == MediaTypeWebPage || type == MediaTypeGame || type == MediaTypeLocation); + return (type == MediaTypeWebPage) + || (type == MediaTypeGame) + || (type == MediaTypeLocation); }; if (_flags & MTPDmessage_ClientFlag::f_from_inline_bot) { bool needReSet = true; @@ -1804,7 +1871,9 @@ void HistoryMessage::draw(Painter &p, QRect clip, TextSelection selection, TimeM auto entry = Get(); auto mediaDisplayed = _media && _media->isDisplayed(); - auto skipTail = isAttachedToNext() || (_media && _media->skipBubbleTail()) || (keyboard != nullptr); + auto skipTail = isAttachedToNext() + || (_media && _media->skipBubbleTail()) + || (keyboard != nullptr); auto displayTail = skipTail ? RectPart::None : (outbg && !Adaptive::ChatWide()) ? RectPart::Right : RectPart::Left; HistoryLayout::paintBubble(p, g, width(), selected, outbg, displayTail); @@ -1872,7 +1941,7 @@ void HistoryMessage::draw(Painter &p, QRect clip, TextSelection selection, TimeM const auto fastShareTop = g.top() + g.height() - fastShareSkip - st::historyFastShareSize; drawRightAction(p, fastShareLeft, fastShareTop, width()); } - } else if (_media) { + } else if (_media && _media->isDisplayed()) { p.translate(g.topLeft()); _media->draw(p, clip.translated(-g.topLeft()), skipTextSelection(selection), ms); p.translate(-g.topLeft()); @@ -1880,7 +1949,7 @@ void HistoryMessage::draw(Painter &p, QRect clip, TextSelection selection, TimeM p.restoreTextPalette(); - auto reply = Get(); + const auto reply = Get(); if (reply && reply->isNameUpdated()) { const_cast(this)->setPendingInitDimensions(); } @@ -2006,16 +2075,16 @@ void HistoryMessage::dependencyItemRemoved(HistoryItem *dependency) { } int HistoryMessage::resizeContentGetHeight() { - int result = performResizeGetHeight(); + const auto result = performResizeGetHeight(); - auto keyboard = inlineReplyKeyboard(); - if (auto markup = Get()) { - int oldTop = markup->oldTop; + const auto keyboard = inlineReplyKeyboard(); + if (const auto markup = Get()) { + const auto oldTop = markup->oldTop; if (oldTop >= 0) { markup->oldTop = -1; if (keyboard) { - int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); - int keyboardTop = _height - h + st::msgBotKbButton.margin - marginBottom(); + const auto height = st::msgBotKbButton.margin + keyboard->naturalHeight(); + const auto keyboardTop = _height - height + st::msgBotKbButton.margin - marginBottom(); if (keyboardTop != oldTop) { Notify::inlineKeyboardMoved(this, oldTop, keyboardTop); } @@ -2027,7 +2096,9 @@ int HistoryMessage::resizeContentGetHeight() { } int HistoryMessage::performResizeGetHeight() { - if (width() < st::msgMinWidth) return _height; + if (width() < st::msgMinWidth) { + return _height; + } auto contentWidth = width() - (st::msgMargin.left() + st::msgMargin.right()); if (history()->peer->isSelf() && !hasOutLayout()) { @@ -2111,14 +2182,14 @@ int HistoryMessage::performResizeGetHeight() { reply->resize(countGeometry().width() - st::msgPadding.left() - st::msgPadding.right()); _height += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); } - } else if (_media) { + } else if (_media && _media->isDisplayed()) { _height = _media->resizeGetHeight(contentWidth); } else { _height = 0; } - if (auto keyboard = inlineReplyKeyboard()) { - auto g = countGeometry(); - auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); + if (const auto keyboard = inlineReplyKeyboard()) { + const auto g = countGeometry(); + const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); _height += keyboardHeight; keyboard->resize(g.width(), keyboardHeight - st::msgBotKbButton.margin); } @@ -2128,7 +2199,7 @@ int HistoryMessage::performResizeGetHeight() { } bool HistoryMessage::hasPoint(QPoint point) const { - auto g = countGeometry(); + const auto g = countGeometry(); if (g.width() < 1) { return false; } @@ -2248,7 +2319,7 @@ HistoryTextState HistoryMessage::getState(QPoint point, HistoryStateRequest requ result.link = rightActionLink(); } } - } else if (_media) { + } else if (_media && _media->isDisplayed()) { result = _media->getState(point - g.topLeft(), request); result.symbol += _text.length(); } diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index f64671bd8..8b6ae7e90 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -30,26 +30,119 @@ void FastShareMessage(not_null item); class HistoryMessage : public HistoryItem, private HistoryItemInstantiated { public: - static not_null create(not_null history, const MTPDmessage &msg) { + static not_null create( + not_null history, + const MTPDmessage &msg) { return _create(history, msg); } - static not_null create(not_null history, const MTPDmessageService &msg) { + static not_null create( + not_null history, + const MTPDmessageService &msg) { return _create(history, msg); } - static not_null create(not_null history, MsgId msgId, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, not_null fwd) { + static not_null create( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null fwd) { return _create(history, msgId, flags, date, from, postAuthor, fwd); } - static not_null create(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, const TextWithEntities &textWithEntities) { - return _create(history, msgId, flags, replyTo, viaBotId, date, from, postAuthor, textWithEntities); + static not_null create( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + const TextWithEntities &textWithEntities) { + return _create( + history, + msgId, + flags, + replyTo, + viaBotId, + date, + from, + postAuthor, + textWithEntities); } - static not_null create(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { - return _create(history, msgId, flags, replyTo, viaBotId, date, from, postAuthor, doc, caption, markup); + static not_null create( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null document, + const QString &caption, + const MTPReplyMarkup &markup) { + return _create( + history, + msgId, + flags, + replyTo, + viaBotId, + date, + from, + postAuthor, + document, + caption, + markup); } - static not_null create(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) { - return _create(history, msgId, flags, replyTo, viaBotId, date, from, postAuthor, photo, caption, markup); + static not_null create( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null photo, + const QString &caption, + const MTPReplyMarkup &markup) { + return _create( + history, + msgId, + flags, + replyTo, + viaBotId, + date, + from, + postAuthor, + photo, + caption, + markup); } - static not_null create(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup) { - return _create(history, msgId, flags, replyTo, viaBotId, date, from, postAuthor, game, markup); + static not_null create( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null game, + const MTPReplyMarkup &markup) { + return _create( + history, + msgId, + flags, + replyTo, + viaBotId, + date, + from, + postAuthor, + game, + markup); } void initTime(); @@ -156,13 +249,65 @@ public: ~HistoryMessage(); private: - HistoryMessage(not_null history, const MTPDmessage &msg); - HistoryMessage(not_null history, const MTPDmessageService &msg); - HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, not_null fwd); // local forwarded - HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, const TextWithEntities &textWithEntities); // local message - HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); // local document - HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); // local photo - HistoryMessage(not_null history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, UserId viaBotId, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup); // local game + HistoryMessage( + not_null history, + const MTPDmessage &msg); + HistoryMessage( + not_null history, + const MTPDmessageService &msg); + HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null fwd); // local forwarded + HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + const TextWithEntities &textWithEntities); // local message + HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null document, + const QString &caption, + const MTPReplyMarkup &markup); // local document + HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null photo, + const QString &caption, + const MTPReplyMarkup &markup); // local photo + HistoryMessage( + not_null history, + MsgId msgId, + MTPDmessage::Flags flags, + MsgId replyTo, + UserId viaBotId, + QDateTime date, + UserId from, + const QString &postAuthor, + not_null game, + const MTPReplyMarkup &markup); // local game friend class HistoryItemInstantiated; void setEmptyText(); @@ -214,6 +359,7 @@ private: QString authorOriginal; QDateTime originalDate; QDateTime editDate; + MessageGroupId groupId = MessageGroupId::None; // For messages created from MTP structs. const MTPReplyMarkup *mtpMarkup = nullptr; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 3715cac84..6401fcc0a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4521,7 +4521,9 @@ void HistoryWidget::onThumbDocumentUploaded( void HistoryWidget::onPhotoProgress(const FullMsgId &newId) { if (const auto item = App::histItemById(newId)) { - const auto photo = (item->getMedia() && item->getMedia()->type() == MediaTypePhoto) ? static_cast(item->getMedia())->photo() : nullptr; + const auto photo = (item->getMedia() && item->getMedia()->type() == MediaTypePhoto) + ? static_cast(item->getMedia())->photo().get() + : nullptr; updateSendAction(item->history(), SendAction::Type::UploadPhoto, 0); Auth().data().requestItemRepaint(item); } diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 5fcd6b0b5..20e08e50e 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -3680,31 +3680,26 @@ void MainWidget::gotChannelDifference(ChannelData *channel, const MTPupdates_Cha // feed messages and groups, copy from App::feedMsgs auto h = App::history(channel->id); auto &vmsgs = d.vnew_messages.v; - QMap msgsIds; - for (int i = 0, l = vmsgs.size(); i < l; ++i) { - auto &msg = vmsgs[i]; - switch (msg.type()) { - case mtpc_message: { - const auto &d(msg.c_message()); - if (App::checkEntitiesAndViewsUpdate(d)) { // new message, index my forwarded messages to links _overview, already in blocks + auto indices = base::flat_map(); + for (auto i = 0, l = vmsgs.size(); i != l; ++i) { + const auto &msg = vmsgs[i]; + if (msg.type() == mtpc_message) { + const auto &data = msg.c_message(); + if (App::checkEntitiesAndViewsUpdate(data)) { // new message, index my forwarded messages to links _overview, already in blocks LOG(("Skipping message, because it is already in blocks!")); - } else { - msgsIds.insert((uint64(uint32(d.vid.v)) << 32) | uint64(i), i + 1); + continue; } - } break; - case mtpc_messageEmpty: msgsIds.insert((uint64(uint32(msg.c_messageEmpty().vid.v)) << 32) | uint64(i), i + 1); break; - case mtpc_messageService: msgsIds.insert((uint64(uint32(msg.c_messageService().vid.v)) << 32) | uint64(i), i + 1); break; } + const auto msgId = idFromMessage(msg); + indices.emplace((uint64(uint32(msgId)) << 32) | uint64(i), i); } - for_const (auto msgIndex, msgsIds) { - if (msgIndex > 0) { // add message - auto &msg = vmsgs.at(msgIndex - 1); - if (channel->id != peerFromMessage(msg)) { - LOG(("API Error: message with invalid peer returned in channelDifference, channelId: %1, peer: %2").arg(peerToChannel(channel->id)).arg(peerFromMessage(msg))); - continue; // wtf - } - h->addNewMessage(msg, NewMessageUnread); + for (const auto [position, index] : indices) { + const auto &msg = vmsgs[index]; + if (channel->id != peerFromMessage(msg)) { + LOG(("API Error: message with invalid peer returned in channelDifference, channelId: %1, peer: %2").arg(peerToChannel(channel->id)).arg(peerFromMessage(msg))); + continue; // wtf } + h->addNewMessage(msg, NewMessageUnread); } feedUpdateVector(d.vother_updates, true); diff --git a/Telegram/SourceFiles/mtproto/type_utils.h b/Telegram/SourceFiles/mtproto/type_utils.h index 6e704bd52..c2ac9029e 100644 --- a/Telegram/SourceFiles/mtproto/type_utils.h +++ b/Telegram/SourceFiles/mtproto/type_utils.h @@ -81,8 +81,11 @@ enum class MTPDmessage_ClientFlag : uint32 { // message has an admin badge in supergroup f_has_admin_badge = (1U << 20), + // message is not displayed because it is part of a group + f_hidden_by_group = (1U << 19), + // update this when adding new client side flags - MIN_FIELD = (1U << 20), + MIN_FIELD = (1U << 19), }; DEFINE_MTP_CLIENT_FLAGS(MTPDmessage) diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 790dbeb61..a8abfb80f 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -1201,7 +1201,9 @@ Link::Link( } } - _page = (media && media->type() == MediaTypeWebPage) ? static_cast(media)->webpage().get() : nullptr; + _page = (media && media->type() == MediaTypeWebPage) + ? static_cast(media)->webpage().get() + : nullptr; if (_page) { mainUrl = _page->url; if (_page->document) { diff --git a/Telegram/SourceFiles/ui/grouped_layout.cpp b/Telegram/SourceFiles/ui/grouped_layout.cpp new file mode 100644 index 000000000..0a14dbb60 --- /dev/null +++ b/Telegram/SourceFiles/ui/grouped_layout.cpp @@ -0,0 +1,589 @@ +/* +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 "ui/grouped_layout.h" + +namespace Data { +namespace { + +int Round(float64 value) { + return int(std::round(value)); +} + +class Layouter { +public: + Layouter( + const std::vector &sizes, + int maxWidth, + int minWidth, + int spacing); + + std::vector layout() const; + +private: + static std::vector CountRatios(const std::vector &sizes); + static std::string CountProportions(const std::vector &ratios); + + std::vector layoutTwo() const; + std::vector layoutThree() const; + std::vector layoutFour() const; + + std::vector layoutOne() const; + std::vector layoutTwoTopBottom() const; + std::vector layoutTwoLeftRightEqual() const; + std::vector layoutTwoLeftRight() const; + std::vector layoutThreeLeftAndOther() const; + std::vector layoutThreeTopAndOther() const; + std::vector layoutFourLeftAndOther() const; + std::vector layoutFourTopAndOther() const; + + const std::vector &_sizes; + const std::vector _ratios; + const std::string _proportions; + const int _count = 0; + const int _maxWidth = 0; + const int _maxHeight = 0; + const int _minWidth = 0; + const int _spacing = 0; + const float64 _averageRatio = 1.; + const float64 _maxSizeRatio = 1.; + +}; + +class ComplexLayouter { +public: + ComplexLayouter( + const std::vector &ratios, + float64 averageRatio, + int maxWidth, + int minWidth, + int spacing); + + std::vector layout() const; + +private: + struct Attempt { + std::vector lineCounts; + std::vector heights; + }; + + static std::vector CropRatios( + const std::vector &ratios, + float64 averageRatio); + + const std::vector _ratios; + const int _count = 0; + const int _maxWidth = 0; + const int _maxHeight = 0; + const int _minWidth = 0; + const int _spacing = 0; + const float64 _averageRatio = 1.; + +}; + +Layouter::Layouter( + const std::vector &sizes, + int maxWidth, + int minWidth, + int spacing) +: _sizes(sizes) +, _ratios(CountRatios(_sizes)) +, _proportions(CountProportions(_ratios)) +, _count(int(_ratios.size())) +// All apps currently use square max size first. +// In complex case they use maxWidth * 4 / 3 as maxHeight. +, _maxWidth(maxWidth) +, _maxHeight(maxWidth) +, _minWidth(minWidth) +, _spacing(spacing) +, _averageRatio(ranges::accumulate(_ratios, 0.) / _count) +, _maxSizeRatio(_maxWidth / float64(_maxHeight)) { +} + +std::vector Layouter::CountRatios(const std::vector &sizes) { + return ranges::view::all( + sizes + ) | ranges::view::transform([](const QSize &size) { + return size.width() / float64(size.height()); + }) | ranges::to_vector; +} + +std::string Layouter::CountProportions(const std::vector &ratios) { + return ranges::view::all( + ratios + ) | ranges::view::transform([](float64 ratio) { + return (ratio > 1.2) ? 'w' : (ratio < 0.8) ? 'n' : 'q'; + }) | ranges::to_(); +} + +std::vector Layouter::layout() const { + if (!_count) { + return {}; + } else if (_count == 1) { + return layoutOne(); + } + + using namespace rpl::mappers; + if (_count >= 5 || ranges::find_if(_ratios, _1 > 2) != _ratios.end()) { + return ComplexLayouter( + _ratios, + _averageRatio, + _maxWidth, + _minWidth, + _spacing).layout(); + } + + if (_count == 2) { + return layoutTwo(); + } else if (_count == 3) { + return layoutThree(); + } + return layoutFour(); +} + +std::vector Layouter::layoutTwo() const { + Expects(_count == 2); + + if ((_proportions == "ww") + && (_averageRatio > 1.4 * _maxSizeRatio) + && (_ratios[1] - _ratios[0] < 0.2)) { + return layoutTwoTopBottom(); + } else if (_proportions == "ww" || _proportions == "qq") { + return layoutTwoLeftRightEqual(); + } + return layoutTwoLeftRight(); +} + +std::vector Layouter::layoutThree() const { + Expects(_count == 3); + + auto result = std::vector(_count); + if (_proportions[0] == 'n') { + return layoutThreeLeftAndOther(); + } + return layoutThreeTopAndOther(); +} + +std::vector Layouter::layoutFour() const { + Expects(_count == 4); + + auto result = std::vector(_count); + if (_proportions[0] == 'w') { + return layoutFourTopAndOther(); + } + return layoutFourLeftAndOther(); +} + +std::vector Layouter::layoutOne() const { + Expects(_count == 1); + + const auto width = _maxWidth; + const auto height = (_sizes[0].height() * width) / _sizes[0].width(); + + return { + { + QRect(0, 0, width, height), + RectPart::Left | RectPart::Top | RectPart::Right | RectPart::Bottom + }, + }; +} + +std::vector Layouter::layoutTwoTopBottom() const { + Expects(_count == 2); + + const auto width = _maxWidth; + const auto height = Round(std::min( + width / _ratios[0], + std::min( + width / _ratios[1], + (_maxHeight - _spacing) / 2.))); + + return { + { + QRect(0, 0, width, height), + RectPart::Left | RectPart::Top | RectPart::Right + }, + { + QRect(0, height + _spacing, width, height), + RectPart::Left | RectPart::Bottom | RectPart::Right + }, + }; +} + +std::vector Layouter::layoutTwoLeftRightEqual() const { + Expects(_count == 2); + + const auto width = (_maxWidth - _spacing) / 2; + const auto height = Round(std::min( + width / _ratios[0], + std::min(width / _ratios[1], _maxHeight * 1.))); + + return { + { + QRect(0, 0, width, height), + RectPart::Top | RectPart::Left | RectPart::Bottom + }, + { + QRect(width + _spacing, 0, width, height), + RectPart::Top | RectPart::Right | RectPart::Bottom + }, + }; +} + +std::vector Layouter::layoutTwoLeftRight() const { + Expects(_count == 2); + + const auto minimalWidth = Round(_minWidth * 1.5); + const auto secondWidth = std::min( + Round(std::max( + 0.4 * (_maxWidth - _spacing), + (_maxWidth - _spacing) / _ratios[0] + / (1. / _ratios[0] + 1. / _ratios[1]))), + _maxWidth - _spacing - minimalWidth); + const auto firstWidth = _maxWidth + - secondWidth + - _spacing; + const auto height = std::min( + _maxHeight, + Round(std::min( + firstWidth / _ratios[0], + secondWidth / _ratios[1]))); + + return { + { + QRect(0, 0, firstWidth, height), + RectPart::Top | RectPart::Left | RectPart::Bottom + }, + { + QRect(firstWidth + _spacing, 0, secondWidth, height), + RectPart::Top | RectPart::Right | RectPart::Bottom + }, + }; +} + +std::vector Layouter::layoutThreeLeftAndOther() const { + Expects(_count == 3); + + const auto firstHeight = _maxHeight; + const auto thirdHeight = Round(std::min( + (_maxHeight - _spacing) / 2., + (_ratios[1] * (_maxWidth - _spacing) + / (_ratios[2] + _ratios[1])))); + const auto secondHeight = firstHeight + - thirdHeight + - _spacing; + const auto rightWidth = std::max( + _minWidth, + Round(std::min( + (_maxWidth - _spacing) / 2., + std::min( + thirdHeight * _ratios[2], + secondHeight * _ratios[1])))); + const auto leftWidth = std::min( + Round(firstHeight * _ratios[0]), + _maxWidth - _spacing - rightWidth); + + return { + { + QRect(0, 0, leftWidth, firstHeight), + RectPart::Top | RectPart::Left | RectPart::Bottom + }, + { + QRect(leftWidth + _spacing, 0, rightWidth, secondHeight), + RectPart::Top | RectPart::Right + }, + { + QRect(leftWidth + _spacing, secondHeight + _spacing, rightWidth, thirdHeight), + RectPart::Bottom | RectPart::Right + }, + }; +} + +std::vector Layouter::layoutThreeTopAndOther() const { + Expects(_count == 3); + + const auto firstWidth = _maxWidth; + const auto firstHeight = Round(std::min( + firstWidth / _ratios[0], + (_maxHeight - _spacing) * 0.66)); + const auto secondWidth = (_maxWidth - _spacing) / 2; + const auto secondHeight = std::min( + _maxHeight - firstHeight - _spacing, + Round(std::min( + secondWidth / _ratios[1], + secondWidth / _ratios[2]))); + const auto thirdWidth = firstWidth - secondWidth - _spacing; + + return { + { + QRect(0, 0, firstWidth, firstHeight), + RectPart::Left | RectPart::Top | RectPart::Right + }, + { + QRect(0, firstHeight + _spacing, secondWidth, secondHeight), + RectPart::Bottom | RectPart::Left + }, + { + QRect(secondWidth + _spacing, firstHeight + _spacing, thirdWidth, secondHeight), + RectPart::Bottom | RectPart::Right + }, + }; +} + +std::vector Layouter::layoutFourTopAndOther() const { + Expects(_count == 4); + + const auto w = _maxWidth; + const auto h0 = Round(std::min( + w / _ratios[0], + (_maxHeight - _spacing) * 0.66)); + const auto h = Round( + (_maxWidth - 2 * _spacing) + / (_ratios[1] + _ratios[2] + _ratios[3])); + const auto w0 = std::max( + _minWidth, + Round(std::min( + (_maxWidth - 2 * _spacing) * 0.4, + h * _ratios[1]))); + const auto w2 = Round(std::max( + std::max( + _minWidth * 1., + (_maxWidth - 2 * _spacing) * 0.33), + h * _ratios[3])); + const auto w1 = w - w0 - w2 - 2 * _spacing; + const auto h1 = std::min( + _maxHeight - h0 - _spacing, + h); + + return { + { + QRect(0, 0, w, h0), + RectPart::Left | RectPart::Top | RectPart::Right + }, + { + QRect(0, h0 + _spacing, w0, h1), + RectPart::Bottom | RectPart::Left + }, + { + QRect(w0 + _spacing, h0 + _spacing, w1, h1), + RectPart::Bottom, + }, + { + QRect(w0 + _spacing + w1 + _spacing, h0 + _spacing, w2, h1), + RectPart::Right | RectPart::BottomLeft + }, + }; +} + +std::vector Layouter::layoutFourLeftAndOther() const { + Expects(_count == 4); + + const auto h = _maxHeight; + const auto w0 = Round(std::min( + h * _ratios[0], + (_maxWidth - _spacing) * 0.6)); + + const auto w = Round( + (_maxHeight - 2 * _spacing) + / (1. / _ratios[1] + 1. / _ratios[2] + 1. / _ratios[3]) + ); + const auto h0 = Round(w / _ratios[1]); + const auto h1 = Round(w / _ratios[2]); + const auto h2 = h - h0 - h1 - 2 * _spacing; + const auto w1 = std::max( + _minWidth, + std::min(_maxWidth - w0 - _spacing, w)); + + return { + { + QRect(0, 0, w0, h), + RectPart::Top | RectPart::Left | RectPart::Bottom + }, + { + QRect(w0 + _spacing, 0, w1, h0), + RectPart::Top | RectPart::Right + }, + { + QRect(w0 + _spacing, h0 + _spacing, w1, h1), + RectPart::Right + }, + { + QRect(w0 + _spacing, h0 + h1 + 2 * _spacing, w1, h2), + RectPart::Bottom | RectPart::Right + }, + }; +} + +ComplexLayouter::ComplexLayouter( + const std::vector &ratios, + float64 averageRatio, + int maxWidth, + int minWidth, + int spacing) +: _ratios(CropRatios(ratios, averageRatio)) +, _count(int(_ratios.size())) +// All apps currently use square max size first. +// In complex case they use maxWidth * 4 / 3 as maxHeight. +, _maxWidth(maxWidth) +, _maxHeight(maxWidth * 4 / 3) +, _minWidth(minWidth) +, _spacing(spacing) +, _averageRatio(averageRatio) { +} + +std::vector ComplexLayouter::CropRatios( + const std::vector &ratios, + float64 averageRatio) { + return ranges::view::all( + ratios + ) | ranges::view::transform([&](float64 ratio) { + return (averageRatio > 1.1) + ? snap(ratio, 1., 1.7) + : snap(ratio, 0.66667, 1.); + }) | ranges::to_vector; +} + +std::vector ComplexLayouter::layout() const { + Expects(_count > 1); + + auto result = std::vector(_count); + + auto attempts = std::vector(); + const auto multiHeight = [&](int offset, int count) { + const auto ratios = gsl::make_span(_ratios).subspan(offset, count); + const auto sum = ranges::accumulate(ratios, 0.); + return (_maxWidth - (count - 1) * _spacing) / sum; + }; + const auto pushAttempt = [&](std::vector lineCounts) { + auto heights = std::vector(); + heights.reserve(lineCounts.size()); + auto offset = 0; + for (auto count : lineCounts) { + heights.push_back(multiHeight(offset, count)); + offset += count; + } + attempts.push_back({ std::move(lineCounts), std::move(heights) }); + }; + + for (auto first = 1; first != _count; ++first) { + const auto second = _count - first; + if (first > 3 || second > 3) { + continue; + } + pushAttempt({ first, second }); + } + for (auto first = 1; first != _count - 1; ++first) { + for (auto second = 1; second != _count - first; ++second) { + const auto third = _count - first - second; + if ((first > 3) + || (second > ((_averageRatio < 0.85) ? 4 : 3)) + || (third > 3)) { + continue; + } + pushAttempt({ first, second, third }); + } + } + for (auto first = 1; first != _count - 1; ++first) { + for (auto second = 1; second != _count - first; ++second) { + for (auto third = 1; third != _count - first - second; ++third) { + const auto fourth = _count - first - second - third; + if (first > 3 || second > 3 || third > 3 || fourth > 3) { + continue; + } + pushAttempt({ first, second, third, fourth }); + } + } + } + + auto optimalAttempt = (const Attempt*)nullptr; + auto optimalDiff = 0.; + for (const auto &attempt : attempts) { + const auto &heights = attempt.heights; + const auto &counts = attempt.lineCounts; + const auto lineCount = int(counts.size()); + const auto totalHeight = ranges::accumulate(heights, 0.) + + _spacing * (lineCount - 1); + const auto minLineHeight = ranges::min(heights); + const auto maxLineHeight = ranges::max(heights); + const auto bad1 = (minLineHeight < _minWidth) ? 1.5 : 1.; + const auto bad2 = [&] { + for (auto line = 1; line != lineCount; ++line) { + if (counts[line - 1] > counts[line]) { + return 1.5; + } + } + return 1.; + }(); + const auto diff = std::abs(totalHeight - _maxHeight) * bad1 * bad2; + if (!optimalAttempt || diff < optimalDiff) { + optimalAttempt = &attempt; + optimalDiff = diff; + } + } + Assert(optimalAttempt != nullptr); + + const auto &optimalCounts = optimalAttempt->lineCounts; + const auto &optimalHeights = optimalAttempt->heights; + const auto rowCount = int(optimalCounts.size()); + + auto index = 0; + auto y = 0.; + for (auto row = 0; row != rowCount; ++row) { + const auto colCount = optimalCounts[row]; + const auto lineHeight = optimalHeights[row]; + const auto height = Round(lineHeight); + + auto x = 0; + for (auto col = 0; col != colCount; ++col) { + const auto sides = RectPart::None + | (row == 0 ? RectPart::Top : RectPart::None) + | (row == rowCount - 1 ? RectPart::Bottom : RectPart::None) + | (col == 0 ? RectPart::Left : RectPart::None) + | (col == colCount - 1 ? RectPart::Right : RectPart::None); + + const auto ratio = _ratios[index]; + const auto width = (col == colCount - 1) + ? (_maxWidth - x) + : Round(ratio * lineHeight); + result[index] = { + QRect(x, y, width, height), + sides + }; + + x += width + _spacing; + ++index; + } + y += height + _spacing; + } + + return result; +} + +} // namespace + +std::vector LayoutMediaGroup( + const std::vector &sizes, + int maxWidth, + int minWidth, + int spacing) { + return Layouter(sizes, maxWidth, minWidth, spacing).layout(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/ui/grouped_layout.h b/Telegram/SourceFiles/ui/grouped_layout.h new file mode 100644 index 000000000..1efb4b9cb --- /dev/null +++ b/Telegram/SourceFiles/ui/grouped_layout.h @@ -0,0 +1,36 @@ +/* +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 + +namespace Data { + +struct GroupMediaLayout { + QRect geometry; + RectParts sides = RectPart::None; +}; + +std::vector LayoutMediaGroup( + const std::vector &sizes, + int maxWidth, + int minWidth, + int spacing); + +} // namespace Data diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index bbf2b2ade..f69d23cc4 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -219,6 +219,8 @@ <(src_loc)/history/history_location_manager.h <(src_loc)/history/history_media.h <(src_loc)/history/history_media.cpp +<(src_loc)/history/history_media_grouped.h +<(src_loc)/history/history_media_grouped.cpp <(src_loc)/history/history_media_types.cpp <(src_loc)/history/history_media_types.h <(src_loc)/history/history_message.cpp @@ -607,6 +609,8 @@ <(src_loc)/ui/empty_userpic.cpp <(src_loc)/ui/empty_userpic.h <(src_loc)/ui/focus_persister.h +<(src_loc)/ui/grouped_layout.cpp +<(src_loc)/ui/grouped_layout.h <(src_loc)/ui/images.cpp <(src_loc)/ui/images.h <(src_loc)/ui/resize_area.h