/* 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-2016 John Preston, https://desktop.telegram.org */ #include "stdafx.h" #include "history.h" #include "core/click_handler_types.h" #include "dialogs/dialogs_indexed_list.h" #include "ui/style.h" #include "lang.h" #include "mainwidget.h" #include "application.h" #include "fileuploader.h" #include "mainwindow.h" #include "ui/filedialog.h" #include "boxes/addcontactbox.h" #include "boxes/confirmbox.h" #include "audio.h" #include "localstorage.h" #include "apiwrap.h" #include "window/top_bar_widget.h" #include "playerwidget.h" namespace { TextParseOptions _historySrvOptions = { TextParseLinks | TextParseMentions | TextParseHashtags | TextParseMultiline | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // lang-dependent }; TextParseOptions _webpageTitleOptions = { TextParseMultiline | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; TextParseOptions _webpageDescriptionOptions = { TextParseLinks | TextParseMultiline | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; TextParseOptions _twitterDescriptionOptions = { TextParseLinks | TextParseMentions | TextTwitterMentions | TextParseHashtags | TextTwitterHashtags | TextParseMultiline | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; TextParseOptions _instagramDescriptionOptions = { TextParseLinks | TextParseMentions | TextInstagramMentions | TextParseHashtags | TextInstagramHashtags | TextParseMultiline | TextParseRichText, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; inline void _initTextOptions() { _historySrvOptions.dir = _textNameOptions.dir = _textDlgOptions.dir = cLangDir(); _textDlgOptions.maxw = st::dlgMaxWidth * 2; _webpageTitleOptions.maxw = st::msgMaxWidth - st::msgPadding.left() - st::msgPadding.right() - st::webPageLeft; _webpageTitleOptions.maxh = st::webPageTitleFont->height * 2; _webpageDescriptionOptions.maxw = st::msgMaxWidth - st::msgPadding.left() - st::msgPadding.right() - st::webPageLeft; _webpageDescriptionOptions.maxh = st::webPageDescriptionFont->height * 3; } inline const TextParseOptions &itemTextOptions(HistoryItem *item) { return itemTextOptions(item->history(), item->author()); } inline const TextParseOptions &itemTextNoMonoOptions(const HistoryItem *item) { return itemTextNoMonoOptions(item->history(), item->author()); } bool needReSetInlineResultDocument(const MTPMessageMedia &media, DocumentData *existing) { if (media.type() == mtpc_messageMediaDocument) { if (DocumentData *document = App::feedDocument(media.c_messageMediaDocument().vdocument)) { if (document == existing) { return false; } else { document->collectLocalData(existing); } } } return true; } } // namespace void historyInit() { _initTextOptions(); } History::History(const PeerId &peerId) : peer(App::peer(peerId)) , _mute(isNotifyMuted(peer->notify)) { if (peer->isChannel() || (peer->isUser() && peer->asUser()->botInfo)) { outboxReadBefore = INT_MAX; } for (auto &countData : overviewCountData) { countData = -1; // not loaded yet } } void History::clearLastKeyboard() { if (lastKeyboardId) { if (lastKeyboardId == lastKeyboardHiddenId) { lastKeyboardHiddenId = 0; } lastKeyboardId = 0; } lastKeyboardInited = true; lastKeyboardFrom = 0; } bool History::canHaveFromPhotos() const { if (peer->isUser() && !Adaptive::Wide()) { return false; } else if (isChannel() && asChannelHistory()->onlyImportant()) { return false; } return true; } void History::setHasPendingResizedItems() { _flags |= Flag::f_has_pending_resized_items; Global::RefHandleHistoryUpdate().call(); } bool History::updateTyping(uint64 ms, bool force) { bool changed = force; for (TypingUsers::iterator i = typing.begin(), e = typing.end(); i != e;) { if (ms >= i.value()) { i = typing.erase(i); changed = true; } else { ++i; } } for (SendActionUsers::iterator i = sendActions.begin(); i != sendActions.cend();) { if (ms >= i.value().until) { i = sendActions.erase(i); changed = true; } else { ++i; } } if (changed) { QString newTypingStr; int32 cnt = typing.size(); if (cnt > 2) { newTypingStr = lng_many_typing(lt_count, cnt); } else if (cnt > 1) { newTypingStr = lng_users_typing(lt_user, typing.begin().key()->firstName, lt_second_user, (typing.end() - 1).key()->firstName); } else if (cnt) { newTypingStr = peer->isUser() ? lang(lng_typing) : lng_user_typing(lt_user, typing.begin().key()->firstName); } else if (!sendActions.isEmpty()) { switch (sendActions.begin().value().type) { case SendActionRecordVideo: newTypingStr = peer->isUser() ? lang(lng_send_action_record_video) : lng_user_action_record_video(lt_user, sendActions.begin().key()->firstName); break; case SendActionUploadVideo: newTypingStr = peer->isUser() ? lang(lng_send_action_upload_video) : lng_user_action_upload_video(lt_user, sendActions.begin().key()->firstName); break; case SendActionRecordVoice: newTypingStr = peer->isUser() ? lang(lng_send_action_record_audio) : lng_user_action_record_audio(lt_user, sendActions.begin().key()->firstName); break; case SendActionUploadVoice: newTypingStr = peer->isUser() ? lang(lng_send_action_upload_audio) : lng_user_action_upload_audio(lt_user, sendActions.begin().key()->firstName); break; case SendActionUploadPhoto: newTypingStr = peer->isUser() ? lang(lng_send_action_upload_photo) : lng_user_action_upload_photo(lt_user, sendActions.begin().key()->firstName); break; case SendActionUploadFile: newTypingStr = peer->isUser() ? lang(lng_send_action_upload_file) : lng_user_action_upload_file(lt_user, sendActions.begin().key()->firstName); break; case SendActionChooseLocation: newTypingStr = peer->isUser() ? lang(lng_send_action_geo_location) : lng_user_action_geo_location(lt_user, sendActions.begin().key()->firstName); break; case SendActionChooseContact: newTypingStr = peer->isUser() ? lang(lng_send_action_choose_contact) : lng_user_action_choose_contact(lt_user, sendActions.begin().key()->firstName); break; } } if (!newTypingStr.isEmpty()) { newTypingStr += qsl("..."); } if (typingStr != newTypingStr) { typingText.setText(st::dlgHistFont, (typingStr = newTypingStr), _textNameOptions); } } if (!typingStr.isEmpty()) { if (typingText.lastDots(typingDots % 4)) { changed = true; } } if (changed && App::main()) { updateChatListEntry(); if (App::main()->historyPeer() == peer) { App::main()->topBar()->update(); } } return changed; } ChannelHistory::ChannelHistory(const PeerId &peer) : History(peer) , unreadCountAll(0) , _onlyImportant(!isMegagroup()) , _otherOldLoaded(false) , _otherNewLoaded(true) , _collapseMessage(nullptr) , _joinedMessage(nullptr) { } bool ChannelHistory::isSwitchReadyFor(MsgId switchId, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop) { if (switchId == SwitchAtTopMsgId) { if (_onlyImportant) { if (isMegagroup()) switchMode(); return true; } int32 bottomUnderScrollTop = 0; HistoryItem *atTopItem = App::main()->atTopImportantMsg(bottomUnderScrollTop); if (atTopItem) { fixInScrollMsgId = atTopItem->id; fixInScrollMsgTop = atTopItem->y + atTopItem->block()->y + atTopItem->height() - bottomUnderScrollTop - height; if (_otherList.indexOf(atTopItem) >= 0) { switchMode(); return true; } return false; } if (!_otherList.isEmpty()) { switchMode(); return true; } return false; } if (HistoryItem *item = App::histItemById(channelId(), switchId)) { HistoryItemType itemType = item->type(); if (itemType == HistoryItemGroup || itemType == HistoryItemCollapse) { if (isMegagroup()) return true; if (itemType == HistoryItemGroup && !_onlyImportant) return true; if (itemType == HistoryItemCollapse && _onlyImportant) return true; bool willNeedCollapse = (itemType == HistoryItemGroup); HistoryItem *prev = findPrevItem(item); if (prev) { fixInScrollMsgId = prev->id; fixInScrollMsgTop = prev->y + prev->block()->y + prev->height() - height; if (_otherList.indexOf(prev) >= 0) { switchMode(); insertCollapseItem(fixInScrollMsgId); return true; } return false; } if (itemType == HistoryItemGroup) { fixInScrollMsgId = qMax(static_cast(item)->minId(), 1); fixInScrollMsgTop = item->y + item->block()->y - height; if (oldLoaded && _otherOldLoaded) { switchMode(); insertCollapseItem(fixInScrollMsgId); return true; } } else if (itemType == HistoryItemCollapse) { fixInScrollMsgId = qMax(static_cast(item)->wasMinId(), 1); fixInScrollMsgTop = item->y + item->block()->y - height; if (oldLoaded && _otherOldLoaded) { switchMode(); return true; } } return false; } if (item->history() == this) { if (_onlyImportant && !item->isImportant()) { if (_otherList.indexOf(item) >= 0) { switchMode(); return true; } return false; } else if (!item->detached()) { return true; } } } else if (switchId < 0) { LOG(("App Error: isSwitchReadyFor() switchId not found!")); switchMode(); return true; } return false; } void ChannelHistory::getSwitchReadyFor(MsgId switchId, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop) { if (!isSwitchReadyFor(switchId, fixInScrollMsgId, fixInScrollMsgTop)) { if (switchId > 0) { if (HistoryItem *item = App::histItemById(channelId(), switchId)) { if (_onlyImportant && !item->isImportant()) { _otherList.clear(); _otherNewLoaded = _otherOldLoaded = false; switchMode(); } else { clear(true); } } else { clear(true); } } else { _otherList.clear(); _otherNewLoaded = _otherOldLoaded = false; switchMode(); } } } void ChannelHistory::insertCollapseItem(MsgId wasMinId) { if (_onlyImportant || isMegagroup()) return; bool insertAfter = false; for (int32 blockIndex = 0, blocksCount = blocks.size(); blockIndex < blocksCount; ++blockIndex) { HistoryBlock *block = blocks.at(blockIndex); for (int32 itemIndex = 0, itemsCount = block->items.size(); itemIndex < itemsCount; ++itemIndex) { HistoryItem *item = block->items.at(itemIndex); if (insertAfter || item->id > wasMinId || (item->id == wasMinId && !item->isImportant())) { _collapseMessage = HistoryCollapse::create((History*)this, wasMinId, item->date); addNewInTheMiddle(_collapseMessage, blockIndex, itemIndex); return; } else if (item->id == wasMinId && item->isImportant()) { insertAfter = true; } } } } void ChannelHistory::getRangeDifference() { MsgId fromId = 0, toId = 0; for (int32 blockIndex = 0, blocksCount = blocks.size(); blockIndex < blocksCount; ++blockIndex) { HistoryBlock *block = blocks.at(blockIndex); for (int32 itemIndex = 0, itemsCount = block->items.size(); itemIndex < itemsCount; ++itemIndex) { HistoryItem *item = block->items.at(itemIndex); if (item->type() == HistoryItemMsg && item->id > 0) { fromId = item->id; break; } else if (item->type() == HistoryItemGroup) { fromId = static_cast(item)->minId() + 1; break; } } if (fromId) break; } if (!fromId) return; for (int32 blockIndex = blocks.size(); blockIndex > 0;) { HistoryBlock *block = blocks.at(--blockIndex); for (int32 itemIndex = block->items.size(); itemIndex > 0;) { HistoryItem *item = block->items.at(--itemIndex); if (item->type() == HistoryItemMsg && item->id > 0) { toId = item->id; break; } else if (item->type() == HistoryItemGroup) { toId = static_cast(item)->maxId() - 1; break; } } if (toId) break; } if (fromId > 0 && peer->asChannel()->pts() > 0) { if (_rangeDifferenceRequestId) { MTP::cancel(_rangeDifferenceRequestId); } _rangeDifferenceFromId = fromId; _rangeDifferenceToId = toId; MTP_LOG(0, ("getChannelDifference { good - after channelDifferenceTooLong was received, validating history part }%1").arg(cTestMode() ? " TESTMODE" : "")); getRangeDifferenceNext(peer->asChannel()->pts()); } } void ChannelHistory::getRangeDifferenceNext(int32 pts) { if (!App::main() || _rangeDifferenceToId < _rangeDifferenceFromId) return; int32 limit = _rangeDifferenceToId + 1 - _rangeDifferenceFromId; _rangeDifferenceRequestId = MTP::send(MTPupdates_GetChannelDifference(peer->asChannel()->inputChannel, MTP_channelMessagesFilter(MTP_flags(MTPDchannelMessagesFilter::Flags(0)), MTP_vector(1, MTP_messageRange(MTP_int(_rangeDifferenceFromId), MTP_int(_rangeDifferenceToId)))), MTP_int(pts), MTP_int(limit)), App::main()->rpcDone(&MainWidget::gotRangeDifference, peer->asChannel())); } void ChannelHistory::addNewGroup(const MTPMessageGroup &group) { if (group.type() != mtpc_messageGroup) return; const auto &d(group.c_messageGroup()); if (onlyImportant()) { _otherNewLoaded = false; } else if (_otherNewLoaded) { if (_otherList.isEmpty() || _otherList.back()->type() != HistoryItemGroup) { _otherList.push_back(HistoryGroup::create(this, d, _otherList.isEmpty() ? date(d.vdate) : _otherList.back()->date)); } else { static_cast(_otherList.back())->uniteWith(d.vmin_id.v, d.vmax_id.v, d.vcount.v); } } if (onlyImportant()) { if (newLoaded) { t_assert(!isBuildingFrontBlock()); addMessageGroup(d); } } else { setNotLoadedAtBottom(); } } HistoryJoined *ChannelHistory::insertJoinedMessage(bool unread) { if (_joinedMessage || !peer->asChannel()->amIn() || (peer->isMegagroup() && peer->asChannel()->mgInfo->joinedMessageFound)) { return _joinedMessage; } UserData *inviter = (peer->asChannel()->inviter > 0) ? App::userLoaded(peer->asChannel()->inviter) : nullptr; if (!inviter) return nullptr; MTPDmessage::Flags flags = 0; if (peerToUser(inviter->id) == MTP::authedId()) { unread = false; } else if (unread) { flags |= MTPDmessage::Flag::f_unread; } QDateTime inviteDate = peer->asChannel()->inviteDate; if (unread) _maxReadMessageDate = inviteDate; if (isEmpty()) { _joinedMessage = HistoryJoined::create(this, inviteDate, inviter, flags); addNewItem(_joinedMessage, unread); return _joinedMessage; } for (int32 blockIndex = blocks.size(); blockIndex > 0;) { HistoryBlock *block = blocks.at(--blockIndex); for (int32 itemIndex = block->items.size(); itemIndex > 0;) { HistoryItem *item = block->items.at(--itemIndex); HistoryItemType type = item->type(); if (type == HistoryItemMsg || type == HistoryItemGroup) { if (item->date <= inviteDate) { if (peer->isMegagroup() && peer->migrateFrom() && item->isGroupMigrate()) { peer->asChannel()->mgInfo->joinedMessageFound = true; return nullptr; } ++itemIndex; _joinedMessage = HistoryJoined::create(this, inviteDate, inviter, flags); addNewInTheMiddle(_joinedMessage, blockIndex, itemIndex); if (lastMsgDate.isNull() || inviteDate >= lastMsgDate) { setLastMessage(_joinedMessage); if (unread) { newItemAdded(_joinedMessage); } } return _joinedMessage; } } } } startBuildingFrontBlock(); _joinedMessage = HistoryJoined::create(this, inviteDate, inviter, flags); addItemToBlock(_joinedMessage); finishBuildingFrontBlock(); return _joinedMessage; } void ChannelHistory::checkJoinedMessage(bool createUnread) { if (_joinedMessage || peer->asChannel()->inviter <= 0) { return; } if (isEmpty()) { if (loadedAtTop() && loadedAtBottom()) { if (insertJoinedMessage(createUnread)) { if (!_joinedMessage->detached()) { setLastMessage(_joinedMessage); } } return; } } QDateTime inviteDate = peer->asChannel()->inviteDate; QDateTime firstDate, lastDate; for (int blockIndex = 0, blocksCount = blocks.size(); blockIndex < blocksCount; ++blockIndex) { HistoryBlock *block = blocks.at(blockIndex); int itemIndex = 0, itemsCount = block->items.size(); for (; itemIndex < itemsCount; ++itemIndex) { HistoryItem *item = block->items.at(itemIndex); HistoryItemType type = item->type(); if (type == HistoryItemMsg || type == HistoryItemGroup) { firstDate = item->date; break; } } if (itemIndex < itemsCount) break; } for (int blockIndex = blocks.size(); blockIndex > 0;) { HistoryBlock *block = blocks.at(--blockIndex); int itemIndex = block->items.size(); for (; itemIndex > 0;) { HistoryItem *item = block->items.at(--itemIndex); HistoryItemType type = item->type(); if (type == HistoryItemMsg || type == HistoryItemGroup) { lastDate = item->date; ++itemIndex; break; } } if (itemIndex) break; } if (!firstDate.isNull() && !lastDate.isNull() && (firstDate <= inviteDate || loadedAtTop()) && (lastDate > inviteDate || loadedAtBottom())) { bool willBeLastMsg = (inviteDate >= lastDate); if (insertJoinedMessage(createUnread && willBeLastMsg) && willBeLastMsg) { if (!_joinedMessage->detached()) { setLastMessage(_joinedMessage); } } } } void ChannelHistory::checkMaxReadMessageDate() { if (_maxReadMessageDate.isValid()) return; for (int blockIndex = blocks.size(); blockIndex > 0;) { HistoryBlock *block = blocks.at(--blockIndex); for (int itemIndex = block->items.size(); itemIndex > 0;) { HistoryItem *item = block->items.at(--itemIndex); if ((item->isImportant() || isMegagroup()) && !item->unread()) { _maxReadMessageDate = item->date; if (item->isGroupMigrate() && isMegagroup() && peer->migrateFrom()) { _maxReadMessageDate = date(MTP_int(peer->asChannel()->date + 1)); // no report spam panel } return; } } } if (loadedAtTop() && (!isMegagroup() || !isEmpty())) { _maxReadMessageDate = date(MTP_int(peer->asChannel()->date)); } } const QDateTime &ChannelHistory::maxReadMessageDate() { return _maxReadMessageDate; } HistoryItem *ChannelHistory::addNewChannelMessage(const MTPMessage &msg, NewMessageType type) { if (type == NewMessageExisting) return addToHistory(msg); HistoryItem *result = addNewToBlocks(msg, type); if (result) addNewToOther(result, type); return result; } HistoryItem *ChannelHistory::addNewToBlocks(const MTPMessage &msg, NewMessageType type) { bool isImportantFlags = isImportantChannelMessage(idFromMessage(msg), flagsFromMessage(msg)); bool isImportant = (isChannel() && !isMegagroup()) ? isImportantFlags : true; if (!loadedAtBottom()) { HistoryItem *item = addToHistory(msg); if (item && isImportant) { setLastMessage(item); if (type == NewMessageUnread) { newItemAdded(item); } } return item; } if (!isImportant && onlyImportant()) { HistoryItem *item = addToHistory(msg); t_assert(!isBuildingFrontBlock()); addMessageGroup([item, this](HistoryItem *previous) -> HistoryGroup* { // create(..) return HistoryGroup::create(this, item, previous ? previous->date : item->date); }, [item](HistoryGroup *existing) { // unite(..) existing->uniteWith(item); }); return item; } // when we are receiving channel dialog rows we get one important and one not important // message for each history, adding all of them with type == NewMessageLast // if we get a second (not important) message of this two we need to clear the history // because a lot of messages in between those two are skipped if (!isImportantFlags && !onlyImportant() && !isEmpty() && type == NewMessageLast) { clear(true); newLoaded = true; // adding the last message } return addNewToLastBlock(msg, type); } void ChannelHistory::addNewToOther(HistoryItem *item, NewMessageType type) { if (!_otherNewLoaded || isMegagroup()) return; if (!item->isImportant()) { if (onlyImportant()) { if (type == NewMessageLast) { _otherList.clear(); _otherOldLoaded = false; } } else { if (_otherList.isEmpty() || _otherList.back()->type() != HistoryItemGroup) { _otherList.push_back(HistoryGroup::create(this, item, _otherList.isEmpty() ? item->date : _otherList.back()->date)); } else { static_cast(_otherList.back())->uniteWith(item); } return; } } _otherList.push_back(item); } void ChannelHistory::switchMode() { if (isMegagroup() && !_onlyImportant) return; OtherList savedList; if (!blocks.isEmpty()) { savedList.reserve(((blocks.size() - 1) * MessagesPerPage + blocks.back()->items.size()) * (onlyImportant() ? 2 : 1)); for_const (const HistoryBlock *block, blocks) { for_const (HistoryItem *item, block->items) { HistoryItemType itemType = item->type(); if (itemType == HistoryItemMsg || itemType == HistoryItemGroup) { savedList.push_back(item); } } } } bool savedNewLoaded = newLoaded, savedOldLoaded = oldLoaded; clear(true); t_assert(!isBuildingFrontBlock()); newLoaded = _otherNewLoaded; oldLoaded = _otherOldLoaded; if (int count = _otherList.size()) { blocks.reserve((count / MessagesPerPage) + 1); for (int i = 0; i < count; ++i) { t_assert(_otherList.at(i)->detached()); addItemToBlock(_otherList.at(i)); } } _otherList = savedList; _otherNewLoaded = savedNewLoaded; _otherOldLoaded = savedOldLoaded; _onlyImportant = !_onlyImportant; // scroll to the bottom if nothing special is intended // (like scrolling to the collapse item) scrollTopItem = nullptr; checkJoinedMessage(); } void ChannelHistory::cleared(bool leaveItems) { _collapseMessage = nullptr; _joinedMessage = nullptr; if (!leaveItems) { _otherList.clear(); } } HistoryGroup *ChannelHistory::findGroup(MsgId msgId) const { // find message group using binary search if (!_onlyImportant) return findGroupInOther(msgId); HistoryBlock *block = findGroupBlock(msgId); if (!block) return nullptr; int32 itemIndex = 0; if (block->items.size() > 1) for (int32 minItem = 0, maxItem = block->items.size();;) { for (int32 startCheckItem = (minItem + maxItem) / 2, checkItem = startCheckItem;;) { HistoryItem *item = block->items.at(checkItem); // out msgs could be a mess in monotonic ids if ((item->id > 0 && !item->out()) || item->type() == HistoryItemGroup) { MsgId threshold = (item->id > 0) ? item->id : static_cast(item)->minId(); if (threshold > msgId) { maxItem = startCheckItem; } else { minItem = checkItem; } break; } if (++checkItem == maxItem) { maxItem = startCheckItem; break; } } if (minItem + 1 == maxItem) { itemIndex = minItem; break; } } auto item = block->items.at(itemIndex); if (item->type() == HistoryItemGroup) { auto result = static_cast(item); if (result->minId() < msgId && result->maxId() > msgId) { return result; } } return nullptr; } HistoryBlock *ChannelHistory::findGroupBlock(MsgId msgId) const { // find block with message group using binary search if (isEmpty()) return nullptr; int blockIndex = 0; if (blocks.size() > 1) for (int minBlock = 0, maxBlock = blocks.size();;) { for (int startCheckBlock = (minBlock + maxBlock) / 2, checkBlock = startCheckBlock;;) { HistoryBlock *block = blocks.at(checkBlock); auto i = block->items.cbegin(), e = block->items.cend(); for (; i != e; ++i) { // out msgs could be a mess in monotonic ids if (((*i)->id > 0 && !(*i)->out()) || (*i)->type() == HistoryItemGroup) { MsgId threshold = ((*i)->id > 0) ? (*i)->id : static_cast(*i)->minId(); if (threshold > msgId) { maxBlock = startCheckBlock; } else { minBlock = checkBlock; } break; } } if (i != e) { break; } if (++checkBlock == maxBlock) { maxBlock = startCheckBlock; break; } } if (minBlock + 1 == maxBlock) { blockIndex = minBlock; break; } } return blocks.at(blockIndex); } HistoryGroup *ChannelHistory::findGroupInOther(MsgId msgId) const { // find message group using binary search in _otherList if (_otherList.isEmpty()) return 0; int32 otherIndex = 0; if (_otherList.size() > 1) for (int32 minOther = 0, maxOther = _otherList.size();;) { for (int32 startCheckOther = (minOther + maxOther) / 2, checkOther = startCheckOther;;) { HistoryItem *item = _otherList.at(checkOther); // out msgs could be a mess in monotonic ids if ((item->id > 0 && !item->out()) || item->type() == HistoryItemGroup) { MsgId threshold = (item->id > 0) ? item->id : static_cast(item)->minId(); if (threshold > msgId) { maxOther = startCheckOther; } else { minOther = checkOther; } break; } if (++checkOther == maxOther) { maxOther = startCheckOther; break; } } if (minOther + 1 == maxOther) { otherIndex = minOther; break; } } HistoryItem *item = _otherList.at(otherIndex); if (item->type() != HistoryItemGroup) return nullptr; HistoryGroup *result = static_cast(item); return (result->minId() < msgId && result->maxId() > msgId) ? result : nullptr; } HistoryItem *ChannelHistory::findPrevItem(HistoryItem *item) const { if (item->detached()) return nullptr; int itemIndex = item->indexInBlock(); int blockIndex = item->block()->indexInHistory(); for (++blockIndex, ++itemIndex; blockIndex > 0;) { --blockIndex; HistoryBlock *block = blocks.at(blockIndex); if (!itemIndex) itemIndex = block->items.size(); for (; itemIndex > 0;) { --itemIndex; if (block->items.at(itemIndex)->type() == HistoryItemMsg) { return block->items.at(itemIndex); } } } return nullptr; } void ChannelHistory::messageDetached(HistoryItem *msg) { if (_collapseMessage == msg) { _collapseMessage = nullptr; } else if (_joinedMessage == msg) { _joinedMessage = nullptr; } } void ChannelHistory::messageDeleted(HistoryItem *msg) { int32 otherIndex = _otherList.indexOf(msg); if (otherIndex >= 0) _otherList.removeAt(otherIndex); if (msg->isImportant()) { // unite message groups around this important message in _otherList if (!_onlyImportant && otherIndex > 0 && otherIndex < _otherList.size()) { if (HistoryGroup *groupPrev = (_otherList[otherIndex - 1]->type() == HistoryItemGroup) ? static_cast(_otherList[otherIndex - 1]) : 0) { if (HistoryGroup *groupNext = (_otherList[otherIndex]->type() == HistoryItemGroup) ? static_cast(_otherList[otherIndex]) : 0) { groupPrev->uniteWith(groupNext); groupNext->destroy(); } } } } else { messageWithIdDeleted(msg->id); } } void ChannelHistory::messageWithIdDeleted(MsgId msgId) { if (HistoryGroup *group = findGroup(msgId)) { if (!group->decrementCount()) { group->destroy(); } } } ChannelHistory::~ChannelHistory() { // all items must be destroyed before ChannelHistory is destroyed // or they will call history()->asChannelHistory() -> undefined behaviour clearOnDestroy(); } History *Histories::find(const PeerId &peerId) { Map::const_iterator i = map.constFind(peerId); return (i == map.cend()) ? 0 : i.value(); } History *Histories::findOrInsert(const PeerId &peerId, int32 unreadCount, int32 maxInboxRead) { Map::const_iterator i = map.constFind(peerId); if (i == map.cend()) { i = map.insert(peerId, peerIsChannel(peerId) ? static_cast(new ChannelHistory(peerId)) : (new History(peerId))); i.value()->setUnreadCount(unreadCount); i.value()->inboxReadBefore = maxInboxRead + 1; } return i.value(); } void Histories::clear() { App::historyClearMsgs(); Map temp; std::swap(temp, map); for_const (auto history, temp) { delete history; } _unreadFull = _unreadMuted = 0; Notify::unreadCounterUpdated(); App::historyClearItems(); typing.clear(); } void Histories::regSendAction(History *history, UserData *user, const MTPSendMessageAction &action) { if (action.type() == mtpc_sendMessageCancelAction) { history->unregTyping(user); return; } uint64 ms = getms(); switch (action.type()) { case mtpc_sendMessageTypingAction: history->typing[user] = ms + 6000; break; case mtpc_sendMessageRecordVideoAction: history->sendActions.insert(user, SendAction(SendActionRecordVideo, ms + 6000)); break; case mtpc_sendMessageUploadVideoAction: history->sendActions.insert(user, SendAction(SendActionUploadVideo, ms + 6000, action.c_sendMessageUploadVideoAction().vprogress.v)); break; case mtpc_sendMessageRecordAudioAction: history->sendActions.insert(user, SendAction(SendActionRecordVoice, ms + 6000)); break; case mtpc_sendMessageUploadAudioAction: history->sendActions.insert(user, SendAction(SendActionUploadVoice, ms + 6000, action.c_sendMessageUploadAudioAction().vprogress.v)); break; case mtpc_sendMessageUploadPhotoAction: history->sendActions.insert(user, SendAction(SendActionUploadPhoto, ms + 6000, action.c_sendMessageUploadPhotoAction().vprogress.v)); break; case mtpc_sendMessageUploadDocumentAction: history->sendActions.insert(user, SendAction(SendActionUploadFile, ms + 6000, action.c_sendMessageUploadDocumentAction().vprogress.v)); break; case mtpc_sendMessageGeoLocationAction: history->sendActions.insert(user, SendAction(SendActionChooseLocation, ms + 6000)); break; case mtpc_sendMessageChooseContactAction: history->sendActions.insert(user, SendAction(SendActionChooseContact, ms + 6000)); break; default: return; } user->madeAction(); TypingHistories::const_iterator i = typing.find(history); if (i == typing.cend()) { typing.insert(history, ms); history->typingDots = 0; _a_typings.start(); } history->updateTyping(ms, true); } void Histories::step_typings(uint64 ms, bool timer) { for (TypingHistories::iterator i = typing.begin(), e = typing.end(); i != e;) { i.key()->typingDots = (ms - i.value()) / 150; i.key()->updateTyping(ms); if (i.key()->typing.isEmpty() && i.key()->sendActions.isEmpty()) { i = typing.erase(i); } else { ++i; } } if (typing.isEmpty()) { _a_typings.stop(); } } void Histories::remove(const PeerId &peer) { Map::iterator i = map.find(peer); if (i != map.cend()) { typing.remove(i.value()); delete i.value(); map.erase(i); } } namespace { void checkForSwitchInlineButton(HistoryItem *item) { if (item->out() || !item->hasSwitchInlineButton()) { return; } if (UserData *user = item->history()->peer->asUser()) { if (!user->botInfo || !user->botInfo->inlineReturnPeerId) { return; } if (auto markup = item->Get()) { for_const (const auto &row, markup->rows) { for_const (const auto &button, row) { if (button.type == HistoryMessageReplyMarkup::Button::SwitchInline) { Notify::switchInlineBotButtonReceived(QString::fromUtf8(button.data)); return; } } } } } } } // namespace HistoryItem *Histories::addNewMessage(const MTPMessage &msg, NewMessageType type) { PeerId peer = peerFromMessage(msg); if (!peer) return nullptr; HistoryItem *result = findOrInsert(peer, 0, 0)->addNewMessage(msg, type); if (result && type == NewMessageUnread) { checkForSwitchInlineButton(result); } return result; } HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, bool detachExistingItem) { 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; } if (!msgId) return nullptr; HistoryItem *result = App::histItemById(channelId(), msgId); if (result) { if (!result->detached() && detachExistingItem) { result->detach(); } if (msg.type() == mtpc_message) { result->updateMedia(msg.c_message().has_media() ? (&msg.c_message().vmedia) : 0); if (applyServiceAction) { App::checkSavedGif(result); } } return result; } switch (msg.type()) { case mtpc_messageEmpty: result = HistoryService::create(this, msg.c_messageEmpty().vid.v, date(), lang(lng_message_empty)); break; case mtpc_message: { const auto &m(msg.c_message()); int badMedia = 0; // 1 - unsupported, 2 - empty if (m.has_media()) switch (m.vmedia.type()) { case mtpc_messageMediaEmpty: case mtpc_messageMediaContact: break; case mtpc_messageMediaGeo: switch (m.vmedia.c_messageMediaGeo().vgeo.type()) { case mtpc_geoPoint: break; case mtpc_geoPointEmpty: badMedia = 2; break; default: badMedia = 1; break; } break; case mtpc_messageMediaVenue: switch (m.vmedia.c_messageMediaVenue().vgeo.type()) { case mtpc_geoPoint: break; case mtpc_geoPointEmpty: badMedia = 2; break; default: badMedia = 1; break; } break; case mtpc_messageMediaPhoto: switch (m.vmedia.c_messageMediaPhoto().vphoto.type()) { case mtpc_photo: break; case mtpc_photoEmpty: badMedia = 2; break; default: badMedia = 1; break; } break; case mtpc_messageMediaDocument: switch (m.vmedia.c_messageMediaDocument().vdocument.type()) { case mtpc_document: break; case mtpc_documentEmpty: badMedia = 2; break; default: badMedia = 1; break; } break; case mtpc_messageMediaWebPage: switch (m.vmedia.c_messageMediaWebPage().vwebpage.type()) { case mtpc_webPage: case mtpc_webPageEmpty: case mtpc_webPagePending: break; default: badMedia = 1; break; } break; case mtpc_messageMediaUnsupported: default: badMedia = 1; break; } if (badMedia == 1) { QString text(lng_message_unsupported(lt_link, qsl("https://desktop.telegram.org"))); EntitiesInText entities = textParseEntities(text, _historyTextNoMonoOptions.flags); entities.push_front(EntityInText(EntityInTextItalic, 0, text.size())); result = HistoryMessage::create(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v, text, entities); } else if (badMedia) { result = HistoryService::create(this, m.vid.v, date(m.vdate), lang(lng_message_empty), m.vflags.v, m.has_from_id() ? m.vfrom_id.v : 0); } else { result = HistoryMessage::create(this, m); } } break; case mtpc_messageService: { const auto &d(msg.c_messageService()); result = HistoryService::create(this, d); if (applyServiceAction) { const auto &action(d.vaction); switch (d.vaction.type()) { case mtpc_messageActionChatAddUser: { const auto &d(action.c_messageActionChatAddUser()); if (peer->isMegagroup()) { const auto &v(d.vusers.c_vector().v); for (int32 i = 0, l = v.size(); i < l; ++i) { if (UserData *user = App::userLoaded(peerFromUser(v.at(i)))) { if (peer->asChannel()->mgInfo->lastParticipants.indexOf(user) < 0) { peer->asChannel()->mgInfo->lastParticipants.push_front(user); peer->asChannel()->mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsAdminsOutdated; } if (user->botInfo) { peer->asChannel()->mgInfo->bots.insert(user); if (peer->asChannel()->mgInfo->botStatus != 0 && peer->asChannel()->mgInfo->botStatus < 2) { peer->asChannel()->mgInfo->botStatus = 2; } } } } } } break; case mtpc_messageActionChatJoinedByLink: { const auto &d(action.c_messageActionChatJoinedByLink()); if (peer->isMegagroup()) { if (result->from()->isUser()) { if (peer->asChannel()->mgInfo->lastParticipants.indexOf(result->from()->asUser()) < 0) { peer->asChannel()->mgInfo->lastParticipants.push_front(result->from()->asUser()); } if (result->from()->asUser()->botInfo) { peer->asChannel()->mgInfo->bots.insert(result->from()->asUser()); if (peer->asChannel()->mgInfo->botStatus != 0 && peer->asChannel()->mgInfo->botStatus < 2) { peer->asChannel()->mgInfo->botStatus = 2; } } } } } break; case mtpc_messageActionChatDeletePhoto: { ChatData *chat = peer->asChat(); if (chat) chat->setPhoto(MTP_chatPhotoEmpty()); } break; case mtpc_messageActionChatDeleteUser: { const auto &d(action.c_messageActionChatDeleteUser()); PeerId uid = peerFromUser(d.vuser_id); if (lastKeyboardFrom == uid) { clearLastKeyboard(); if (App::main()) App::main()->updateBotKeyboard(this); } if (peer->isMegagroup()) { if (UserData *user = App::userLoaded(uid)) { int32 index = peer->asChannel()->mgInfo->lastParticipants.indexOf(user); if (index >= 0) { peer->asChannel()->mgInfo->lastParticipants.removeAt(index); } if (peer->asChannel()->count > 1) { --peer->asChannel()->count; } else { peer->asChannel()->mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated; peer->asChannel()->mgInfo->lastParticipantsCount = 0; } if (peer->asChannel()->mgInfo->lastAdmins.contains(user)) { peer->asChannel()->mgInfo->lastAdmins.remove(user); if (peer->asChannel()->adminsCount > 1) { --peer->asChannel()->adminsCount; } } peer->asChannel()->mgInfo->bots.remove(user); if (peer->asChannel()->mgInfo->bots.isEmpty() && peer->asChannel()->mgInfo->botStatus > 0) { peer->asChannel()->mgInfo->botStatus = -1; } } } } break; case mtpc_messageActionChatEditPhoto: { const auto &d(action.c_messageActionChatEditPhoto()); if (d.vphoto.type() == mtpc_photo) { const auto &sizes(d.vphoto.c_photo().vsizes.c_vector().v); if (!sizes.isEmpty()) { PhotoData *photo = App::feedPhoto(d.vphoto.c_photo()); if (photo) photo->peer = peer; const auto &smallSize(sizes.front()), &bigSize(sizes.back()); const MTPFileLocation *smallLoc = 0, *bigLoc = 0; switch (smallSize.type()) { case mtpc_photoSize: smallLoc = &smallSize.c_photoSize().vlocation; break; case mtpc_photoCachedSize: smallLoc = &smallSize.c_photoCachedSize().vlocation; break; } switch (bigSize.type()) { case mtpc_photoSize: bigLoc = &bigSize.c_photoSize().vlocation; break; case mtpc_photoCachedSize: bigLoc = &bigSize.c_photoCachedSize().vlocation; break; } if (smallLoc && bigLoc) { if (peer->isChat()) { peer->asChat()->setPhoto(MTP_chatPhoto(*smallLoc, *bigLoc), photo ? photo->id : 0); } else if (peer->isChannel()) { peer->asChannel()->setPhoto(MTP_chatPhoto(*smallLoc, *bigLoc), photo ? photo->id : 0); } peer->loadUserpic(); } } } } break; case mtpc_messageActionChatEditTitle: { const auto &d(action.c_messageActionChatEditTitle()); ChatData *chat = peer->asChat(); if (chat) chat->updateName(qs(d.vtitle), QString(), QString()); } break; case mtpc_messageActionChatMigrateTo: { peer->asChat()->flags |= MTPDchat::Flag::f_deactivated; //const auto &d(action.c_messageActionChatMigrateTo()); //PeerData *channel = App::channelLoaded(d.vchannel_id.v); } break; case mtpc_messageActionChannelMigrateFrom: { //const auto &d(action.c_messageActionChannelMigrateFrom()); //PeerData *chat = App::chatLoaded(d.vchat_id.v); } break; case mtpc_messageActionPinMessage: { if (d.has_reply_to_msg_id() && result && result->history()->peer->isMegagroup()) { result->history()->peer->asChannel()->mgInfo->pinnedMsgId = d.vreply_to_msg_id.v; if (App::main()) emit App::main()->peerUpdated(result->history()->peer); } } break; } } } break; } if (applyServiceAction) { App::checkSavedGif(result); } return result; } HistoryItem *History::createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *msg) { return HistoryMessage::create(this, id, flags, date, from, msg); } HistoryItem *History::createItemDocument(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, doc, caption, markup); } HistoryItem *History::createItemPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) { return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, photo, caption, markup); } HistoryItem *History::addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags, bool newMsg) { return addNewItem(HistoryService::create(this, msgId, date, text, flags), newMsg); } HistoryItem *History::addNewMessage(const MTPMessage &msg, NewMessageType type) { if (isChannel()) return asChannelHistory()->addNewChannelMessage(msg, type); if (type == NewMessageExisting) return addToHistory(msg); if (!loadedAtBottom() || peer->migrateTo()) { HistoryItem *item = addToHistory(msg); if (item) { setLastMessage(item); if (type == NewMessageUnread) { newItemAdded(item); } } return item; } return addNewToLastBlock(msg, type); } HistoryItem *History::addNewToLastBlock(const MTPMessage &msg, NewMessageType type) { bool applyServiceAction = (type == NewMessageUnread), detachExistingItem = (type != NewMessageLast); HistoryItem *item = createItem(msg, applyServiceAction, detachExistingItem); if (!item || !item->detached()) { return item; } return addNewItem(item, (type == NewMessageUnread)); } HistoryItem *History::addToHistory(const MTPMessage &msg) { return createItem(msg, false, false); } HistoryItem *History::addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *item) { return addNewItem(createItemForwarded(id, flags, date, from, item), true); } HistoryItem *History::addNewDocument(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { return addNewItem(createItemDocument(id, flags, viaBotId, replyTo, date, from, doc, caption, markup), true); } HistoryItem *History::addNewPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) { return addNewItem(createItemPhoto(id, flags, viaBotId, replyTo, date, from, photo, caption, markup), true); } bool History::addToOverview(MediaOverviewType type, MsgId msgId, AddToOverviewMethod method) { bool adding = false; switch (method) { case AddToOverviewNew: case AddToOverviewFront: adding = (overviewIds[type].constFind(msgId) == overviewIds[type].cend()); break; case AddToOverviewBack: adding = (overviewCountData[type] != 0); break; } if (!adding) return false; overviewIds[type].insert(msgId, NullType()); switch (method) { case AddToOverviewNew: case AddToOverviewBack: overview[type].push_back(msgId); break; case AddToOverviewFront: overview[type].push_front(msgId); break; } if (method == AddToOverviewNew) { if (overviewCountData[type] > 0) { ++overviewCountData[type]; } if (App::wnd()) App::wnd()->mediaOverviewUpdated(peer, type); } return true; } void History::eraseFromOverview(MediaOverviewType type, MsgId msgId) { if (overviewIds[type].isEmpty()) return; History::MediaOverviewIds::iterator i = overviewIds[type].find(msgId); if (i == overviewIds[type].cend()) return; overviewIds[type].erase(i); for (History::MediaOverview::iterator i = overview[type].begin(), e = overview[type].end(); i != e; ++i) { if ((*i) == msgId) { overview[type].erase(i); if (overviewCountData[type] > 0) { --overviewCountData[type]; } break; } } if (App::wnd()) App::wnd()->mediaOverviewUpdated(peer, type); } HistoryItem *History::addNewItem(HistoryItem *adding, bool newMsg) { t_assert(!isBuildingFrontBlock()); addItemToBlock(adding); setLastMessage(adding); if (newMsg) { newItemAdded(adding); } adding->addToOverview(AddToOverviewNew); if (adding->from()->id) { if (adding->from()->isUser()) { QList *lastAuthors = 0; if (peer->isChat()) { lastAuthors = &peer->asChat()->lastAuthors; } else if (peer->isMegagroup()) { lastAuthors = &peer->asChannel()->mgInfo->lastParticipants; if (adding->from()->asUser()->botInfo) { peer->asChannel()->mgInfo->bots.insert(adding->from()->asUser()); if (peer->asChannel()->mgInfo->botStatus != 0 && peer->asChannel()->mgInfo->botStatus < 2) { peer->asChannel()->mgInfo->botStatus = 2; } } } if (lastAuthors) { int prev = lastAuthors->indexOf(adding->from()->asUser()); if (prev > 0) { lastAuthors->removeAt(prev); } else if (prev < 0 && peer->isMegagroup()) { // nothing is outdated if just reordering peer->asChannel()->mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsAdminsOutdated; } if (prev) { lastAuthors->push_front(adding->from()->asUser()); } } } if (adding->definesReplyKeyboard()) { MTPDreplyKeyboardMarkup::Flags markupFlags = adding->replyKeyboardFlags(); if (!(markupFlags & MTPDreplyKeyboardMarkup::Flag::f_selective) || adding->mentionsMe()) { OrderedSet *markupSenders = 0; if (peer->isChat()) { markupSenders = &peer->asChat()->markupSenders; } else if (peer->isMegagroup()) { markupSenders = &peer->asChannel()->mgInfo->markupSenders; } if (markupSenders) { markupSenders->insert(adding->from()); } if (markupFlags & MTPDreplyKeyboardMarkup_ClientFlag::f_zero) { // zero markup means replyKeyboardHide if (lastKeyboardFrom == adding->from()->id || (!lastKeyboardInited && !peer->isChat() && !peer->isMegagroup() && !adding->out())) { clearLastKeyboard(); } } else { bool botNotInChat = false; if (peer->isChat()) { botNotInChat = adding->from()->isUser() && (!peer->canWrite() || !peer->asChat()->participants.isEmpty()) && !peer->asChat()->participants.contains(adding->from()->asUser()); } else if (peer->isMegagroup()) { botNotInChat = adding->from()->isUser() && (!peer->canWrite() || peer->asChannel()->mgInfo->botStatus != 0) && !peer->asChannel()->mgInfo->bots.contains(adding->from()->asUser()); } if (botNotInChat) { clearLastKeyboard(); } else { lastKeyboardInited = true; lastKeyboardId = adding->id; lastKeyboardFrom = adding->from()->id; lastKeyboardUsed = false; } } } } } return adding; } void History::unregTyping(UserData *from) { uint64 updateAtMs = 0; TypingUsers::iterator i = typing.find(from); if (i != typing.end()) { updateAtMs = getms(); i.value() = updateAtMs; } SendActionUsers::iterator j = sendActions.find(from); if (j != sendActions.end()) { if (!updateAtMs) updateAtMs = getms(); j.value().until = updateAtMs; } if (updateAtMs) { updateTyping(updateAtMs, true); } } void History::newItemAdded(HistoryItem *item) { App::checkImageCacheSize(); if (item->from() && item->from()->isUser()) { if (item->from() == item->author()) { unregTyping(item->from()->asUser()); } item->from()->asUser()->madeAction(); } if (item->out()) { if (unreadBar) unreadBar->destroyUnreadBar(); if (!item->unread()) { outboxRead(item); } } else if (item->unread()) { bool skip = false; if (!isChannel() || peer->asChannel()->amIn()) { notifies.push_back(item); App::main()->newUnreadMsg(this, item); } } else if (!item->isGroupMigrate() || !peer->isMegagroup()) { inboxRead(item); } } HistoryBlock *History::prepareBlockForAddingItem() { if (isBuildingFrontBlock()) { if (_buildingFrontBlock->block) { return _buildingFrontBlock->block; } HistoryBlock *result = _buildingFrontBlock->block = new HistoryBlock(this); if (_buildingFrontBlock->expectedItemsCount > 0) { result->items.reserve(_buildingFrontBlock->expectedItemsCount + 1); } result->setIndexInHistory(0); blocks.push_front(result); for (int i = 1, l = blocks.size(); i < l; ++i) { blocks.at(i)->setIndexInHistory(i); } return result; } bool addNewBlock = blocks.isEmpty() || (blocks.back()->items.size() >= MessagesPerPage); if (!addNewBlock) { return blocks.back(); } HistoryBlock *result = new HistoryBlock(this); result->setIndexInHistory(blocks.size()); blocks.push_back(result); result->items.reserve(MessagesPerPage); return result; }; void History::addItemToBlock(HistoryItem *item) { t_assert(item != nullptr); t_assert(item->detached()); HistoryBlock *block = prepareBlockForAddingItem(); item->attachToBlock(block, block->items.size()); block->items.push_back(item); item->previousItemChanged(); if (isBuildingFrontBlock() && _buildingFrontBlock->expectedItemsCount > 0) { --_buildingFrontBlock->expectedItemsCount; } } void History::addOlderSlice(const QVector &slice, const QVector *collapsed) { if (slice.isEmpty()) { oldLoaded = true; if (!collapsed || collapsed->isEmpty() || !isChannel()) { if (isChannel()) { asChannelHistory()->checkJoinedMessage(); asChannelHistory()->checkMaxReadMessageDate(); } return; } } const MTPMessageGroup *groupsBegin = (isChannel() && collapsed) ? collapsed->constData() : 0, *groupsIt = groupsBegin, *groupsEnd = (isChannel() && collapsed) ? (groupsBegin + collapsed->size()) : 0; startBuildingFrontBlock(slice.size() + (collapsed ? collapsed->size() : 0)); for (auto i = slice.cend(), e = slice.cbegin(); i != e;) { --i; HistoryItem *adding = createItem(*i, false, true); if (!adding) continue; for (; groupsIt != groupsEnd; ++groupsIt) { if (groupsIt->type() != mtpc_messageGroup) continue; const auto &group(groupsIt->c_messageGroup()); if (group.vmin_id.v >= adding->id) break; addMessageGroup(group); } addItemToBlock(adding); } for (; groupsIt != groupsEnd; ++groupsIt) { if (groupsIt->type() != mtpc_messageGroup) continue; const auto &group(groupsIt->c_messageGroup()); addMessageGroup(group); } HistoryBlock *block = finishBuildingFrontBlock(); if (!block) { // If no items were added it means we've loaded everything old. oldLoaded = true; } else if (loadedAtBottom()) { // add photos to overview and authors to lastAuthors / lastParticipants bool channel = isChannel(); int32 mask = 0; QList *lastAuthors = 0; OrderedSet *markupSenders = 0; if (peer->isChat()) { lastAuthors = &peer->asChat()->lastAuthors; markupSenders = &peer->asChat()->markupSenders; } else if (peer->isMegagroup()) { lastAuthors = &peer->asChannel()->mgInfo->lastParticipants; markupSenders = &peer->asChannel()->mgInfo->markupSenders; } for (int32 i = block->items.size(); i > 0; --i) { HistoryItem *item = block->items[i - 1]; mask |= item->addToOverview(AddToOverviewFront); if (item->from()->id) { if (lastAuthors) { // chats if (item->from()->isUser()) { if (!lastAuthors->contains(item->from()->asUser())) { lastAuthors->push_back(item->from()->asUser()); if (peer->isMegagroup()) { peer->asChannel()->mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsAdminsOutdated; } } } } } if (item->author()->id) { if (markupSenders) { // chats with bots if (!lastKeyboardInited && item->definesReplyKeyboard() && !item->out()) { MTPDreplyKeyboardMarkup::Flags markupFlags = item->replyKeyboardFlags(); if (!(markupFlags & MTPDreplyKeyboardMarkup::Flag::f_selective) || item->mentionsMe()) { bool wasKeyboardHide = markupSenders->contains(item->author()); if (!wasKeyboardHide) { markupSenders->insert(item->author()); } if (!(markupFlags & MTPDreplyKeyboardMarkup_ClientFlag::f_zero)) { if (!lastKeyboardInited) { bool botNotInChat = false; if (peer->isChat()) { botNotInChat = (!peer->canWrite() || !peer->asChat()->participants.isEmpty()) && item->author()->isUser() && !peer->asChat()->participants.contains(item->author()->asUser()); } else if (peer->isMegagroup()) { botNotInChat = (!peer->canWrite() || peer->asChannel()->mgInfo->botStatus != 0) && item->author()->isUser() && !peer->asChannel()->mgInfo->bots.contains(item->author()->asUser()); } if (wasKeyboardHide || botNotInChat) { clearLastKeyboard(); } else { lastKeyboardInited = true; lastKeyboardId = item->id; lastKeyboardFrom = item->author()->id; lastKeyboardUsed = false; } } } } } } else if (!lastKeyboardInited && item->definesReplyKeyboard() && !item->out()) { // conversations with bots MTPDreplyKeyboardMarkup::Flags markupFlags = item->replyKeyboardFlags(); if (!(markupFlags & MTPDreplyKeyboardMarkup::Flag::f_selective) || item->mentionsMe()) { if (markupFlags & MTPDreplyKeyboardMarkup_ClientFlag::f_zero) { clearLastKeyboard(); } else { lastKeyboardInited = true; lastKeyboardId = item->id; lastKeyboardFrom = item->author()->id; lastKeyboardUsed = false; } } } } } for (int32 t = 0; t < OverviewCount; ++t) { if ((mask & (1 << t)) && App::wnd()) App::wnd()->mediaOverviewUpdated(peer, MediaOverviewType(t)); } } if (isChannel()) { asChannelHistory()->checkJoinedMessage(); asChannelHistory()->checkMaxReadMessageDate(); } checkLastMsg(); } void History::addNewerSlice(const QVector &slice, const QVector *collapsed) { bool wasEmpty = isEmpty(), wasLoadedAtBottom = loadedAtBottom(); if (slice.isEmpty()) { newLoaded = true; if (!lastMsg) { setLastMessage(lastImportantMessage()); } } t_assert(!isBuildingFrontBlock()); if (!slice.isEmpty() || (isChannel() && collapsed && !collapsed->isEmpty())) { const MTPMessageGroup *groupsBegin = (isChannel() && collapsed) ? collapsed->constData() : 0, *groupsIt = groupsBegin, *groupsEnd = (isChannel() && collapsed) ? (groupsBegin + collapsed->size()) : 0; bool atLeastOneAdded = false; for (auto i = slice.cend(), e = slice.cbegin(); i != e;) { --i; HistoryItem *adding = createItem(*i, false, true); if (!adding) continue; for (; groupsIt != groupsEnd; ++groupsIt) { if (groupsIt->type() != mtpc_messageGroup) continue; const auto &group(groupsIt->c_messageGroup()); if (group.vmin_id.v >= adding->id) break; addMessageGroup(group); } addItemToBlock(adding); atLeastOneAdded = true; } for (; groupsIt != groupsEnd; ++groupsIt) { if (groupsIt->type() != mtpc_messageGroup) continue; const auto &group(groupsIt->c_messageGroup()); addMessageGroup(group); } if (!atLeastOneAdded) { newLoaded = true; setLastMessage(lastImportantMessage()); } } if (!wasLoadedAtBottom && loadedAtBottom()) { // add all loaded photos to overview int32 mask = 0; for (int32 i = 0; i < OverviewCount; ++i) { if (overviewCountData[i] == 0) continue; // all loaded if (!overview[i].isEmpty() || !overviewIds[i].isEmpty()) { overview[i].clear(); overviewIds[i].clear(); mask |= (1 << i); } } for_const (HistoryBlock *block, blocks) { for_const (HistoryItem *item, block->items) { mask |= item->addToOverview(AddToOverviewBack); } } for (int32 t = 0; t < OverviewCount; ++t) { if ((mask & (1 << t)) && App::wnd()) App::wnd()->mediaOverviewUpdated(peer, MediaOverviewType(t)); } } if (isChannel()) asChannelHistory()->checkJoinedMessage(); checkLastMsg(); } void History::checkLastMsg() { if (lastMsg) { if (!newLoaded && !lastMsg->detached() && (!isChannel() || asChannelHistory()->onlyImportant())) { newLoaded = true; } } else if (newLoaded) { setLastMessage(lastImportantMessage()); } } int History::countUnread(MsgId upTo) { int result = 0; for (auto i = blocks.cend(), e = blocks.cbegin(); i != e;) { --i; for (auto j = (*i)->items.cend(), en = (*i)->items.cbegin(); j != en;) { --j; if ((*j)->id > 0 && (*j)->id <= upTo) { break; } else if (!(*j)->out() && (*j)->unread() && (*j)->id > upTo) { ++result; } } } return result; } void History::updateShowFrom() { if (showFrom) return; for (auto i = blocks.cend(); i != blocks.cbegin();) { --i; for (auto j = (*i)->items.cend(); j != (*i)->items.cbegin();) { --j; if ((*j)->type() == HistoryItemMsg && (*j)->id > 0 && (!(*j)->out() || !showFrom)) { if ((*j)->id >= inboxReadBefore) { showFrom = *j; } else { return; } } } } } MsgId History::inboxRead(MsgId upTo) { if (upTo < 0) return upTo; if (unreadCount()) { if (upTo && loadedAtBottom()) App::main()->historyToDown(this); setUnreadCount(upTo ? countUnread(upTo) : 0); } if (!upTo) upTo = msgIdForRead(); inboxReadBefore = qMax(inboxReadBefore, upTo + 1); updateChatListEntry(); if (peer->migrateTo()) { if (History *h = App::historyLoaded(peer->migrateTo()->id)) { h->updateChatListEntry(); } } showFrom = 0; App::wnd()->notifyClear(this); clearNotifications(); return upTo; } MsgId History::inboxRead(HistoryItem *wasRead) { return inboxRead(wasRead ? wasRead->id : 0); } MsgId History::outboxRead(int32 upTo) { if (upTo < 0) return upTo; if (!upTo) upTo = msgIdForRead(); if (outboxReadBefore < upTo + 1) outboxReadBefore = upTo + 1; return upTo; } MsgId History::outboxRead(HistoryItem *wasRead) { return outboxRead(wasRead ? wasRead->id : 0); } HistoryItem *History::lastImportantMessage() const { if (isEmpty()) { return nullptr; } bool importantOnly = isChannel() && !isMegagroup(); for (int blockIndex = blocks.size(); blockIndex > 0;) { HistoryBlock *block = blocks.at(--blockIndex); for (int itemIndex = block->items.size(); itemIndex > 0;) { HistoryItem *item = block->items.at(--itemIndex); if (importantOnly ? item->isImportant() : (item->type() == HistoryItemMsg)) { return item; } } } return nullptr; } void History::setUnreadCount(int newUnreadCount) { if (_unreadCount != newUnreadCount) { if (newUnreadCount == 1) { if (loadedAtBottom()) showFrom = lastImportantMessage(); inboxReadBefore = qMax(inboxReadBefore, msgIdForRead()); } else if (!newUnreadCount) { showFrom = nullptr; inboxReadBefore = qMax(inboxReadBefore, msgIdForRead() + 1); } if (inChatList(Dialogs::Mode::All)) { App::histories().unreadIncrement(newUnreadCount - _unreadCount, mute()); if (!mute() || cIncludeMuted()) { Notify::unreadCounterUpdated(); } } _unreadCount = newUnreadCount; if (unreadBar) { int32 count = _unreadCount; if (peer->migrateTo()) { if (History *h = App::historyLoaded(peer->migrateTo()->id)) { count += h->unreadCount(); } } if (count > 0) { unreadBar->setUnreadBarCount(count); } else { unreadBar->setUnreadBarFreezed(); } } } } void History::setMute(bool newMute) { if (_mute != newMute) { _mute = newMute; if (inChatList(Dialogs::Mode::All)) { if (_unreadCount) { App::histories().unreadMuteChanged(_unreadCount, newMute); Notify::unreadCounterUpdated(); } Notify::historyMuteUpdated(this); } updateChatListEntry(); } } void History::getNextShowFrom(HistoryBlock *block, int i) { if (i >= 0) { int l = block->items.size(); for (++i; i < l; ++i) { if (block->items.at(i)->type() == HistoryItemMsg) { showFrom = block->items.at(i); return; } } } for (int j = block->indexInHistory() + 1, s = blocks.size(); j < s; ++j) { block = blocks.at(j); for_const (HistoryItem *item, block->items) { if (item->type() == HistoryItemMsg) { showFrom = item; return; } } } showFrom = nullptr; } void History::countScrollState(int top) { countScrollTopItem(top); if (scrollTopItem) { scrollTopOffset = (top - scrollTopItem->block()->y - scrollTopItem->y); } } void History::countScrollTopItem(int top) { if (isEmpty()) { forgetScrollState(); return; } int itemIndex = 0, blockIndex = 0, itemTop = 0; if (scrollTopItem && !scrollTopItem->detached()) { itemIndex = scrollTopItem->indexInBlock(); blockIndex = scrollTopItem->block()->indexInHistory(); itemTop = blocks.at(blockIndex)->y + scrollTopItem->y; } if (itemTop > top) { // go backward through history while we don't find an item that starts above do { HistoryBlock *block = blocks.at(blockIndex); for (--itemIndex; itemIndex >= 0; --itemIndex) { HistoryItem *item = block->items.at(itemIndex); itemTop = block->y + item->y; if (itemTop <= top) { scrollTopItem = item; return; } } if (--blockIndex >= 0) { itemIndex = blocks.at(blockIndex)->items.size(); } else { break; } } while (true); scrollTopItem = blocks.front()->items.front(); } else { // go forward through history while we don't find the last item that starts above for (int blocksCount = blocks.size(); blockIndex < blocksCount; ++blockIndex) { HistoryBlock *block = blocks.at(blockIndex); for (int itemsCount = block->items.size(); itemIndex < itemsCount; ++itemIndex) { HistoryItem *item = block->items.at(itemIndex); itemTop = block->y + item->y; if (itemTop > top) { t_assert(itemIndex > 0 || blockIndex > 0); if (itemIndex > 0) { scrollTopItem = block->items.at(itemIndex - 1); } else { scrollTopItem = blocks.at(blockIndex - 1)->items.back(); } return; } } itemIndex = 0; } scrollTopItem = blocks.back()->items.back(); } } void History::getNextScrollTopItem(HistoryBlock *block, int32 i) { ++i; if (i > 0 && i < block->items.size()) { scrollTopItem = block->items.at(i); return; } int j = block->indexInHistory() + 1; if (j > 0 && j < blocks.size()) { scrollTopItem = blocks.at(j)->items.front(); return; } scrollTopItem = nullptr; } void History::addUnreadBar() { if (unreadBar || !showFrom || showFrom->detached() || !unreadCount()) return; int32 count = unreadCount(); if (peer->migrateTo()) { if (History *h = App::historyLoaded(peer->migrateTo()->id)) { count += h->unreadCount(); } } showFrom->setUnreadBarCount(count); unreadBar = showFrom; } void History::destroyUnreadBar() { if (unreadBar) { unreadBar->destroyUnreadBar(); } } HistoryItem *History::addNewInTheMiddle(HistoryItem *newItem, int32 blockIndex, int32 itemIndex) { t_assert(blockIndex >= 0); t_assert(blockIndex < blocks.size()); t_assert(itemIndex >= 0); t_assert(itemIndex <= blocks.at(blockIndex)->items.size()); HistoryBlock *block = blocks.at(blockIndex); newItem->attachToBlock(block, itemIndex); block->items.insert(itemIndex, newItem); newItem->previousItemChanged(); for (int i = itemIndex + 1, l = block->items.size(); i < l; ++i) { block->items.at(i)->setIndexInBlock(i); } if (itemIndex + 1 < block->items.size()) { block->items.at(itemIndex + 1)->previousItemChanged(); } return newItem; } template void History::addMessageGroup(CreateGroup create, UniteGroup unite) { HistoryItem *previous = nullptr; if (isBuildingFrontBlock()) { if (_buildingFrontBlock->block) { previous = _buildingFrontBlock->block->items.back(); } } else { if (!blocks.isEmpty()) { previous = blocks.back()->items.back(); } } if (previous && previous->type() == HistoryItemGroup) { unite(static_cast(previous)); } else { addItemToBlock(create(previous)); } } void History::addMessageGroup(const MTPDmessageGroup &group) { addMessageGroup([&group, this](HistoryItem *previous) -> HistoryGroup* { // create(..) return HistoryGroup::create(this, group, previous ? previous->date : date(group.vdate)); }, [&group](HistoryGroup *existing) { // unite(..) existing->uniteWith(group.vmin_id.v, group.vmax_id.v, group.vcount.v); }); } void History::startBuildingFrontBlock(int expectedItemsCount) { t_assert(!isBuildingFrontBlock()); t_assert(expectedItemsCount > 0); _buildingFrontBlock.reset(new BuildingBlock()); _buildingFrontBlock->expectedItemsCount = expectedItemsCount; } HistoryBlock *History::finishBuildingFrontBlock() { t_assert(isBuildingFrontBlock()); // Some checks if there was some message history already HistoryBlock *block = _buildingFrontBlock->block; if (block && blocks.size() > 1) { HistoryItem *last = block->items.back(); // ... item, item, item, last ], [ first, item, item ... HistoryItem *first = blocks.at(1)->items.front(); // we've added a new front block, so previous item for // the old first item of a first block was changed first->previousItemChanged(); // we've added a new front block, now we check if both // last message of the first block and first message of // the second block are groups, if they are - unite them if (first->type() == HistoryItemGroup && last->type() == HistoryItemGroup) { static_cast(first)->uniteWith(static_cast(last)); last->destroy(); // last->destroy() could've destroyed this new block // so we can't rely on this pointer any more block = _buildingFrontBlock->block; } } _buildingFrontBlock = nullptr; return block; } void History::clearNotifications() { notifies.clear(); } bool History::loadedAtBottom() const { return newLoaded; } bool History::loadedAtTop() const { return oldLoaded; } bool History::isReadyFor(MsgId msgId, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop) { if (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) { // old group history return App::history(peer->migrateFrom()->id)->isReadyFor(-msgId, fixInScrollMsgId, fixInScrollMsgTop); } if (msgId != ShowAtTheEndMsgId && msgId != ShowAtUnreadMsgId && isChannel()) { return asChannelHistory()->isSwitchReadyFor(msgId, fixInScrollMsgId, fixInScrollMsgTop); } fixInScrollMsgId = 0; fixInScrollMsgTop = 0; if (msgId == ShowAtTheEndMsgId) { return loadedAtBottom(); } if (msgId == ShowAtUnreadMsgId) { if (peer->migrateFrom()) { // old group history if (History *h = App::historyLoaded(peer->migrateFrom()->id)) { if (h->unreadCount()) { return h->isReadyFor(msgId, fixInScrollMsgId, fixInScrollMsgTop); } } } if (unreadCount()) { if (!isEmpty()) { return (loadedAtTop() || minMsgId() <= inboxReadBefore) && (loadedAtBottom() || maxMsgId() >= inboxReadBefore); } return false; } return loadedAtBottom(); } HistoryItem *item = App::histItemById(channelId(), msgId); return item && (item->history() == this) && !item->detached(); } void History::getReadyFor(MsgId msgId, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop) { if (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) { History *h = App::history(peer->migrateFrom()->id); h->getReadyFor(-msgId, fixInScrollMsgId, fixInScrollMsgTop); if (h->isEmpty()) { clear(true); } return; } if (msgId != ShowAtTheEndMsgId && msgId != ShowAtUnreadMsgId && isChannel()) { return asChannelHistory()->getSwitchReadyFor(msgId, fixInScrollMsgId, fixInScrollMsgTop); } if (msgId == ShowAtUnreadMsgId && peer->migrateFrom()) { if (History *h = App::historyLoaded(peer->migrateFrom()->id)) { if (h->unreadCount()) { clear(true); h->getReadyFor(msgId, fixInScrollMsgId, fixInScrollMsgTop); return; } } } if (!isReadyFor(msgId, fixInScrollMsgId, fixInScrollMsgTop)) { clear(true); if (msgId == ShowAtTheEndMsgId) { newLoaded = true; } } } void History::setNotLoadedAtBottom() { newLoaded = false; } namespace { uint32 _dialogsPosToTopShift = 0x80000000UL; } inline uint64 dialogPosFromDate(const QDateTime &date) { return (uint64(date.toTime_t()) << 32) | (++_dialogsPosToTopShift); } void History::setLastMessage(HistoryItem *msg) { if (msg) { if (!lastMsg) Local::removeSavedPeer(peer); lastMsg = msg; setChatsListDate(msg->date); } else { lastMsg = 0; } updateChatListEntry(); } void History::setChatsListDate(const QDateTime &date) { bool updateDialog = (App::main() && (!peer->isChannel() || peer->asChannel()->amIn() || inChatList(Dialogs::Mode::All))); if (peer->migrateTo() && !inChatList(Dialogs::Mode::All)) { updateDialog = false; } if (!lastMsgDate.isNull() && lastMsgDate >= date) { if (!updateDialog || !inChatList(Dialogs::Mode::All)) { return; } } lastMsgDate = date; _sortKeyInChatList = dialogPosFromDate(lastMsgDate); if (updateDialog) { App::main()->createDialog(this); } } void History::fixLastMessage(bool wasAtBottom) { setLastMessage(wasAtBottom ? lastImportantMessage() : 0); } MsgId History::minMsgId() const { for_const (const HistoryBlock *block, blocks) { for_const (const HistoryItem *item, block->items) { if (item->id > 0) { return item->id; } } } return 0; } MsgId History::maxMsgId() const { for (auto i = blocks.cend(), e = blocks.cbegin(); i != e;) { --i; for (auto j = (*i)->items.cend(), en = (*i)->items.cbegin(); j != en;) { --j; if ((*j)->id > 0) { return (*j)->id; } } } return 0; } MsgId History::msgIdForRead() const { MsgId result = (lastMsg && lastMsg->id > 0) ? lastMsg->id : 0; if (loadedAtBottom()) result = qMax(result, maxMsgId()); return result; } int History::resizeGetHeight(int newWidth) { bool resizeAllItems = (_flags & Flag::f_pending_resize) || (width != newWidth); if (!resizeAllItems && !hasPendingResizedItems()) { return height; } _flags &= ~(Flag::f_pending_resize | Flag::f_has_pending_resized_items); width = newWidth; int y = 0; for_const (HistoryBlock *block, blocks) { block->y = y; y += block->resizeGetHeight(newWidth, resizeAllItems); } height = y; return height; } ChannelHistory *History::asChannelHistory() { return isChannel() ? static_cast(this) : 0; } const ChannelHistory *History::asChannelHistory() const { return isChannel() ? static_cast(this) : 0; } void History::clear(bool leaveItems) { if (unreadBar) { unreadBar = nullptr; } if (showFrom) { showFrom = nullptr; } if (scrollTopItem) { forgetScrollState(); } if (!leaveItems) { setLastMessage(nullptr); notifies.clear(); auto &pending = Global::RefPendingRepaintItems(); for (auto i = pending.begin(); i != pending.end();) { if ((*i)->history() == this) { i = pending.erase(i); } else { ++i; } } } for (int32 i = 0; i < OverviewCount; ++i) { if (!overview[i].isEmpty() || !overviewIds[i].isEmpty()) { if (leaveItems) { if (overviewCountData[i] == 0) { overviewCountData[i] = overview[i].size(); } } else { overviewCountData[i] = -1; // not loaded yet } overview[i].clear(); overviewIds[i].clear(); if (App::wnd() && !App::quitting()) App::wnd()->mediaOverviewUpdated(peer, MediaOverviewType(i)); } } clearBlocks(leaveItems); if (leaveItems) { lastKeyboardInited = false; } else { setUnreadCount(0); if (peer->isMegagroup()) { peer->asChannel()->mgInfo->pinnedMsgId = 0; } clearLastKeyboard(); } setPendingResize(); newLoaded = oldLoaded = false; forgetScrollState(); if (peer->isChat()) { peer->asChat()->lastAuthors.clear(); peer->asChat()->markupSenders.clear(); } else if (isChannel()) { asChannelHistory()->cleared(leaveItems); if (isMegagroup()) { peer->asChannel()->mgInfo->markupSenders.clear(); } } if (leaveItems && App::main()) App::main()->historyCleared(this); } void History::clearBlocks(bool leaveItems) { Blocks lst; std::swap(lst, blocks); for_const (HistoryBlock *block, lst) { if (leaveItems) { block->clear(true); } delete block; } } void History::clearOnDestroy() { clearBlocks(false); } History::PositionInChatListChange History::adjustByPosInChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed) { t_assert(indexed != nullptr); Dialogs::Row *lnk = mainChatListLink(list); int32 movedFrom = lnk->pos(); indexed->adjustByPos(chatListLinks(list)); int32 movedTo = lnk->pos(); return { movedFrom, movedTo }; } int History::posInChatList(Dialogs::Mode list) const { return mainChatListLink(list)->pos(); } Dialogs::Row *History::addToChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed) { t_assert(indexed != nullptr); if (!inChatList(list)) { chatListLinks(list) = indexed->addToEnd(this); if (list == Dialogs::Mode::All && unreadCount()) { App::histories().unreadIncrement(unreadCount(), mute()); Notify::unreadCounterUpdated(); } } return mainChatListLink(list); } void History::removeFromChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed) { t_assert(indexed != nullptr); if (inChatList(list)) { indexed->del(peer); chatListLinks(list).clear(); if (list == Dialogs::Mode::All && unreadCount()) { App::histories().unreadIncrement(-unreadCount(), mute()); Notify::unreadCounterUpdated(); } } } void History::removeChatListEntryByLetter(Dialogs::Mode list, QChar letter) { t_assert(letter != 0); if (inChatList(list)) { chatListLinks(list).remove(letter); } } void History::addChatListEntryByLetter(Dialogs::Mode list, QChar letter, Dialogs::Row *row) { t_assert(letter != 0); if (inChatList(list)) { chatListLinks(list).insert(letter, row); } } void History::updateChatListEntry() const { if (MainWidget *m = App::main()) { if (inChatList(Dialogs::Mode::All)) { m->dlgUpdated(Dialogs::Mode::All, mainChatListLink(Dialogs::Mode::All)); if (inChatList(Dialogs::Mode::Important)) { m->dlgUpdated(Dialogs::Mode::Important, mainChatListLink(Dialogs::Mode::Important)); } } } } void History::overviewSliceDone(int32 overviewIndex, const MTPmessages_Messages &result, bool onlyCounts) { const QVector *v = 0; switch (result.type()) { case mtpc_messages_messages: { const auto &d(result.c_messages_messages()); App::feedUsers(d.vusers); App::feedChats(d.vchats); v = &d.vmessages.c_vector().v; overviewCountData[overviewIndex] = 0; } break; case mtpc_messages_messagesSlice: { const auto &d(result.c_messages_messagesSlice()); App::feedUsers(d.vusers); App::feedChats(d.vchats); overviewCountData[overviewIndex] = d.vcount.v; v = &d.vmessages.c_vector().v; } break; case mtpc_messages_channelMessages: { const auto &d(result.c_messages_channelMessages()); if (peer->isChannel()) { peer->asChannel()->ptsReceived(d.vpts.v); } else { LOG(("API Error: received messages.channelMessages when no channel was passed! (History::overviewSliceDone, onlyCounts %1)").arg(Logs::b(onlyCounts))); } if (d.has_collapsed()) { // should not be returned LOG(("API Error: channels.getMessages and messages.getMessages should not return collapsed groups! (History::overviewSliceDone, onlyCounts %1)").arg(Logs::b(onlyCounts))); } App::feedUsers(d.vusers); App::feedChats(d.vchats); overviewCountData[overviewIndex] = d.vcount.v; v = &d.vmessages.c_vector().v; } break; default: return; } if (!onlyCounts && v->isEmpty()) { overviewCountData[overviewIndex] = 0; } else if (overviewCountData[overviewIndex] > 0) { for (History::MediaOverviewIds::const_iterator i = overviewIds[overviewIndex].cbegin(), e = overviewIds[overviewIndex].cend(); i != e; ++i) { if (i.key() < 0) { ++overviewCountData[overviewIndex]; } else { break; } } } for (QVector::const_iterator i = v->cbegin(), e = v->cend(); i != e; ++i) { HistoryItem *item = App::histories().addNewMessage(*i, NewMessageExisting); if (item && overviewIds[overviewIndex].constFind(item->id) == overviewIds[overviewIndex].cend()) { overviewIds[overviewIndex].insert(item->id, NullType()); overview[overviewIndex].push_front(item->id); } } } void History::changeMsgId(MsgId oldId, MsgId newId) { for (int32 i = 0; i < OverviewCount; ++i) { History::MediaOverviewIds::iterator j = overviewIds[i].find(oldId); if (j != overviewIds[i].cend()) { overviewIds[i].erase(j); int32 index = overview[i].indexOf(oldId); if (overviewIds[i].constFind(newId) == overviewIds[i].cend()) { overviewIds[i].insert(newId, NullType()); if (index >= 0) { overview[i][index] = newId; } else { overview[i].push_back(newId); } } else if (index >= 0) { overview[i].removeAt(index); } } } } void History::removeBlock(HistoryBlock *block) { t_assert(block->items.isEmpty()); if (_buildingFrontBlock && block == _buildingFrontBlock->block) { _buildingFrontBlock->block = nullptr; } int index = block->indexInHistory(); blocks.removeAt(index); for (int i = index, l = blocks.size(); i < l; ++i) { blocks.at(i)->setIndexInHistory(i); } if (index < blocks.size()) { blocks.at(index)->items.front()->previousItemChanged(); } } History::~History() { clearOnDestroy(); } int HistoryBlock::resizeGetHeight(int newWidth, bool resizeAllItems) { int y = 0; for_const (HistoryItem *item, items) { item->y = y; if (resizeAllItems || item->pendingResize()) { y += item->resizeGetHeight(newWidth); } else { y += item->height(); } } height = y; return height; } void HistoryBlock::clear(bool leaveItems) { Items lst; std::swap(lst, items); if (leaveItems) { for_const (HistoryItem *item, lst) { item->detachFast(); } } else { for_const (HistoryItem *item, lst) { delete item; } } } void HistoryBlock::removeItem(HistoryItem *item) { t_assert(item->block() == this); int itemIndex = item->indexInBlock(); if (history->showFrom == item) { history->getNextShowFrom(this, itemIndex); } if (history->unreadBar == item) { history->unreadBar = nullptr; } if (history->scrollTopItem == item) { history->getNextScrollTopItem(this, itemIndex); } int blockIndex = indexInHistory(); if (blockIndex >= 0) { // fix message groups if (item->isImportant()) { // unite message groups around this important message HistoryGroup *nextGroup = nullptr; HistoryGroup *prevGroup = nullptr; HistoryCollapse *nextCollapse = nullptr; HistoryItem *prevItem = nullptr; for (int nextBlock = blockIndex, nextIndex = qMin(items.size(), itemIndex + 1); nextBlock < history->blocks.size(); ++nextBlock) { HistoryBlock *block = history->blocks.at(nextBlock); for (; nextIndex < block->items.size(); ++nextIndex) { HistoryItem *item = block->items.at(nextIndex); if (item->type() == HistoryItemMsg) { break; } else if (item->type() == HistoryItemGroup) { nextGroup = static_cast(item); break; } else if (item->type() == HistoryItemCollapse) { nextCollapse = static_cast(item); break; } } if (nextIndex == block->items.size()) { nextIndex = 0; } else { break; } } for (int prevBlock = blockIndex + 1, prevIndex = qMax(1, itemIndex); prevBlock > 0;) { --prevBlock; HistoryBlock *block = history->blocks.at(prevBlock); if (!prevIndex) prevIndex = block->items.size(); for (; prevIndex > 0;) { --prevIndex; HistoryItem *item = block->items.at(prevIndex); if (item->type() == HistoryItemMsg || item->type() == HistoryItemCollapse) { prevItem = item; ++prevIndex; break; } else if (item->type() == HistoryItemGroup) { prevGroup = static_cast(item); ++prevIndex; break; } } if (prevIndex != 0) { break; } } if (nextGroup && prevGroup) { prevGroup->uniteWith(nextGroup); nextGroup->destroy(); } else if (nextCollapse && (!prevItem || !prevItem->isImportant())) { nextCollapse->destroy(); } } } // itemIndex/blockIndex can be invalid now, because of destroying previous items/blocks blockIndex = indexInHistory(); itemIndex = item->indexInBlock(); item->detachFast(); items.remove(itemIndex); for (int i = itemIndex, l = items.size(); i < l; ++i) { items.at(i)->setIndexInBlock(i); } if (items.isEmpty()) { history->removeBlock(this); } else if (itemIndex < items.size()) { items.at(itemIndex)->previousItemChanged(); } else if (blockIndex + 1 < history->blocks.size()) { history->blocks.at(blockIndex + 1)->items.front()->previousItemChanged(); } if (items.isEmpty()) { delete this; } } class ReplyMarkupClickHandler : public LeftButtonClickHandler { public: ReplyMarkupClickHandler(const HistoryItem *item, int row, int col) : _item(item), _row(row), _col(col) { } QString tooltip() const override { return _fullDisplayed ? QString() : buttonText(); } void setFullDisplayed(bool full) { _fullDisplayed = full; } // Finds the corresponding button in the items markup struct. // If the button is not found it returns nullptr. // Note: it is possible that we will point to the different button // than the one was used when constructing the handler, but not a big deal. const HistoryMessageReplyMarkup::Button *getButton() const { if (auto markup = _item->Get()) { if (_row < markup->rows.size()) { const HistoryMessageReplyMarkup::ButtonRow &row(markup->rows.at(_row)); if (_col < row.size()) { return &row.at(_col); } } } return nullptr; } protected: void onClickImpl() const override { App::activateBotCommand(_item, _row, _col); } private: const HistoryItem *_item = nullptr; int _row, _col; bool _fullDisplayed = true; // Returns the full text of the corresponding button. QString buttonText() const { if (auto button = getButton()) { return button->text; } return QString(); } }; ReplyKeyboard::ReplyKeyboard(const HistoryItem *item, StylePtr &&s) : _item(item) , _a_selected(animation(this, &ReplyKeyboard::step_selected)) , _st(std_::forward(s)) { if (auto markup = item->Get()) { _rows.reserve(markup->rows.size()); for (int i = 0, l = markup->rows.size(); i != l; ++i) { const HistoryMessageReplyMarkup::ButtonRow &row(markup->rows.at(i)); int s = row.size(); ButtonRow newRow(s, Button()); for (int j = 0; j != s; ++j) { Button &button(newRow[j]); QString str = row.at(j).text; button.type = row.at(j).type; button.link.reset(new ReplyMarkupClickHandler(item, i, j)); button.text.setText(_st->textFont(), textOneLine(str), _textPlainOptions); button.characters = str.isEmpty() ? 1 : str.size(); } _rows.push_back(newRow); } } } void ReplyKeyboard::resize(int width, int height) { _width = width; auto markup = _item->Get(); float64 y = 0, buttonHeight = _rows.isEmpty() ? _st->buttonHeight() : (float64(height + _st->buttonSkip()) / _rows.size()); for (ButtonRow &row : _rows) { int s = row.size(); int widthForText = _width - ((s - 1) * _st->buttonSkip()); int widthOfText = 0; for_const (const Button &button, row) { widthOfText += qMax(button.text.maxWidth(), 1); widthForText -= _st->minButtonWidth(button.type); } bool exact = (widthForText == widthOfText); float64 x = 0; for (Button &button : row) { int buttonw = qMax(button.text.maxWidth(), 1); float64 textw = exact ? buttonw : (widthForText / float64(s)); float64 minw = _st->minButtonWidth(button.type); float64 w = minw + textw; accumulate_max(w, 2 * float64(_st->buttonPadding())); int rectx = static_cast(std::floor(x)); int rectw = static_cast(std::floor(x + w)) - rectx; button.rect = QRect(rectx, qRound(y), rectw, qRound(buttonHeight - _st->buttonSkip())); if (rtl()) button.rect.setX(_width - button.rect.x() - button.rect.width()); x += w + _st->buttonSkip(); button.link->setFullDisplayed(textw >= buttonw); } y += buttonHeight; } } bool ReplyKeyboard::isEnoughSpace(int width, const style::botKeyboardButton &st) const { for_const (const auto &row, _rows) { int s = row.size(); int widthLeft = width - ((s - 1) * st.margin + s * 2 * st.padding); for_const (const auto &button, row) { widthLeft -= qMax(button.text.maxWidth(), 1); if (widthLeft < 0) { if (row.size() > 3) { return false; } else { break; } } } } return true; } void ReplyKeyboard::setStyle(StylePtr &&st) { _st = std_::move(st); } int ReplyKeyboard::naturalWidth() const { auto result = 0; for_const (const auto &row, _rows) { auto rowMaxButtonWidth = 0; for_const (const auto &button, row) { accumulate_max(rowMaxButtonWidth, qMax(button.text.maxWidth(), 1) + _st->minButtonWidth(button.type)); } auto rowSize = row.size(); accumulate_max(result, rowSize * rowMaxButtonWidth + (rowSize - 1) * _st->buttonSkip()); } return result; } int ReplyKeyboard::naturalHeight() const { return (_rows.size() - 1) * _st->buttonSkip() + _rows.size() * _st->buttonHeight(); } void ReplyKeyboard::paint(Painter &p, const QRect &clip) const { t_assert(_st != nullptr); t_assert(_width > 0); _st->startPaint(p); for_const (const ButtonRow &row, _rows) { for_const (const Button &button, row) { QRect rect(button.rect); if (rect.y() >= clip.y() + clip.height()) return; if (rect.y() + rect.height() < clip.y()) continue; // just ignore the buttons that didn't layout well if (rect.x() + rect.width() > _width) break; _st->paintButton(p, button); } } } ClickHandlerPtr ReplyKeyboard::getState(int x, int y) const { t_assert(_width > 0); for_const (const ButtonRow &row, _rows) { for_const (const Button &button, row) { QRect rect(button.rect); // just ignore the buttons that didn't layout well if (rect.x() + rect.width() > _width) break; if (rect.contains(x, y)) { return button.link; } } } return ClickHandlerPtr(); } void ReplyKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (!p) return; bool startAnimation = false; for (int i = 0, rows = _rows.size(); i != rows; ++i) { const ButtonRow &row(_rows.at(i)); for (int j = 0, cols = row.size(); j != cols; ++j) { if (row.at(j).link == p) { bool startAnimation = _animations.isEmpty(); int indexForAnimation = i * MatrixRowShift + j + 1; if (!active) { indexForAnimation = -indexForAnimation; } _animations.remove(-indexForAnimation); if (!_animations.contains(indexForAnimation)) { _animations.insert(indexForAnimation, getms()); } if (startAnimation && !_a_selected.animating()) { _a_selected.start(); } return; } } } } void ReplyKeyboard::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { _st->repaint(_item); } void ReplyKeyboard::step_selected(uint64 ms, bool timer) { for (Animations::iterator i = _animations.begin(); i != _animations.end();) { int index = qAbs(i.key()) - 1, row = (index / MatrixRowShift), col = index % MatrixRowShift; float64 dt = float64(ms - i.value()) / st::botKbDuration; if (dt >= 1) { _rows[row][col].howMuchOver = (i.key() > 0) ? 1 : 0; i = _animations.erase(i); } else { _rows[row][col].howMuchOver = (i.key() > 0) ? dt : (1 - dt); ++i; } } if (timer) _st->repaint(_item); if (_animations.isEmpty()) { _a_selected.stop(); } } void ReplyKeyboard::clearSelection() { for (auto i = _animations.cbegin(), e = _animations.cend(); i != e; ++i) { int index = qAbs(i.key()) - 1, row = (index / MatrixRowShift), col = index % MatrixRowShift; _rows[row][col].howMuchOver = 0; } _animations.clear(); _a_selected.stop(); } void ReplyKeyboard::Style::paintButton(Painter &p, const ReplyKeyboard::Button &button) const { const QRect &rect = button.rect; bool pressed = ClickHandler::showAsPressed(button.link); paintButtonBg(p, rect, pressed, button.howMuchOver); paintButtonIcon(p, rect, button.type); if (button.type == HistoryMessageReplyMarkup::Button::Callback) { if (const HistoryMessageReplyMarkup::Button *data = button.link->getButton()) { if (data->requestId) { paintButtonLoading(p, rect); } } } int tx = rect.x(), tw = rect.width(); if (tw >= st::botKbFont->elidew + _st->padding * 2) { tx += _st->padding; tw -= _st->padding * 2; } else if (tw > st::botKbFont->elidew) { tx += (tw - st::botKbFont->elidew) / 2; tw = st::botKbFont->elidew; } int textTop = rect.y() + (pressed ? _st->downTextTop : _st->textTop); button.text.drawElided(p, tx, textTop + ((rect.height() - _st->height) / 2), tw, 1, style::al_top); } void HistoryMessageReplyMarkup::createFromButtonRows(const QVector &v) { if (v.isEmpty()) { rows.clear(); return; } rows.reserve(v.size()); for_const (const auto &row, v) { switch (row.type()) { case mtpc_keyboardButtonRow: { const auto &r(row.c_keyboardButtonRow()); const auto &b(r.vbuttons.c_vector().v); if (!b.isEmpty()) { ButtonRow buttonRow; buttonRow.reserve(b.size()); for_const (const auto &button, b) { switch (button.type()) { case mtpc_keyboardButton: { buttonRow.push_back({ Button::Default, qs(button.c_keyboardButton().vtext), QByteArray(), 0 }); } break; case mtpc_keyboardButtonCallback: { const auto &buttonData(button.c_keyboardButtonCallback()); buttonRow.push_back({ Button::Callback, qs(buttonData.vtext), qba(buttonData.vdata), 0 }); } break; case mtpc_keyboardButtonRequestGeoLocation: { buttonRow.push_back({ Button::RequestLocation, qs(button.c_keyboardButtonRequestGeoLocation().vtext), QByteArray(), 0 }); } break; case mtpc_keyboardButtonRequestPhone: { buttonRow.push_back({ Button::RequestPhone, qs(button.c_keyboardButtonRequestPhone().vtext), QByteArray(), 0 }); } break; case mtpc_keyboardButtonUrl: { const auto &buttonData(button.c_keyboardButtonUrl()); buttonRow.push_back({ Button::Url, qs(buttonData.vtext), qba(buttonData.vurl), 0 }); } break; case mtpc_keyboardButtonSwitchInline: { const auto &buttonData(button.c_keyboardButtonSwitchInline()); buttonRow.push_back({ Button::SwitchInline, qs(buttonData.vtext), qba(buttonData.vquery), 0 }); flags |= MTPDreplyKeyboardMarkup_ClientFlag::f_has_switch_inline_button; } break; } } if (!buttonRow.isEmpty()) rows.push_back(buttonRow); } } break; } } } void HistoryMessageReplyMarkup::create(const MTPReplyMarkup &markup) { flags = 0; rows.clear(); inlineKeyboard = nullptr; switch (markup.type()) { case mtpc_replyKeyboardMarkup: { const auto &d(markup.c_replyKeyboardMarkup()); flags = d.vflags.v; createFromButtonRows(d.vrows.c_vector().v); } break; case mtpc_replyInlineMarkup: { const auto &d(markup.c_replyInlineMarkup()); flags = MTPDreplyKeyboardMarkup::Flags(0) | MTPDreplyKeyboardMarkup_ClientFlag::f_inline; createFromButtonRows(d.vrows.c_vector().v); } break; case mtpc_replyKeyboardHide: { const auto &d(markup.c_replyKeyboardHide()); flags = mtpCastFlags(d.vflags) | MTPDreplyKeyboardMarkup_ClientFlag::f_zero; } break; case mtpc_replyKeyboardForceReply: { const auto &d(markup.c_replyKeyboardForceReply()); flags = mtpCastFlags(d.vflags) | MTPDreplyKeyboardMarkup_ClientFlag::f_force_reply; } break; } } void HistoryDependentItemCallback::call(ChannelData *channel, MsgId msgId) const { if (HistoryItem *item = App::histItemById(_dependent)) { item->updateDependencyItem(); } } void HistoryMessageUnreadBar::init(int count) { if (_freezed) return; _text = lng_unread_bar(lt_count, count); _width = st::semiboldFont->width(_text); } int HistoryMessageUnreadBar::height() { return st::unreadBarHeight + st::unreadBarMargin; } int HistoryMessageUnreadBar::marginTop() { return st::lineWidth + st::unreadBarMargin; } void HistoryMessageUnreadBar::paint(Painter &p, int y, int w) const { p.fillRect(0, y + marginTop(), w, height() - marginTop() - st::lineWidth, st::unreadBarBG); p.fillRect(0, y + height() - st::lineWidth, w, st::lineWidth, st::unreadBarBorder); p.setFont(st::unreadBarFont); p.setPen(st::unreadBarColor); int left = st::msgServiceMargin.left(); int maxwidth = w; if (Adaptive::Wide()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } w = maxwidth; p.drawText((w - _width) / 2, y + marginTop() + (st::unreadBarHeight - 2 * st::lineWidth - st::unreadBarFont->height) / 2 + st::unreadBarFont->ascent, _text); } void HistoryMessageDate::init(const QDateTime &date) { _text = langDayOfMonthFull(date.date()); _width = st::msgServiceFont->width(_text); } int HistoryMessageDate::height() const { return st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom() + st::msgServiceMargin.bottom(); } void HistoryMessageDate::paint(Painter &p, int y, int w) const { int left = st::msgServiceMargin.left(); int maxwidth = w; if (Adaptive::Wide()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); left += (w - _width - st::msgServicePadding.left() - st::msgServicePadding.right()) / 2; int height = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); App::roundRect(p, left, y + st::msgServiceMargin.top(), _width + st::msgServicePadding.left() + st::msgServicePadding.left(), height, App::msgServiceBg(), ServiceCorners); p.setFont(st::msgServiceFont); p.setPen(st::msgServiceColor); p.drawText(left + st::msgServicePadding.left(), y + st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->ascent, _text); } void HistoryMediaPtr::reset(HistoryMedia *p) { if (_p) { _p->detachFromParent(); delete _p; } _p = p; if (_p) { _p->attachToParent(); } } namespace internal { TextSelection unshiftSelection(TextSelection selection, const Text &byText) { if (selection == FullSelection) { return selection; } return ::unshiftSelection(selection, byText); } TextSelection shiftSelection(TextSelection selection, const Text &byText) { if (selection == FullSelection) { return selection; } return ::shiftSelection(selection, byText); } } // namespace internal HistoryItem::HistoryItem(History *history, MsgId msgId, MTPDmessage::Flags flags, QDateTime msgDate, int32 from) : HistoryElem() , y(0) , id(msgId) , date(msgDate) , _from(from ? App::user(from) : history->peer) , _history(history) , _flags(flags | MTPDmessage_ClientFlag::f_pending_init_dimensions | MTPDmessage_ClientFlag::f_pending_resize) , _authorNameVersion(author()->nameVersion) { } void HistoryItem::finishCreate() { App::historyRegItem(this); } void HistoryItem::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (auto markup = Get()) { if (markup->inlineKeyboard) { markup->inlineKeyboard->clickHandlerActiveChanged(p, active); } } App::hoveredLinkItem(active ? this : nullptr); Ui::repaintHistoryItem(this); } void HistoryItem::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { if (auto markup = Get()) { if (markup->inlineKeyboard) { markup->inlineKeyboard->clickHandlerPressedChanged(p, pressed); } } App::pressedLinkItem(pressed ? this : nullptr); Ui::repaintHistoryItem(this); } void HistoryItem::destroy() { // All this must be done for all items manually in History::clear(false)! bool wasAtBottom = history()->loadedAtBottom(); _history->removeNotification(this); detach(); if (history()->isChannel()) { history()->asChannelHistory()->messageDeleted(this); if (history()->peer->isMegagroup() && history()->peer->asChannel()->mgInfo->pinnedMsgId == id) { history()->peer->asChannel()->mgInfo->pinnedMsgId = 0; } } if (history()->lastMsg == this) { history()->fixLastMessage(wasAtBottom); } if (history()->lastKeyboardId == id) { history()->clearLastKeyboard(); if (App::main()) App::main()->updateBotKeyboard(history()); } if ((!out() || isPost()) && unread() && history()->unreadCount() > 0) { history()->setUnreadCount(history()->unreadCount() - 1); } Global::RefPendingRepaintItems().remove(this); delete this; } void HistoryItem::detach() { if (detached()) return; if (_history->isChannel()) { _history->asChannelHistory()->messageDetached(this); } _block->removeItem(this); App::historyItemDetached(this); _history->setPendingResize(); } void HistoryItem::detachFast() { _block = nullptr; _indexInBlock = -1; } void HistoryItem::previousItemChanged() { if (displayDate()) { if (!Has()) { AddComponents(HistoryMessageDate::Bit()); Get()->init(date); setPendingInitDimensions(); } } else if (Has()) { RemoveComponents(HistoryMessageDate::Bit()); setPendingInitDimensions(); } recountAttachToPrevious(); } void HistoryItem::recountAttachToPrevious() { bool attach = false; if (!isPost() && !Has() && !Has()) { if (HistoryItem *prev = previous()) { attach = !prev->isPost() && !prev->serviceMsg() && prev->from() == from() && qAbs(prev->date.secsTo(date)) < AttachMessageToPreviousSecondsDelta; } } if (attach && !(_flags & MTPDmessage_ClientFlag::f_attach_to_previous)) { _flags |= MTPDmessage_ClientFlag::f_attach_to_previous; setPendingInitDimensions(); } else if (!attach && (_flags & MTPDmessage_ClientFlag::f_attach_to_previous)) { _flags &= ~MTPDmessage_ClientFlag::f_attach_to_previous; setPendingInitDimensions(); } } void HistoryItem::setId(MsgId newId) { history()->changeMsgId(id, newId); id = newId; } bool HistoryItem::canEdit(const QDateTime &cur) const { ChannelData *channel = _history->peer->asChannel(); int32 s = date.secsTo(cur); if (!channel || id < 0 || date.secsTo(cur) >= Global::EditTimeLimit()) return false; if (const HistoryMessage *msg = toHistoryMessage()) { if (msg->Has() || msg->Has()) return false; if (HistoryMedia *media = msg->getMedia()) { HistoryMediaType t = media->type(); if (t != MediaTypePhoto && t != MediaTypeVideo && t != MediaTypeFile && t != MediaTypeGif && t != MediaTypeMusicFile && t != MediaTypeVoiceFile && t != MediaTypeWebPage) { return false; } } if (isPost()) { return (channel->amCreator() || (channel->amEditor() && out())); } return out(); } return false; } void HistoryItem::destroyUnreadBar() { if (Has()) { RemoveComponents(HistoryMessageUnreadBar::Bit()); setPendingInitDimensions(); if (_history->unreadBar == this) { _history->unreadBar = nullptr; } recountAttachToPrevious(); } } void HistoryItem::setUnreadBarCount(int count) { if (count > 0) { HistoryMessageUnreadBar *bar; if (!Has()) { AddComponents(HistoryMessageUnreadBar::Bit()); setPendingInitDimensions(); recountAttachToPrevious(); bar = Get(); } else { bar = Get(); if (bar->_freezed) { return; } Global::RefPendingRepaintItems().insert(this); } bar->init(count); } else { destroyUnreadBar(); } } void HistoryItem::setUnreadBarFreezed() { if (auto bar = Get()) { bar->_freezed = true; } } void HistoryItem::clipCallback(ClipReaderNotification notification) { HistoryMedia *media = getMedia(); if (!media) return; ClipReader *reader = media ? media->getClipReader() : 0; if (!reader) return; switch (notification) { case ClipReaderReinit: { bool stopped = false; if (reader->paused()) { if (MainWidget *m = App::main()) { if (!m->isItemVisible(this)) { // stop animation if it is not visible media->stopInline(); if (DocumentData *document = media->getDocument()) { // forget data from memory document->forget(); } stopped = true; } } } if (!stopped) { setPendingInitDimensions(); Notify::historyItemLayoutChanged(this); } } break; case ClipReaderRepaint: { if (!reader->currentDisplayed()) { Ui::repaintHistoryItem(this); } } break; } } HistoryItem::~HistoryItem() { App::historyUnregItem(this); if (id < 0 && App::uploader()) { App::uploader()->cancel(fullId()); } } RadialAnimation::RadialAnimation(AnimationCreator creator) : _firstStart(0) , _lastStart(0) , _lastTime(0) , _opacity(0) , a_arcEnd(0, 0) , a_arcStart(0, FullArcLength) , _animation(creator) { } void RadialAnimation::start(float64 prg) { _firstStart = _lastStart = _lastTime = getms(); int32 iprg = qRound(qMax(prg, 0.0001) * AlmostFullArcLength), iprgstrict = qRound(prg * AlmostFullArcLength); a_arcEnd = anim::ivalue(iprgstrict, iprg); _animation.start(); } void RadialAnimation::update(float64 prg, bool finished, uint64 ms) { int32 iprg = qRound(qMax(prg, 0.0001) * AlmostFullArcLength); if (iprg != a_arcEnd.to()) { a_arcEnd.start(iprg); _lastStart = _lastTime; } _lastTime = ms; float64 dt = float64(ms - _lastStart), fulldt = float64(ms - _firstStart); _opacity = qMin(fulldt / st::radialDuration, 1.); if (!finished) { a_arcEnd.update(1. - (st::radialDuration / (st::radialDuration + dt)), anim::linear); } else if (dt >= st::radialDuration) { a_arcEnd.update(1, anim::linear); stop(); } else { float64 r = dt / st::radialDuration; a_arcEnd.update(r, anim::linear); _opacity *= 1 - r; } float64 fromstart = fulldt / st::radialPeriod; a_arcStart.update(fromstart - std::floor(fromstart), anim::linear); } void RadialAnimation::stop() { _firstStart = _lastStart = _lastTime = 0; a_arcEnd = anim::ivalue(0, 0); _animation.stop(); } void RadialAnimation::step(uint64 ms) { _animation.step(ms); } void RadialAnimation::draw(Painter &p, const QRect &inner, int32 thickness, const style::color &color) { float64 o = p.opacity(); p.setOpacity(o * _opacity); QPen pen(color->p), was(p.pen()); pen.setWidth(thickness); p.setPen(pen); int32 len = MinArcLength + a_arcEnd.current(); int32 from = QuarterArcLength - a_arcStart.current() - len; if (rtl()) { from = QuarterArcLength - (from - QuarterArcLength) - len; if (from < 0) from += FullArcLength; } p.setRenderHint(QPainter::HighQualityAntialiasing); p.drawArc(inner, from, len); p.setRenderHint(QPainter::HighQualityAntialiasing, false); p.setPen(was); p.setOpacity(o); } namespace { int32 documentMaxStatusWidth(DocumentData *document) { int32 result = st::normalFont->width(formatDownloadText(document->size, document->size)); if (SongData *song = document->song()) { result = qMax(result, st::normalFont->width(formatPlayedText(song->duration, song->duration))); result = qMax(result, st::normalFont->width(formatDurationAndSizeText(song->duration, document->size))); } else if (VoiceData *voice = document->voice()) { result = qMax(result, st::normalFont->width(formatPlayedText(voice->duration, voice->duration))); result = qMax(result, st::normalFont->width(formatDurationAndSizeText(voice->duration, document->size))); } else if (document->isVideo()) { result = qMax(result, st::normalFont->width(formatDurationAndSizeText(document->duration(), document->size))); } else { result = qMax(result, st::normalFont->width(formatSizeText(document->size))); } return result; } int32 gifMaxStatusWidth(DocumentData *document) { int32 result = st::normalFont->width(formatDownloadText(document->size, document->size)); result = qMax(result, st::normalFont->width(formatGifAndSizeText(document->size))); return result; } QString captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) { if (selection != FullSelection) { return caption.original(selection, Text::ExpandLinksAll); } QString result; result.reserve(5 + attachType.size() + caption.length()); result.append(qstr("[ ")).append(attachType).append(qstr(" ]")); if (!caption.isEmpty()) { result.append(qstr("\n")).append(caption.original(AllTextSelection)); } return result; } } // namespace void HistoryFileMedia::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (p == _savel || p == _cancell) { if (active && !dataLoaded()) { ensureAnimation(); _animation->a_thumbOver.start(1); _animation->_a_thumbOver.start(); } else if (!active && _animation) { _animation->a_thumbOver.start(0); _animation->_a_thumbOver.start(); } } } void HistoryFileMedia::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { Ui::repaintHistoryItem(_parent); } void HistoryFileMedia::setLinks(ClickHandlerPtr &&openl, ClickHandlerPtr &&savel, ClickHandlerPtr &&cancell) { _openl = std_::move(openl); _savel = std_::move(savel); _cancell = std_::move(cancell); } void HistoryFileMedia::setStatusSize(int32 newSize, int32 fullSize, int32 duration, qint64 realDuration) const { _statusSize = newSize; if (_statusSize == FileStatusSizeReady) { _statusText = (duration >= 0) ? formatDurationAndSizeText(duration, fullSize) : (duration < -1 ? formatGifAndSizeText(fullSize) : formatSizeText(fullSize)); } else if (_statusSize == FileStatusSizeLoaded) { _statusText = (duration >= 0) ? formatDurationText(duration) : (duration < -1 ? qsl("GIF") : formatSizeText(fullSize)); } else if (_statusSize == FileStatusSizeFailed) { _statusText = lang(lng_attach_failed); } else if (_statusSize >= 0) { _statusText = formatDownloadText(_statusSize, fullSize); } else { _statusText = formatPlayedText(-_statusSize - 1, realDuration); } } void HistoryFileMedia::step_thumbOver(float64 ms, bool timer) { float64 dt = ms / st::msgFileOverDuration; if (dt >= 1) { _animation->a_thumbOver.finish(); _animation->_a_thumbOver.stop(); checkAnimationFinished(); } else if (!timer) { _animation->a_thumbOver.update(dt, anim::linear); } if (timer) { Ui::repaintHistoryItem(_parent); } } void HistoryFileMedia::step_radial(uint64 ms, bool timer) { if (timer) { Ui::repaintHistoryItem(_parent); } else { _animation->radial.update(dataProgress(), dataFinished(), ms); if (!_animation->radial.animating()) { checkAnimationFinished(); } } } void HistoryFileMedia::ensureAnimation() const { if (!_animation) { _animation = new AnimationData( animation(const_cast(this), &HistoryFileMedia::step_thumbOver), animation(const_cast(this), &HistoryFileMedia::step_radial)); } } void HistoryFileMedia::checkAnimationFinished() { if (_animation && !_animation->_a_thumbOver.animating() && !_animation->radial.animating()) { if (dataLoaded()) { delete _animation; _animation = 0; } } } HistoryFileMedia::~HistoryFileMedia() { deleteAndMark(_animation); } HistoryPhoto::HistoryPhoto(HistoryItem *parent, PhotoData *photo, const QString &caption) : HistoryFileMedia(parent) , _data(photo) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { setLinks(MakeShared(_data), MakeShared(_data), MakeShared(_data)); if (!caption.isEmpty()) { _caption.setText(st::msgFont, caption + _parent->skipBlock(), itemTextNoMonoOptions(_parent)); } init(); } HistoryPhoto::HistoryPhoto(HistoryItem *parent, PeerData *chat, const MTPDphoto &photo, int32 width) : HistoryFileMedia(parent) , _data(App::feedPhoto(photo)) { setLinks(MakeShared(_data, chat), MakeShared(_data, chat), MakeShared(_data, chat)); _width = width; init(); } HistoryPhoto::HistoryPhoto(HistoryItem *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)); init(); } void HistoryPhoto::init() { _data->thumb->load(); } void HistoryPhoto::initDimensions() { if (_caption.hasSkipBlock()) { _caption.setSkipBlock(_parent->skipBlockWidth(), _parent->skipBlockHeight()); } int32 tw = convertScale(_data->full->width()), th = convertScale(_data->full->height()); if (!tw || !th) { tw = th = 1; } if (tw > st::maxMediaSize) { th = (st::maxMediaSize * th) / tw; tw = st::maxMediaSize; } if (th > st::maxMediaSize) { tw = (st::maxMediaSize * tw) / th; th = st::maxMediaSize; } if (_parent->toHistoryMessage()) { bool bubble = _parent->hasBubble(); int32 minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); int32 maxActualWidth = qMax(tw, minWidth); _maxw = qMax(maxActualWidth, th); _minh = qMax(th, int32(st::minPhotoSize)); if (bubble) { maxActualWidth += st::mediaPadding.left() + st::mediaPadding.right(); _maxw += st::mediaPadding.left() + st::mediaPadding.right(); _minh += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { _minh += st::mediaCaptionSkip + _caption.countHeight(maxActualWidth - st::msgPadding.left() - st::msgPadding.right()) + st::msgPadding.bottom(); } } } else { _maxw = _minh = _width; } } int HistoryPhoto::resizeGetHeight(int width) { bool bubble = _parent->hasBubble(); int tw = convertScale(_data->full->width()), th = convertScale(_data->full->height()); if (tw > st::maxMediaSize) { th = (st::maxMediaSize * th) / tw; tw = st::maxMediaSize; } if (th > st::maxMediaSize) { tw = (st::maxMediaSize * tw) / th; th = st::maxMediaSize; } _pixw = qMin(width, _maxw); if (bubble) { _pixw -= st::mediaPadding.left() + st::mediaPadding.right(); } _pixh = th; if (tw > _pixw) { _pixh = (_pixw * _pixh / tw); } else { _pixw = tw; } if (_pixh > width) { _pixw = (_pixw * width) / _pixh; _pixh = width; } if (_pixw < 1) _pixw = 1; if (_pixh < 1) _pixh = 1; int minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); _width = qMax(_pixw, int16(minWidth)); _height = qMax(_pixh, int16(st::minPhotoSize)); if (bubble) { _width += st::mediaPadding.left() + st::mediaPadding.right(); _height += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { int captionw = _width - st::msgPadding.left() - st::msgPadding.right(); _height += st::mediaCaptionSkip + _caption.countHeight(captionw) + st::msgPadding.bottom(); } } return _height; } void HistoryPhoto::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; _data->automaticLoad(_parent); bool selected = (selection == FullSelection); bool loaded = _data->loaded(), displayLoading = _data->displayLoading(); bool notChild = (_parent->getMedia() == this); int skipx = 0, skipy = 0, width = _width, height = _height; bool bubble = _parent->hasBubble(); bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; int captionw = width - st::msgPadding.left() - st::msgPadding.right(); if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(_data->progress()); } } bool radial = isRadialAnimation(ms); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); width -= st::mediaPadding.left() + st::mediaPadding.right(); height -= skipy + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { height -= st::mediaCaptionSkip + _caption.countHeight(captionw) + st::msgPadding.bottom(); } } else { App::roundShadow(p, 0, 0, width, height, selected ? st::msgInShadowSelected : st::msgInShadow, selected ? InSelectedShadowCorners : InShadowCorners); } QPixmap pix; if (loaded) { pix = _data->full->pixSingle(_pixw, _pixh, width, height); } else { pix = _data->thumb->pixBlurredSingle(_pixw, _pixh, width, height); } QRect rthumb(rtlrect(skipx, skipy, width, height, _width)); p.drawPixmap(rthumb.topLeft(), pix); if (selected) { App::roundRect(p, rthumb, textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } if (notChild && (radial || (!loaded && !_data->loading()))) { float64 radialOpacity = (radial && loaded && !_data->uploading()) ? _animation->radial.opacity() : 1; QRect inner(rthumb.x() + (rthumb.width() - st::msgFileSize) / 2, rthumb.y() + (rthumb.height() - st::msgFileSize) / 2, st::msgFileSize, st::msgFileSize); p.setPen(Qt::NoPen); if (selected) { p.setBrush(st::msgDateImgBgSelected); } else if (isThumbAnimation(ms)) { float64 over = _animation->a_thumbOver.current(); p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over)); p.setBrush(st::black); } else { bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg); } p.setOpacity(radialOpacity * p.opacity()); p.setRenderHint(QPainter::HighQualityAntialiasing); p.drawEllipse(inner); p.setRenderHint(QPainter::HighQualityAntialiasing, false); p.setOpacity(radial ? _animation->radial.opacity() : 1); p.setOpacity(radialOpacity); style::sprite icon; if (radial || _data->loading()) { DelayedStorageImage *delayed = _data->full->toDelayedStorageImage(); if (!delayed || !delayed->location().isNull()) { icon = (selected ? st::msgFileInCancelSelected : st::msgFileInCancel); } } else { icon = (selected ? st::msgFileInDownloadSelected : st::msgFileInDownload); } if (!icon.isEmpty()) { p.drawSpriteCenter(inner, icon); } if (radial) { p.setOpacity(1); QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, selected ? st::msgInBgSelected : st::msgInBg); } } // date if (_caption.isEmpty()) { if (notChild && (_data->uploading() || App::hoveredItem() == _parent)) { int32 fullRight = skipx + width, fullBottom = skipy + height; _parent->drawInfo(p, fullRight, fullBottom, 2 * skipx + width, selected, InfoDisplayOverImage); } } else { p.setPen(st::black); _caption.draw(p, st::msgPadding.left(), skipy + height + st::mediaPadding.bottom() + st::mediaCaptionSkip, captionw, style::al_left, 0, -1, selection); } } HistoryTextState HistoryPhoto::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return result; int skipx = 0, skipy = 0, width = _width, height = _height; bool bubble = _parent->hasBubble(); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); if (!_caption.isEmpty()) { int captionw = width - st::msgPadding.left() - st::msgPadding.right(); height -= _caption.countHeight(captionw) + st::msgPadding.bottom(); if (x >= st::msgPadding.left() && y >= height && x < st::msgPadding.left() + captionw && y < _height) { result = _caption.getState(x - st::msgPadding.left(), y - height, captionw, request.forText()); return result; } height -= st::mediaCaptionSkip; } width -= st::mediaPadding.left() + st::mediaPadding.right(); height -= skipy + st::mediaPadding.bottom(); } if (x >= skipx && y >= skipy && x < skipx + width && y < skipy + height) { if (_data->uploading()) { result.link = _cancell; } else if (_data->loaded()) { result.link = _openl; } else if (_data->loading()) { DelayedStorageImage *delayed = _data->full->toDelayedStorageImage(); if (!delayed || !delayed->location().isNull()) { result.link = _cancell; } } else { result.link = _savel; } if (_caption.isEmpty() && _parent->getMedia() == this) { int32 fullRight = skipx + width, fullBottom = skipy + height; bool inDate = _parent->pointInTime(fullRight, fullBottom, x, y, InfoDisplayOverImage); if (inDate) { result.cursor = HistoryInDateCursorState; } } return result; } return result; } void HistoryPhoto::updateSentMedia(const MTPMessageMedia &media) { if (media.type() == mtpc_messageMediaPhoto) { const auto &photo(media.c_messageMediaPhoto().vphoto); App::feedPhoto(photo, _data); if (photo.type() == mtpc_photo) { const auto &sizes(photo.c_photo().vsizes.c_vector().v); int32 max = 0; const MTPDfileLocation *maxLocation = 0; for (int32 i = 0, l = sizes.size(); i < l; ++i) { char size = 0; const MTPFileLocation *loc = 0; switch (sizes.at(i).type()) { case mtpc_photoSize: { const string &s(sizes.at(i).c_photoSize().vtype.c_string().v); loc = &sizes.at(i).c_photoSize().vlocation; if (s.size()) size = s[0]; } break; case mtpc_photoCachedSize: { const string &s(sizes.at(i).c_photoCachedSize().vtype.c_string().v); loc = &sizes.at(i).c_photoCachedSize().vlocation; if (s.size()) size = s[0]; } break; } if (!loc || loc->type() != mtpc_fileLocation) continue; if (size == 's') { Local::writeImage(storageKey(loc->c_fileLocation()), _data->thumb); } else if (size == 'm') { Local::writeImage(storageKey(loc->c_fileLocation()), _data->medium); } else if (size == 'x' && max < 1) { max = 1; maxLocation = &loc->c_fileLocation(); } else if (size == 'y' && max < 2) { max = 2; maxLocation = &loc->c_fileLocation(); //} else if (size == 'w' && max < 3) { // max = 3; // maxLocation = &loc->c_fileLocation(); } } if (maxLocation) { Local::writeImage(storageKey(*maxLocation), _data->full); } } } } bool HistoryPhoto::needReSetInlineResultMedia(const MTPMessageMedia &media) { if (media.type() == mtpc_messageMediaPhoto) { if (PhotoData *existing = App::feedPhoto(media.c_messageMediaPhoto().vphoto)) { if (existing == _data) { return false; } else { // collect data } } } return false; } void HistoryPhoto::attachToParent() { App::regPhotoItem(_data, _parent); } void HistoryPhoto::detachFromParent() { App::unregPhotoItem(_data, _parent); } QString HistoryPhoto::inDialogsText() const { return _caption.isEmpty() ? lang(lng_in_dlg_photo) : _caption.original(AllTextSelection, Text::ExpandLinksNone); } QString HistoryPhoto::selectedText(TextSelection selection) const { return captionedSelectedText(lang(lng_in_dlg_photo), _caption, selection); } ImagePtr HistoryPhoto::replyPreview() { return _data->makeReplyPreview(); } HistoryVideo::HistoryVideo(HistoryItem *parent, DocumentData *document, const QString &caption) : HistoryFileMedia(parent) , _data(document) , _thumbw(1) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { if (!caption.isEmpty()) { _caption.setText(st::msgFont, caption + _parent->skipBlock(), itemTextNoMonoOptions(_parent)); } setDocumentLinks(_data); setStatusSize(FileStatusSizeReady); _data->thumb->load(); } HistoryVideo::HistoryVideo(HistoryItem *parent, const HistoryVideo &other) : HistoryFileMedia(parent) , _data(other._data) , _thumbw(other._thumbw) , _caption(other._caption) { setDocumentLinks(_data); setStatusSize(other._statusSize); } void HistoryVideo::initDimensions() { bool bubble = _parent->hasBubble(); if (_caption.hasSkipBlock()) { _caption.setSkipBlock(_parent->skipBlockWidth(), _parent->skipBlockHeight()); } int32 tw = convertScale(_data->thumb->width()), th = convertScale(_data->thumb->height()); if (!tw || !th) { tw = th = 1; } if (tw * st::msgVideoSize.height() > th * st::msgVideoSize.width()) { th = qRound((st::msgVideoSize.width() / float64(tw)) * th); tw = st::msgVideoSize.width(); } else { tw = qRound((st::msgVideoSize.height() / float64(th)) * tw); th = st::msgVideoSize.height(); } _thumbw = qMax(tw, 1); int32 minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); minWidth = qMax(minWidth, documentMaxStatusWidth(_data) + 2 * int32(st::msgDateImgDelta + st::msgDateImgPadding.x())); _maxw = qMax(_thumbw, int32(minWidth)); _minh = qMax(th, int32(st::minPhotoSize)); if (bubble) { _maxw += st::mediaPadding.left() + st::mediaPadding.right(); _minh += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { _minh += st::mediaCaptionSkip + _caption.countHeight(_maxw - st::msgPadding.left() - st::msgPadding.right()) + st::msgPadding.bottom(); } } } int HistoryVideo::resizeGetHeight(int width) { bool bubble = _parent->hasBubble(); int tw = convertScale(_data->thumb->width()), th = convertScale(_data->thumb->height()); if (!tw || !th) { tw = th = 1; } if (tw * st::msgVideoSize.height() > th * st::msgVideoSize.width()) { th = qRound((st::msgVideoSize.width() / float64(tw)) * th); tw = st::msgVideoSize.width(); } else { tw = qRound((st::msgVideoSize.height() / float64(th)) * tw); th = st::msgVideoSize.height(); } if (bubble) { width -= st::mediaPadding.left() + st::mediaPadding.right(); } if (width < tw) { th = qRound((width / float64(tw)) * th); tw = width; } _thumbw = qMax(tw, 1); int minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); minWidth = qMax(minWidth, documentMaxStatusWidth(_data) + 2 * int(st::msgDateImgDelta + st::msgDateImgPadding.x())); _width = qMax(_thumbw, int(minWidth)); _height = qMax(th, int(st::minPhotoSize)); if (bubble) { _width += st::mediaPadding.left() + st::mediaPadding.right(); _height += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { int captionw = _width - st::msgPadding.left() - st::msgPadding.right(); _height += st::mediaCaptionSkip + _caption.countHeight(captionw) + st::msgPadding.bottom(); } } return _height; } void HistoryVideo::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; _data->automaticLoad(_parent); bool loaded = _data->loaded(), displayLoading = _data->displayLoading(); bool selected = (selection == FullSelection); int skipx = 0, skipy = 0, width = _width, height = _height; bool bubble = _parent->hasBubble(); bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; int captionw = width - st::msgPadding.left() - st::msgPadding.right(); if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(_data->progress()); } } updateStatusText(); bool radial = isRadialAnimation(ms); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); width -= st::mediaPadding.left() + st::mediaPadding.right(); height -= skipy + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { height -= st::mediaCaptionSkip + _caption.countHeight(captionw) + st::msgPadding.bottom(); } } else { App::roundShadow(p, 0, 0, width, height, selected ? st::msgInShadowSelected : st::msgInShadow, selected ? InSelectedShadowCorners : InShadowCorners); } QRect rthumb(rtlrect(skipx, skipy, width, height, _width)); p.drawPixmap(rthumb.topLeft(), _data->thumb->pixBlurredSingle(_thumbw, 0, width, height)); if (selected) { App::roundRect(p, rthumb, textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } QRect inner(rthumb.x() + (rthumb.width() - st::msgFileSize) / 2, rthumb.y() + (rthumb.height() - st::msgFileSize) / 2, st::msgFileSize, st::msgFileSize); p.setPen(Qt::NoPen); if (selected) { p.setBrush(st::msgDateImgBgSelected); } else if (isThumbAnimation(ms)) { float64 over = _animation->a_thumbOver.current(); p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over)); p.setBrush(st::black); } else { bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg); } p.setRenderHint(QPainter::HighQualityAntialiasing); p.drawEllipse(inner); p.setRenderHint(QPainter::HighQualityAntialiasing, false); if (!selected && _animation) { p.setOpacity(1); } style::sprite icon; if (loaded) { icon = (selected ? st::msgFileInPlaySelected : st::msgFileInPlay); } else if (radial || _data->loading()) { icon = (selected ? st::msgFileInCancelSelected : st::msgFileInCancel); } else { icon = (selected ? st::msgFileInDownloadSelected : st::msgFileInDownload); } p.drawSpriteCenter(inner, icon); if (radial) { QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, selected ? st::msgInBgSelected : st::msgInBg); } int32 statusX = skipx + st::msgDateImgDelta + st::msgDateImgPadding.x(), statusY = skipy + st::msgDateImgDelta + st::msgDateImgPadding.y(); int32 statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x(); int32 statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y(); App::roundRect(p, rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, _width), selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? DateSelectedCorners : DateCorners); p.setFont(st::normalFont); p.setPen(st::white); p.drawTextLeft(statusX, statusY, _width, _statusText, statusW - 2 * st::msgDateImgPadding.x()); // date if (_caption.isEmpty()) { if (_parent->getMedia() == this) { int32 fullRight = skipx + width, fullBottom = skipy + height; _parent->drawInfo(p, fullRight, fullBottom, 2 * skipx + width, selected, InfoDisplayOverImage); } } else { p.setPen(st::black); _caption.draw(p, st::msgPadding.left(), skipy + height + st::mediaPadding.bottom() + st::mediaCaptionSkip, captionw, style::al_left, 0, -1, selection); } } HistoryTextState HistoryVideo::getState(int x, int y, HistoryStateRequest request) const { 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; bool bubble = _parent->hasBubble(); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); if (!_caption.isEmpty()) { int32 captionw = width - st::msgPadding.left() - st::msgPadding.right(); height -= _caption.countHeight(captionw) + st::msgPadding.bottom(); if (x >= st::msgPadding.left() && y >= height && x < st::msgPadding.left() + captionw && y < _height) { result = _caption.getState(x - st::msgPadding.left(), y - height, captionw, request.forText()); } height -= st::mediaCaptionSkip; } width -= st::mediaPadding.left() + st::mediaPadding.right(); height -= skipy + st::mediaPadding.bottom(); } if (x >= skipx && y >= skipy && x < skipx + width && y < skipy + height) { result.link = loaded ? _openl : (_data->loading() ? _cancell : _savel); if (_caption.isEmpty() && _parent->getMedia() == this) { int32 fullRight = skipx + width, fullBottom = skipy + height; bool inDate = _parent->pointInTime(fullRight, fullBottom, x, y, InfoDisplayOverImage); if (inDate) { result.cursor = HistoryInDateCursorState; } } return result; } return result; } void HistoryVideo::setStatusSize(int32 newSize) const { HistoryFileMedia::setStatusSize(newSize, _data->size, _data->duration(), 0); } QString HistoryVideo::inDialogsText() const { return _caption.isEmpty() ? lang(lng_in_dlg_video) : _caption.original(AllTextSelection, Text::ExpandLinksNone); } QString HistoryVideo::selectedText(TextSelection selection) const { return captionedSelectedText(lang(lng_in_dlg_video), _caption, selection); } void HistoryVideo::updateStatusText() const { bool showPause = false; int32 statusSize = 0, realDuration = 0; if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) { statusSize = FileStatusSizeFailed; } else if (_data->status == FileUploading) { statusSize = _data->uploadOffset; } else if (_data->loading()) { statusSize = _data->loadOffset(); } else if (_data->loaded()) { statusSize = FileStatusSizeLoaded; } else { statusSize = FileStatusSizeReady; } if (statusSize != _statusSize) { setStatusSize(statusSize); } } void HistoryVideo::attachToParent() { App::regDocumentItem(_data, _parent); } void HistoryVideo::detachFromParent() { App::unregDocumentItem(_data, _parent); } bool HistoryVideo::needReSetInlineResultMedia(const MTPMessageMedia &media) { return needReSetInlineResultDocument(media, _data); } ImagePtr HistoryVideo::replyPreview() { if (_data->replyPreview->isNull() && !_data->thumb->isNull()) { if (_data->thumb->loaded()) { int w = convertScale(_data->thumb->width()), h = convertScale(_data->thumb->height()); if (w <= 0) w = 1; if (h <= 0) h = 1; _data->replyPreview = ImagePtr(w > h ? _data->thumb->pix(w * st::msgReplyBarSize.height() / h, st::msgReplyBarSize.height()) : _data->thumb->pix(st::msgReplyBarSize.height()), "PNG"); } else { _data->thumb->load(); } } return _data->replyPreview; } HistoryDocumentVoicePlayback::HistoryDocumentVoicePlayback(const HistoryDocument *that) : _position(0) , a_progress(0., 0.) , _a_progress(animation(const_cast(that), &HistoryDocument::step_voiceProgress)) { } void HistoryDocumentVoice::ensurePlayback(const HistoryDocument *that) const { if (!_playback) { _playback = new HistoryDocumentVoicePlayback(that); } } void HistoryDocumentVoice::checkPlaybackFinished() const { if (_playback && !_playback->_a_progress.animating()) { delete _playback; _playback = nullptr; } } HistoryDocument::HistoryDocument(HistoryItem *parent, DocumentData *document, const QString &caption) : HistoryFileMedia(parent) , _data(document) { createComponents(!caption.isEmpty()); if (auto named = Get()) { named->_name = documentName(_data); named->_namew = st::semiboldFont->width(named->_name); } setDocumentLinks(_data); setStatusSize(FileStatusSizeReady); if (auto captioned = Get()) { captioned->_caption.setText(st::msgFont, caption + _parent->skipBlock(), itemTextNoMonoOptions(_parent)); } } HistoryDocument::HistoryDocument(HistoryItem *parent, const HistoryDocument &other) : HistoryFileMedia(parent) , Composer() , _data(other._data) { auto captioned = other.Get(); createComponents(captioned != 0); if (auto named = Get()) { if (auto othernamed = other.Get()) { named->_name = othernamed->_name; named->_namew = othernamed->_namew; } else { named->_name = documentName(_data); named->_namew = st::semiboldFont->width(named->_name); } } setDocumentLinks(_data); setStatusSize(other._statusSize); if (captioned) { Get()->_caption = captioned->_caption; } } void HistoryDocument::createComponents(bool caption) { uint64 mask = 0; if (_data->voice()) { mask |= HistoryDocumentVoice::Bit(); } else { mask |= HistoryDocumentNamed::Bit(); if (!_data->song() && !_data->thumb->isNull() && _data->thumb->width() && _data->thumb->height()) { mask |= HistoryDocumentThumbed::Bit(); } } if (caption) { mask |= HistoryDocumentCaptioned::Bit(); } UpdateComponents(mask); if (auto thumbed = Get()) { thumbed->_linksavel.reset(new DocumentSaveClickHandler(_data)); thumbed->_linkcancell.reset(new DocumentCancelClickHandler(_data)); } } void HistoryDocument::initDimensions() { auto captioned = Get(); if (captioned && captioned->_caption.hasSkipBlock()) { captioned->_caption.setSkipBlock(_parent->skipBlockWidth(), _parent->skipBlockHeight()); } auto thumbed = Get(); if (thumbed) { _data->thumb->load(); int32 tw = convertScale(_data->thumb->width()), th = convertScale(_data->thumb->height()); if (tw > th) { thumbed->_thumbw = (tw * st::msgFileThumbSize) / th; } else { thumbed->_thumbw = st::msgFileThumbSize; } } _maxw = st::msgFileMinWidth; int32 tleft = 0, tright = 0; if (thumbed) { tleft = st::msgFileThumbPadding.left() + st::msgFileThumbSize + st::msgFileThumbPadding.right(); tright = st::msgFileThumbPadding.left(); _maxw = qMax(_maxw, tleft + documentMaxStatusWidth(_data) + tright); } else { tleft = st::msgFilePadding.left() + st::msgFileSize + st::msgFilePadding.right(); tright = st::msgFileThumbPadding.left(); int32 unread = _data->voice() ? (st::mediaUnreadSkip + st::mediaUnreadSize) : 0; _maxw = qMax(_maxw, tleft + documentMaxStatusWidth(_data) + unread + _parent->skipBlockWidth() + st::msgPadding.right()); } if (auto named = Get()) { _maxw = qMax(tleft + named->_namew + tright, _maxw); _maxw = qMin(_maxw, int(st::msgMaxWidth)); } if (thumbed) { _minh = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom(); if (!captioned && _parent->Has()) { _minh += st::msgDateFont->height - st::msgDateDelta.y(); } } else { _minh = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom(); } if (captioned) { _minh += captioned->_caption.countHeight(_maxw - st::msgPadding.left() - st::msgPadding.right()) + st::msgPadding.bottom(); } else { _height = _minh; } } int HistoryDocument::resizeGetHeight(int width) { auto captioned = Get(); if (!captioned) { return HistoryFileMedia::resizeGetHeight(width); } _width = qMin(width, _maxw); if (Get()) { _height = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom(); } else { _height = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom(); } _height += captioned->_caption.countHeight(_width - st::msgPadding.left() - st::msgPadding.right()) + st::msgPadding.bottom(); return _height; } void HistoryDocument::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; _data->automaticLoad(_parent); bool loaded = _data->loaded(), displayLoading = _data->displayLoading(); bool selected = (selection == FullSelection); int captionw = _width - st::msgPadding.left() - st::msgPadding.right(); bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(_data->progress()); } } bool showPause = updateStatusText(); bool radial = isRadialAnimation(ms); int nameleft = 0, nametop = 0, nameright = 0, statustop = 0, linktop = 0, bottom = 0; if (auto thumbed = Get()) { nameleft = st::msgFileThumbPadding.left() + st::msgFileThumbSize + st::msgFileThumbPadding.right(); nametop = st::msgFileThumbNameTop; nameright = st::msgFileThumbPadding.left(); statustop = st::msgFileThumbStatusTop; linktop = st::msgFileThumbLinkTop; bottom = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom(); QRect rthumb(rtlrect(st::msgFileThumbPadding.left(), st::msgFileThumbPadding.top(), st::msgFileThumbSize, st::msgFileThumbSize, _width)); QPixmap thumb = loaded ? _data->thumb->pixSingle(thumbed->_thumbw, 0, st::msgFileThumbSize, st::msgFileThumbSize) : _data->thumb->pixBlurredSingle(thumbed->_thumbw, 0, st::msgFileThumbSize, st::msgFileThumbSize); p.drawPixmap(rthumb.topLeft(), thumb); if (selected) { App::roundRect(p, rthumb, textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } if (radial || (!loaded && !_data->loading())) { float64 radialOpacity = (radial && loaded && !_data->uploading()) ? _animation->radial.opacity() : 1; QRect inner(rthumb.x() + (rthumb.width() - st::msgFileSize) / 2, rthumb.y() + (rthumb.height() - st::msgFileSize) / 2, st::msgFileSize, st::msgFileSize); p.setPen(Qt::NoPen); if (selected) { p.setBrush(st::msgDateImgBgSelected); } else if (isThumbAnimation(ms)) { float64 over = _animation->a_thumbOver.current(); p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over)); p.setBrush(st::black); } else { bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg); } p.setOpacity(radialOpacity * p.opacity()); p.setRenderHint(QPainter::HighQualityAntialiasing); p.drawEllipse(inner); p.setRenderHint(QPainter::HighQualityAntialiasing, false); p.setOpacity(radialOpacity); style::sprite icon; if (radial || _data->loading()) { icon = (selected ? st::msgFileInCancelSelected : st::msgFileInCancel); } else { icon = (selected ? st::msgFileInDownloadSelected : st::msgFileInDownload); } p.setOpacity((radial && loaded) ? _animation->radial.opacity() : 1); p.drawSpriteCenter(inner, icon); if (radial) { p.setOpacity(1); QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, selected ? st::msgInBgSelected : st::msgInBg); } } if (_data->status != FileUploadFailed) { const ClickHandlerPtr &lnk((_data->loading() || _data->status == FileUploading) ? thumbed->_linkcancell : thumbed->_linksavel); bool over = ClickHandler::showAsActive(lnk); p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont); p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg)); p.drawTextLeft(nameleft, linktop, _width, thumbed->_link, thumbed->_linkw); } } else { nameleft = st::msgFilePadding.left() + st::msgFileSize + st::msgFilePadding.right(); nametop = st::msgFileNameTop; nameright = st::msgFilePadding.left(); statustop = st::msgFileStatusTop; bottom = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom(); QRect inner(rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top(), st::msgFileSize, st::msgFileSize, _width)); p.setPen(Qt::NoPen); if (selected) { p.setBrush(outbg ? st::msgFileOutBgSelected : st::msgFileInBgSelected); } else if (isThumbAnimation(ms)) { float64 over = _animation->a_thumbOver.current(); p.setBrush(style::interpolate(outbg ? st::msgFileOutBg : st::msgFileInBg, outbg ? st::msgFileOutBgOver : st::msgFileInBgOver, over)); } else { bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); p.setBrush(outbg ? (over ? st::msgFileOutBgOver : st::msgFileOutBg) : (over ? st::msgFileInBgOver : st::msgFileInBg)); } p.setRenderHint(QPainter::HighQualityAntialiasing); p.drawEllipse(inner); p.setRenderHint(QPainter::HighQualityAntialiasing, false); if (radial) { QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); style::color bg(outbg ? (selected ? st::msgOutBgSelected : st::msgOutBg) : (selected ? st::msgInBgSelected : st::msgInBg)); _animation->radial.draw(p, rinner, st::msgFileRadialLine, bg); } style::sprite icon; if (showPause) { icon = outbg ? (selected ? st::msgFileOutPauseSelected : st::msgFileOutPause) : (selected ? st::msgFileInPauseSelected : st::msgFileInPause); } else if (radial || _data->loading()) { icon = outbg ? (selected ? st::msgFileOutCancelSelected : st::msgFileOutCancel) : (selected ? st::msgFileInCancelSelected : st::msgFileInCancel); } else if (loaded) { if (_data->song() || _data->voice()) { icon = outbg ? (selected ? st::msgFileOutPlaySelected : st::msgFileOutPlay) : (selected ? st::msgFileInPlaySelected : st::msgFileInPlay); } else if (_data->isImage()) { icon = outbg ? (selected ? st::msgFileOutImageSelected : st::msgFileOutImage) : (selected ? st::msgFileInImageSelected : st::msgFileInImage); } else { icon = outbg ? (selected ? st::msgFileOutFileSelected : st::msgFileOutFile) : (selected ? st::msgFileInFileSelected : st::msgFileInFile); } } else { icon = outbg ? (selected ? st::msgFileOutDownloadSelected : st::msgFileOutDownload) : (selected ? st::msgFileInDownloadSelected : st::msgFileInDownload); } p.drawSpriteCenter(inner, icon); } int32 namewidth = _width - nameleft - nameright; if (auto voice = Get()) { const VoiceWaveform *wf = 0; uchar norm_value = 0; if (_data->voice()) { wf = &_data->voice()->waveform; if (wf->isEmpty()) { wf = 0; if (loaded) { Local::countVoiceWaveform(_data); } } else if (wf->at(0) < 0) { wf = 0; } else { norm_value = _data->voice()->wavemax; } } float64 prg = voice->_playback ? voice->_playback->a_progress.current() : 0; // rescale waveform by going in waveform.size * bar_count 1D grid style::color active(outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive)); style::color inactive(outbg ? (selected ? st::msgWaveformOutInactiveSelected : st::msgWaveformOutInactive) : (selected ? st::msgWaveformInInactiveSelected : st::msgWaveformInInactive)); int32 wf_size = wf ? wf->size() : WaveformSamplesCount, availw = int32(namewidth + st::msgWaveformSkip), activew = qRound(availw * prg); if (!outbg && !voice->_playback && _parent->isMediaUnread()) { activew = availw; } int32 bar_count = qMin(availw / int32(st::msgWaveformBar + st::msgWaveformSkip), wf_size); uchar max_value = 0; int32 max_delta = st::msgWaveformMax - st::msgWaveformMin, bottom = st::msgFilePadding.top() + st::msgWaveformMax; p.setPen(Qt::NoPen); for (int32 i = 0, bar_x = 0, sum_i = 0; i < wf_size; ++i) { uchar value = wf ? wf->at(i) : 0; if (sum_i + bar_count >= wf_size) { // draw bar sum_i = sum_i + bar_count - wf_size; if (sum_i < (bar_count + 1) / 2) { if (max_value < value) max_value = value; } int32 bar_value = ((max_value * max_delta) + ((norm_value + 1) / 2)) / (norm_value + 1); if (bar_x >= activew) { p.fillRect(nameleft + bar_x, bottom - bar_value, st::msgWaveformBar, st::msgWaveformMin + bar_value, inactive); } else if (bar_x + st::msgWaveformBar <= activew) { p.fillRect(nameleft + bar_x, bottom - bar_value, st::msgWaveformBar, st::msgWaveformMin + bar_value, active); } else { p.fillRect(nameleft + bar_x, bottom - bar_value, activew - bar_x, st::msgWaveformMin + bar_value, active); p.fillRect(nameleft + activew, bottom - bar_value, st::msgWaveformBar - (activew - bar_x), st::msgWaveformMin + bar_value, inactive); } bar_x += st::msgWaveformBar + st::msgWaveformSkip; if (sum_i < (bar_count + 1) / 2) { max_value = 0; } else { max_value = value; } } else { if (max_value < value) max_value = value; sum_i += bar_count; } } } else if (auto named = Get()) { p.setFont(st::semiboldFont); p.setPen(st::black); if (namewidth < named->_namew) { p.drawTextLeft(nameleft, nametop, _width, st::semiboldFont->elided(named->_name, namewidth)); } else { p.drawTextLeft(nameleft, nametop, _width, named->_name, named->_namew); } } style::color status(outbg ? (selected ? st::mediaOutFgSelected : st::mediaOutFg) : (selected ? st::mediaInFgSelected : st::mediaInFg)); p.setFont(st::normalFont); p.setPen(status); p.drawTextLeft(nameleft, statustop, _width, _statusText); if (_parent->isMediaUnread()) { int32 w = st::normalFont->width(_statusText); if (w + st::mediaUnreadSkip + st::mediaUnreadSize <= namewidth) { p.setPen(Qt::NoPen); p.setBrush(outbg ? (selected ? st::msgFileOutBgSelected : st::msgFileOutBg) : (selected ? st::msgFileInBgSelected : st::msgFileInBg)); p.setRenderHint(QPainter::HighQualityAntialiasing, true); p.drawEllipse(rtlrect(nameleft + w + st::mediaUnreadSkip, statustop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, _width)); p.setRenderHint(QPainter::HighQualityAntialiasing, false); } } if (auto captioned = Get()) { p.setPen(st::black); captioned->_caption.draw(p, st::msgPadding.left(), bottom, captionw, style::al_left, 0, -1, selection); } } HistoryTextState HistoryDocument::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return result; bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; bool loaded = _data->loaded(); bool showPause = updateStatusText(); int32 nameleft = 0, nametop = 0, nameright = 0, statustop = 0, linktop = 0, bottom = 0; if (auto thumbed = Get()) { nameleft = st::msgFileThumbPadding.left() + st::msgFileThumbSize + st::msgFileThumbPadding.right(); linktop = st::msgFileThumbLinkTop; bottom = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom(); QRect rthumb(rtlrect(st::msgFileThumbPadding.left(), st::msgFileThumbPadding.top(), st::msgFileThumbSize, st::msgFileThumbSize, _width)); if ((_data->loading() || _data->uploading() || !loaded) && rthumb.contains(x, y)) { result.link = (_data->loading() || _data->uploading()) ? _cancell : _savel; return result; } if (_data->status != FileUploadFailed) { if (rtlrect(nameleft, linktop, thumbed->_linkw, st::semiboldFont->height, _width).contains(x, y)) { result.link = (_data->loading() || _data->uploading()) ? thumbed->_linkcancell : thumbed->_linksavel; return result; } } } else { bottom = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom(); QRect inner(rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top(), st::msgFileSize, st::msgFileSize, _width)); if ((_data->loading() || _data->uploading() || !loaded) && inner.contains(x, y)) { result.link = (_data->loading() || _data->uploading()) ? _cancell : _savel; return result; } } int32 height = _height; if (auto captioned = Get()) { if (y >= bottom) { result = captioned->_caption.getState(x - st::msgPadding.left(), y - bottom, _width - st::msgPadding.left() - st::msgPadding.right(), request.forText()); return result; } height -= captioned->_caption.countHeight(_width - st::msgPadding.left() - st::msgPadding.right()) + st::msgPadding.bottom(); } if (x >= 0 && y >= 0 && x < _width && y < height && !_data->loading() && !_data->uploading() && _data->isValid()) { result.link = _openl; return result; } return result; } QString HistoryDocument::inDialogsText() const { QString result; if (Has()) { result = lang(lng_in_dlg_audio); } else if (auto song = _data->song()) { result = documentName(_data); if (result.isEmpty()) { result = lang(lng_in_dlg_audio_file); } } else { auto named = Get(); result = (!named || named->_name.isEmpty()) ? lang(lng_in_dlg_file) : named->_name; } if (auto captioned = Get()) { if (!captioned->_caption.isEmpty()) { result.append(' ').append(captioned->_caption.original(AllTextSelection, Text::ExpandLinksNone)); } } return result; } QString HistoryDocument::selectedText(TextSelection selection) const { const Text emptyCaption; const Text *caption = &emptyCaption; if (auto captioned = Get()) { caption = &captioned->_caption; } QString attachType = lang(lng_in_dlg_file); if (Has()) { attachType = lang(lng_in_dlg_audio); } else if (_data->song()) { attachType = lang(lng_in_dlg_audio_file); } if (auto named = Get()) { if (!named->_name.isEmpty()) { attachType.append(qstr(" : ")).append(named->_name); } } return captionedSelectedText(attachType, *caption, selection); } void HistoryDocument::setStatusSize(int32 newSize, qint64 realDuration) const { int32 duration = _data->song() ? _data->song()->duration : (_data->voice() ? _data->voice()->duration : -1); HistoryFileMedia::setStatusSize(newSize, _data->size, duration, realDuration); if (auto thumbed = Get()) { if (_statusSize == FileStatusSizeReady) { thumbed->_link = lang(lng_media_download).toUpper(); } else if (_statusSize == FileStatusSizeLoaded) { thumbed->_link = lang(lng_media_open_with).toUpper(); } else if (_statusSize == FileStatusSizeFailed) { thumbed->_link = lang(lng_media_download).toUpper(); } else if (_statusSize >= 0) { thumbed->_link = lang(lng_media_cancel).toUpper(); } else { thumbed->_link = lang(lng_media_open_with).toUpper(); } thumbed->_linkw = st::semiboldFont->width(thumbed->_link); } } bool HistoryDocument::updateStatusText() const { bool showPause = false; int32 statusSize = 0, realDuration = 0; if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) { statusSize = FileStatusSizeFailed; } else if (_data->status == FileUploading) { statusSize = _data->uploadOffset; } else if (_data->loading()) { statusSize = _data->loadOffset(); } else if (_data->loaded()) { if (_data->voice()) { AudioMsgId playing; AudioPlayerState playingState = AudioPlayerStopped; int64 playingPosition = 0, playingDuration = 0; int32 playingFrequency = 0; if (audioPlayer()) { audioPlayer()->currentState(&playing, &playingState, &playingPosition, &playingDuration, &playingFrequency); } if (playing == AudioMsgId(_data, _parent->fullId()) && !(playingState & AudioPlayerStoppedMask) && playingState != AudioPlayerFinishing) { if (auto voice = Get()) { bool was = voice->_playback; voice->ensurePlayback(this); if (!was || playingPosition != voice->_playback->_position) { float64 prg = playingDuration ? snap(float64(playingPosition) / playingDuration, 0., 1.) : 0.; if (voice->_playback->_position < playingPosition) { voice->_playback->a_progress.start(prg); } else { voice->_playback->a_progress = anim::fvalue(0., prg); } voice->_playback->_position = playingPosition; voice->_playback->_a_progress.start(); } } statusSize = -1 - (playingPosition / (playingFrequency ? playingFrequency : AudioVoiceMsgFrequency)); realDuration = playingDuration / (playingFrequency ? playingFrequency : AudioVoiceMsgFrequency); showPause = (playingState == AudioPlayerPlaying || playingState == AudioPlayerResuming || playingState == AudioPlayerStarting); } else { statusSize = FileStatusSizeLoaded; if (auto voice = Get()) { voice->checkPlaybackFinished(); } } } else if (_data->song()) { SongMsgId playing; AudioPlayerState playingState = AudioPlayerStopped; int64 playingPosition = 0, playingDuration = 0; int32 playingFrequency = 0; if (audioPlayer()) { audioPlayer()->currentState(&playing, &playingState, &playingPosition, &playingDuration, &playingFrequency); } if (playing == SongMsgId(_data, _parent->fullId()) && !(playingState & AudioPlayerStoppedMask) && playingState != AudioPlayerFinishing) { statusSize = -1 - (playingPosition / (playingFrequency ? playingFrequency : AudioVoiceMsgFrequency)); realDuration = playingDuration / (playingFrequency ? playingFrequency : AudioVoiceMsgFrequency); showPause = (playingState == AudioPlayerPlaying || playingState == AudioPlayerResuming || playingState == AudioPlayerStarting); } else { statusSize = FileStatusSizeLoaded; } if (!showPause && (playing == SongMsgId(_data, _parent->fullId())) && App::main() && App::main()->player()->seekingSong(playing)) { showPause = true; } } else { statusSize = FileStatusSizeLoaded; } } else { statusSize = FileStatusSizeReady; } if (statusSize != _statusSize) { setStatusSize(statusSize, realDuration); } return showPause; } void HistoryDocument::step_voiceProgress(float64 ms, bool timer) { if (auto voice = Get()) { if (voice->_playback) { float64 dt = ms / (2 * AudioVoiceMsgUpdateView); if (dt >= 1) { voice->_playback->_a_progress.stop(); voice->_playback->a_progress.finish(); } else { voice->_playback->a_progress.update(qMin(dt, 1.), anim::linear); } if (timer) Ui::repaintHistoryItem(_parent); } } } void HistoryDocument::attachToParent() { App::regDocumentItem(_data, _parent); } void HistoryDocument::detachFromParent() { App::unregDocumentItem(_data, _parent); } void HistoryDocument::updateSentMedia(const MTPMessageMedia &media) { if (media.type() == mtpc_messageMediaDocument) { App::feedDocument(media.c_messageMediaDocument().vdocument, _data); if (!_data->data().isEmpty()) { if (_data->voice()) { Local::writeAudio(_data->mediaKey(), _data->data()); } else { Local::writeStickerImage(_data->mediaKey(), _data->data()); } } } } bool HistoryDocument::needReSetInlineResultMedia(const MTPMessageMedia &media) { return needReSetInlineResultDocument(media, _data); } ImagePtr HistoryDocument::replyPreview() { return _data->makeReplyPreview(); } HistoryGif::HistoryGif(HistoryItem *parent, DocumentData *document, const QString &caption) : HistoryFileMedia(parent) , _data(document) , _thumbw(1) , _thumbh(1) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _gif(nullptr) { setDocumentLinks(_data, true); setStatusSize(FileStatusSizeReady); if (!caption.isEmpty()) { _caption.setText(st::msgFont, caption + _parent->skipBlock(), itemTextNoMonoOptions(_parent)); } _data->thumb->load(); } HistoryGif::HistoryGif(HistoryItem *parent, const HistoryGif &other) : HistoryFileMedia(parent) , _data(other._data) , _thumbw(other._thumbw) , _thumbh(other._thumbh) , _caption(other._caption) , _gif(nullptr) { setDocumentLinks(_data, true); setStatusSize(other._statusSize); } void HistoryGif::initDimensions() { if (_caption.hasSkipBlock()) { _caption.setSkipBlock(_parent->skipBlockWidth(), _parent->skipBlockHeight()); } bool bubble = _parent->hasBubble(); int32 tw = 0, th = 0; if (gif() && _gif->state() == ClipError) { if (!_gif->autoplay()) { Ui::showLayer(new InformBox(lang(lng_gif_error))); } App::unregGifItem(_gif); delete _gif; _gif = BadClipReader; } if (gif() && _gif->ready()) { tw = convertScale(_gif->width()); th = convertScale(_gif->height()); } else { tw = convertScale(_data->dimensions.width()), th = convertScale(_data->dimensions.height()); if (!tw || !th) { tw = convertScale(_data->thumb->width()); th = convertScale(_data->thumb->height()); } } if (tw > st::maxGifSize) { th = (st::maxGifSize * th) / tw; tw = st::maxGifSize; } if (th > st::maxGifSize) { tw = (st::maxGifSize * tw) / th; th = st::maxGifSize; } if (!tw || !th) { tw = th = 1; } _thumbw = tw; _thumbh = th; _maxw = qMax(tw, int32(st::minPhotoSize)); _minh = qMax(th, int32(st::minPhotoSize)); _maxw = qMax(_maxw, _parent->infoWidth() + 2 * int32(st::msgDateImgDelta + st::msgDateImgPadding.x())); if (!gif() || !_gif->ready()) { _maxw = qMax(_maxw, gifMaxStatusWidth(_data) + 2 * int32(st::msgDateImgDelta + st::msgDateImgPadding.x())); } if (bubble) { _maxw += st::mediaPadding.left() + st::mediaPadding.right(); _minh += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { _minh += st::mediaCaptionSkip + _caption.countHeight(_maxw - st::msgPadding.left() - st::msgPadding.right()) + st::msgPadding.bottom(); } } } int HistoryGif::resizeGetHeight(int width) { bool bubble = _parent->hasBubble(); int tw = 0, th = 0; if (gif() && _gif->ready()) { tw = convertScale(_gif->width()); th = convertScale(_gif->height()); } else { tw = convertScale(_data->dimensions.width()), th = convertScale(_data->dimensions.height()); if (!tw || !th) { tw = convertScale(_data->thumb->width()); th = convertScale(_data->thumb->height()); } } if (tw > st::maxGifSize) { th = (st::maxGifSize * th) / tw; tw = st::maxGifSize; } if (th > st::maxGifSize) { tw = (st::maxGifSize * tw) / th; th = st::maxGifSize; } if (!tw || !th) { tw = th = 1; } if (bubble) { width -= st::mediaPadding.left() + st::mediaPadding.right(); } if (width < tw) { th = qRound((width / float64(tw)) * th); tw = width; } _thumbw = tw; _thumbh = th; _width = qMax(tw, int32(st::minPhotoSize)); _height = qMax(th, int32(st::minPhotoSize)); _width = qMax(_width, _parent->infoWidth() + 2 * int32(st::msgDateImgDelta + st::msgDateImgPadding.x())); if (gif() && _gif->ready()) { if (!_gif->started()) { _gif->start(_thumbw, _thumbh, _width, _height, true); } } else { _width = qMax(_width, gifMaxStatusWidth(_data) + 2 * int32(st::msgDateImgDelta + st::msgDateImgPadding.x())); } if (bubble) { _width += st::mediaPadding.left() + st::mediaPadding.right(); _height += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { _height += st::mediaCaptionSkip + _caption.countHeight(_width - st::msgPadding.left() - st::msgPadding.right()) + st::msgPadding.bottom(); } } return _height; } void HistoryGif::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; _data->automaticLoad(_parent); bool loaded = _data->loaded(), displayLoading = (_parent->id < 0) || _data->displayLoading(); bool selected = (selection == FullSelection); if (loaded && !gif() && _gif != BadClipReader && cAutoPlayGif()) { Ui::autoplayMediaInlineAsync(_parent->fullId()); } int32 skipx = 0, skipy = 0, width = _width, height = _height; bool bubble = _parent->hasBubble(); bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; int32 captionw = width - st::msgPadding.left() - st::msgPadding.right(); bool animating = (gif() && _gif->started()); if (!animating || _parent->id < 0) { if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(dataProgress()); } } updateStatusText(); } bool radial = isRadialAnimation(ms); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); width -= st::mediaPadding.left() + st::mediaPadding.right(); height -= skipy + st::mediaPadding.bottom(); if (!_caption.isEmpty()) { height -= st::mediaCaptionSkip + _caption.countHeight(captionw) + st::msgPadding.bottom(); } } else { App::roundShadow(p, 0, 0, width, _height, selected ? st::msgInShadowSelected : st::msgInShadow, selected ? InSelectedShadowCorners : InShadowCorners); } QRect rthumb(rtlrect(skipx, skipy, width, height, _width)); if (animating) { p.drawPixmap(rthumb.topLeft(), _gif->current(_thumbw, _thumbh, width, height, (Ui::isLayerShown() || Ui::isMediaViewShown() || Ui::isInlineItemBeingChosen()) ? 0 : ms)); } else { p.drawPixmap(rthumb.topLeft(), _data->thumb->pixBlurredSingle(_thumbw, _thumbh, width, height)); } if (selected) { App::roundRect(p, rthumb, textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } if (radial || (!_gif && ((!loaded && !_data->loading()) || !cAutoPlayGif())) || (_gif == BadClipReader)) { float64 radialOpacity = (radial && loaded && _parent->id > 0) ? _animation->radial.opacity() : 1; QRect inner(rthumb.x() + (rthumb.width() - st::msgFileSize) / 2, rthumb.y() + (rthumb.height() - st::msgFileSize) / 2, st::msgFileSize, st::msgFileSize); p.setPen(Qt::NoPen); if (selected) { p.setBrush(st::msgDateImgBgSelected); } else if (isThumbAnimation(ms)) { float64 over = _animation->a_thumbOver.current(); p.setOpacity((st::msgDateImgBg->c.alphaF() * (1 - over)) + (st::msgDateImgBgOver->c.alphaF() * over)); p.setBrush(st::black); } else { bool over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg); } p.setOpacity(radialOpacity * p.opacity()); p.setRenderHint(QPainter::HighQualityAntialiasing); p.drawEllipse(inner); p.setRenderHint(QPainter::HighQualityAntialiasing, false); p.setOpacity(radialOpacity); style::sprite icon; if (_data->loaded() && !radial) { icon = (selected ? st::msgFileInPlaySelected : st::msgFileInPlay); } else if (radial || _data->loading()) { if (_parent->id > 0 || _data->uploading()) { icon = (selected ? st::msgFileInCancelSelected : st::msgFileInCancel); } } else { icon = (selected ? st::msgFileInDownloadSelected : st::msgFileInDownload); } if (!icon.isEmpty()) { p.drawSpriteCenter(inner, icon); } if (radial) { p.setOpacity(1); QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, selected ? st::msgInBgSelected : st::msgInBg); } if (!animating || _parent->id < 0) { int32 statusX = skipx + st::msgDateImgDelta + st::msgDateImgPadding.x(), statusY = skipy + st::msgDateImgDelta + st::msgDateImgPadding.y(); int32 statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x(); int32 statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y(); App::roundRect(p, rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, _width), selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? DateSelectedCorners : DateCorners); p.setFont(st::normalFont); p.setPen(st::white); p.drawTextLeft(statusX, statusY, _width, _statusText, statusW - 2 * st::msgDateImgPadding.x()); } } if (!_caption.isEmpty()) { p.setPen(st::black); _caption.draw(p, st::msgPadding.left(), skipy + height + st::mediaPadding.bottom() + st::mediaCaptionSkip, captionw, style::al_left, 0, -1, selection); } else if (_parent->getMedia() == this && (_data->uploading() || App::hoveredItem() == _parent)) { int32 fullRight = skipx + width, fullBottom = skipy + height; _parent->drawInfo(p, fullRight, fullBottom, 2 * skipx + width, selected, InfoDisplayOverImage); } } HistoryTextState HistoryGif::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return result; int32 skipx = 0, skipy = 0, width = _width, height = _height; bool bubble = _parent->hasBubble(); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); if (!_caption.isEmpty()) { int32 captionw = width - st::msgPadding.left() - st::msgPadding.right(); height -= _caption.countHeight(captionw) + st::msgPadding.bottom(); if (x >= st::msgPadding.left() && y >= height && x < st::msgPadding.left() + captionw && y < _height) { result = _caption.getState(x - st::msgPadding.left(), y - height, captionw, request.forText()); return result; } height -= st::mediaCaptionSkip; } width -= st::mediaPadding.left() + st::mediaPadding.right(); height -= skipy + st::mediaPadding.bottom(); } if (x >= skipx && y >= skipy && x < skipx + width && y < skipy + height) { if (_data->uploading()) { result.link = _cancell; } else if (!gif() || !cAutoPlayGif()) { result.link = _data->loaded() ? _openl : (_data->loading() ? _cancell : _savel); } if (_parent->getMedia() == this) { int32 fullRight = skipx + width, fullBottom = skipy + height; bool inDate = _parent->pointInTime(fullRight, fullBottom, x, y, InfoDisplayOverImage); if (inDate) { result.cursor = HistoryInDateCursorState; } } return result; } return result; } QString HistoryGif::inDialogsText() const { return qsl("GIF") + (_caption.isEmpty() ? QString() : (' ' + _caption.original(AllTextSelection, Text::ExpandLinksNone))); } QString HistoryGif::selectedText(TextSelection selection) const { return captionedSelectedText(qsl("GIF"), _caption, selection); } void HistoryGif::setStatusSize(int32 newSize) const { HistoryFileMedia::setStatusSize(newSize, _data->size, -2, 0); } void HistoryGif::updateStatusText() const { bool showPause = false; int32 statusSize = 0, realDuration = 0; if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) { statusSize = FileStatusSizeFailed; } else if (_data->status == FileUploading) { statusSize = _data->uploadOffset; } else if (_data->loading()) { statusSize = _data->loadOffset(); } else if (_data->loaded()) { statusSize = FileStatusSizeLoaded; } else { statusSize = FileStatusSizeReady; } if (statusSize != _statusSize) { setStatusSize(statusSize); } } void HistoryGif::attachToParent() { App::regDocumentItem(_data, _parent); } void HistoryGif::detachFromParent() { App::unregDocumentItem(_data, _parent); } void HistoryGif::updateSentMedia(const MTPMessageMedia &media) { if (media.type() == mtpc_messageMediaDocument) { App::feedDocument(media.c_messageMediaDocument().vdocument, _data); } } bool HistoryGif::needReSetInlineResultMedia(const MTPMessageMedia &media) { return needReSetInlineResultDocument(media, _data); } ImagePtr HistoryGif::replyPreview() { return _data->makeReplyPreview(); } bool HistoryGif::playInline(bool autoplay) { if (gif()) { stopInline(); } else { if (!cAutoPlayGif()) { App::stopGifItems(); } _gif = new ClipReader(_data->location(), _data->data(), func(_parent, &HistoryItem::clipCallback)); App::regGifItem(_gif, _parent); if (gif()) _gif->setAutoplay(); } return true; } void HistoryGif::stopInline() { if (gif()) { App::unregGifItem(_gif); delete _gif; _gif = 0; } _parent->setPendingInitDimensions(); Notify::historyItemLayoutChanged(_parent); } HistoryGif::~HistoryGif() { if (gif()) { App::unregGifItem(_gif); deleteAndMark(_gif); } } float64 HistoryGif::dataProgress() const { return (_data->uploading() || !_parent || _parent->id > 0) ? _data->progress() : 0; } bool HistoryGif::dataFinished() const { return (!_parent || _parent->id > 0) ? (!_data->loading() && !_data->uploading()) : false; } bool HistoryGif::dataLoaded() const { return (!_parent || _parent->id > 0) ? _data->loaded() : false; } namespace { class StickerClickHandler : public LeftButtonClickHandler { public: StickerClickHandler(const HistoryItem *item) : _item(item) { } protected: void onClickImpl() const override { if (HistoryMedia *media = _item->getMedia()) { if (DocumentData *document = media->getDocument()) { if (StickerData *sticker = document->sticker()) { if (sticker->set.type() != mtpc_inputStickerSetEmpty && App::main()) { App::main()->stickersBox(sticker->set); } } } } } private: const HistoryItem *_item; }; } // namespace HistorySticker::HistorySticker(HistoryItem *parent, DocumentData *document) : HistoryMedia(parent) , _pixw(1) , _pixh(1) , _data(document) , _emoji(_data->sticker()->alt) { _data->thumb->load(); if (EmojiPtr e = emojiFromText(_emoji)) { _emoji = emojiString(e); } } void HistorySticker::initDimensions() { if (!_packLink && _data->sticker() && _data->sticker()->set.type() != mtpc_inputStickerSetEmpty) { _packLink = ClickHandlerPtr(new StickerClickHandler(_parent)); } _pixw = _data->dimensions.width(); _pixh = _data->dimensions.height(); if (_pixw > st::maxStickerSize) { _pixh = (st::maxStickerSize * _pixh) / _pixw; _pixw = st::maxStickerSize; } if (_pixh > st::maxStickerSize) { _pixw = (st::maxStickerSize * _pixw) / _pixh; _pixh = st::maxStickerSize; } if (_pixw < 1) _pixw = 1; if (_pixh < 1) _pixh = 1; _maxw = qMax(_pixw, int16(st::minPhotoSize)); _minh = qMax(_pixh, int16(st::minPhotoSize)); if (_parent->getMedia() == this) { _maxw += additionalWidth(); } _height = _minh; } int HistorySticker::resizeGetHeight(int width) { // return new height _width = qMin(width, _maxw); if (_parent->getMedia() == this) { auto via = _parent->Get(); auto reply = _parent->Get(); if (via || reply) { int usew = _maxw - additionalWidth(via, reply); int availw = _width - usew - st::msgReplyPadding.left() - st::msgReplyPadding.left() - st::msgReplyPadding.left(); if (via) { via->resize(availw); } if (reply) { reply->resize(availw); } } } return _height; } void HistorySticker::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; _data->checkSticker(); bool loaded = _data->loaded(); bool selected = (selection == FullSelection); bool out = _parent->out(), isPost = _parent->isPost(), childmedia = (_parent->getMedia() != this); int usew = _maxw, usex = 0; auto via = childmedia ? nullptr : _parent->Get(); auto reply = childmedia ? nullptr : _parent->Get(); if (via || reply) { usew -= additionalWidth(via, reply); if (isPost) { } else if (out) { usex = _width - usew; } } if (rtl()) usex = _width - usex - usew; if (selected) { if (_data->sticker()->img->isNull()) { p.drawPixmap(QPoint(usex + (usew - _pixw) / 2, (_minh - _pixh) / 2), _data->thumb->pixBlurredColored(st::msgStickerOverlay, _pixw, _pixh)); } else { p.drawPixmap(QPoint(usex + (usew - _pixw) / 2, (_minh - _pixh) / 2), _data->sticker()->img->pixColored(st::msgStickerOverlay, _pixw, _pixh)); } } else { if (_data->sticker()->img->isNull()) { p.drawPixmap(QPoint(usex + (usew - _pixw) / 2, (_minh - _pixh) / 2), _data->thumb->pixBlurred(_pixw, _pixh)); } else { p.drawPixmap(QPoint(usex + (usew - _pixw) / 2, (_minh - _pixh) / 2), _data->sticker()->img->pix(_pixw, _pixh)); } } if (!childmedia) { _parent->drawInfo(p, usex + usew, _height, usex * 2 + usew, selected, InfoDisplayOverBackground); if (via || reply) { int rectw = _width - usew - st::msgReplyPadding.left(); int recth = st::msgReplyPadding.top() + st::msgReplyPadding.bottom(); if (via) { recth += st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); } if (reply) { recth += st::msgReplyBarSize.height(); } int rectx = isPost ? (usew + st::msgReplyPadding.left()) : (out ? 0 : (usew + st::msgReplyPadding.left())); int recty = _height - recth; if (rtl()) rectx = _width - rectx - rectw; // Make the bottom of the rect at the same level as the bottom of the info rect. recty -= st::msgDateImgDelta; App::roundRect(p, rectx, recty, rectw, recth, selected ? App::msgServiceSelectBg() : App::msgServiceBg(), selected ? ServiceSelectedCorners : ServiceCorners); rectx += st::msgReplyPadding.left(); rectw -= st::msgReplyPadding.left() + st::msgReplyPadding.right(); if (via) { p.drawTextLeft(rectx, recty + st::msgReplyPadding.top(), 2 * rectx + rectw, via->_text); int skip = st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); recty += skip; } if (reply) { HistoryMessageReply::PaintFlags flags = 0; if (selected) { flags |= HistoryMessageReply::PaintSelected; } reply->paint(p, _parent, rectx, recty, rectw, flags); } } } } HistoryTextState HistorySticker::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return result; bool out = _parent->out(), isPost = _parent->isPost(), childmedia = (_parent->getMedia() != this); int usew = _maxw, usex = 0; auto via = childmedia ? nullptr : _parent->Get(); auto reply = childmedia ? nullptr : _parent->Get(); if (via || reply) { usew -= additionalWidth(via, reply); if (isPost) { } else if (out) { usex = _width - usew; } } if (rtl()) usex = _width - usex - usew; if (via || reply) { int rectw = _width - usew - st::msgReplyPadding.left(); int recth = st::msgReplyPadding.top() + st::msgReplyPadding.bottom(); if (via) { recth += st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); } if (reply) { recth += st::msgReplyBarSize.height(); } int rectx = isPost ? (usew + st::msgReplyPadding.left()) : (out ? 0 : (usew + st::msgReplyPadding.left())); int recty = _height - recth; if (rtl()) rectx = _width - rectx - rectw; // Make the bottom of the rect at the same level as the bottom of the info rect. recty -= st::msgDateImgDelta; if (via) { int viah = st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? 0 : st::msgReplyPadding.bottom()); if (x >= rectx && y >= recty && x < rectx + rectw && y < recty + viah) { result.link = via->_lnk; return result; } int skip = st::msgServiceNameFont->height + (reply ? 2 * st::msgReplyPadding.top() : 0); recty += skip; recth -= skip; } if (reply) { if (x >= rectx && y >= recty && x < rectx + rectw && y < recty + recth) { result.link = reply->replyToLink(); return result; } } } if (_parent->getMedia() == this) { bool inDate = _parent->pointInTime(usex + usew, _height, x, y, InfoDisplayOverImage); if (inDate) { result.cursor = HistoryInDateCursorState; } } int pixLeft = usex + (usew - _pixw) / 2, pixTop = (_minh - _pixh) / 2; if (x >= pixLeft && x < pixLeft + _pixw && y >= pixTop && y < pixTop + _pixh) { result.link = _packLink; return result; } return result; } QString HistorySticker::inDialogsText() const { return _emoji.isEmpty() ? lang(lng_in_dlg_sticker) : lng_in_dlg_sticker_emoji(lt_emoji, _emoji); } QString HistorySticker::selectedText(TextSelection selection) const { if (selection != FullSelection) { return QString(); } return qsl("[ ") + inDialogsText() + qsl(" ]"); } void HistorySticker::attachToParent() { App::regDocumentItem(_data, _parent); } void HistorySticker::detachFromParent() { App::unregDocumentItem(_data, _parent); } void HistorySticker::updateSentMedia(const MTPMessageMedia &media) { if (media.type() == mtpc_messageMediaDocument) { App::feedDocument(media.c_messageMediaDocument().vdocument, _data); if (!_data->data().isEmpty()) { Local::writeStickerImage(_data->mediaKey(), _data->data()); } } } bool HistorySticker::needReSetInlineResultMedia(const MTPMessageMedia &media) { return needReSetInlineResultDocument(media, _data); } int HistorySticker::additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply) const { int result = 0; if (via) { accumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + via->_maxWidth + st::msgReplyPadding.left()); } if (reply) { accumulate_max(result, st::msgReplyPadding.left() + reply->replyToWidth()); } return result; } void SendMessageClickHandler::onClickImpl() const { Ui::showPeerHistory(peer()->id, ShowAtUnreadMsgId); } void AddContactClickHandler::onClickImpl() const { if (HistoryItem *item = App::histItemById(peerToChannel(peer()), msgid())) { if (HistoryMedia *media = item->getMedia()) { if (media->type() == MediaTypeContact) { QString fname = static_cast(media)->fname(); QString lname = static_cast(media)->lname(); QString phone = static_cast(media)->phone(); Ui::showLayer(new AddContactBox(fname, lname, phone)); } } } } HistoryContact::HistoryContact(HistoryItem *parent, int32 userId, const QString &first, const QString &last, const QString &phone) : HistoryMedia(parent) , _userId(userId) , _contact(0) , _phonew(0) , _fname(first) , _lname(last) , _phone(App::formatPhone(phone)) , _linkw(0) { _name.setText(st::semiboldFont, lng_full_name(lt_first_name, first, lt_last_name, last).trimmed(), _textNameOptions); _phonew = st::normalFont->width(_phone); } void HistoryContact::initDimensions() { _maxw = st::msgFileMinWidth; _contact = _userId ? App::userLoaded(_userId) : 0; if (_contact) { _contact->loadUserpic(); } if (_contact && _contact->contact > 0) { _linkl.reset(new SendMessageClickHandler(_contact)); _link = lang(lng_profile_send_message).toUpper(); } else if (_userId) { _linkl.reset(new AddContactClickHandler(_parent->history()->peer->id, _parent->id)); _link = lang(lng_profile_add_contact).toUpper(); } _linkw = _link.isEmpty() ? 0 : st::semiboldFont->width(_link); int32 tleft = 0, tright = 0; if (_userId) { tleft = st::msgFileThumbPadding.left() + st::msgFileThumbSize + st::msgFileThumbPadding.right(); tright = st::msgFileThumbPadding.left(); _maxw = qMax(_maxw, tleft + _phonew + tright); } else { tleft = st::msgFilePadding.left() + st::msgFileSize + st::msgFilePadding.right(); tright = st::msgFileThumbPadding.left(); _maxw = qMax(_maxw, tleft + _phonew + _parent->skipBlockWidth() + st::msgPadding.right()); } _maxw = qMax(tleft + _name.maxWidth() + tright, _maxw); _maxw = qMin(_maxw, int(st::msgMaxWidth)); if (_userId) { _minh = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom(); } else { _minh = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom(); } _height = _minh; } void HistoryContact::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; int32 skipx = 0, skipy = 0, width = _width, height = _height; bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; bool selected = (selection == FullSelection); if (width >= _maxw) { width = _maxw; } int32 nameleft = 0, nametop = 0, nameright = 0, statustop = 0, linktop = 0; if (_userId) { nameleft = st::msgFileThumbPadding.left() + st::msgFileThumbSize + st::msgFileThumbPadding.right(); nametop = st::msgFileThumbNameTop; nameright = st::msgFileThumbPadding.left(); statustop = st::msgFileThumbStatusTop; linktop = st::msgFileThumbLinkTop; QRect rthumb(rtlrect(st::msgFileThumbPadding.left(), st::msgFileThumbPadding.top(), st::msgFileThumbSize, st::msgFileThumbSize, width)); if (_contact) { _contact->paintUserpic(p, st::msgFileThumbSize, rthumb.x(), rthumb.y()); } else { p.drawPixmap(rthumb.topLeft(), userDefPhoto(qAbs(_userId) % UserColorsCount)->pixCircled(st::msgFileThumbSize, st::msgFileThumbSize)); } if (selected) { App::roundRect(p, rthumb, textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } bool over = ClickHandler::showAsActive(_linkl); p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont); p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg)); p.drawTextLeft(nameleft, linktop, width, _link, _linkw); } else { nameleft = st::msgFilePadding.left() + st::msgFileSize + st::msgFilePadding.right(); nametop = st::msgFileNameTop; nameright = st::msgFilePadding.left(); statustop = st::msgFileStatusTop; QRect inner(rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top(), st::msgFileSize, st::msgFileSize, width)); p.drawPixmap(inner.topLeft(), userDefPhoto(qAbs(_parent->id) % UserColorsCount)->pixCircled(st::msgFileSize, st::msgFileSize)); } int32 namewidth = width - nameleft - nameright; p.setFont(st::semiboldFont); p.setPen(st::black); _name.drawLeftElided(p, nameleft, nametop, namewidth, width); style::color status(outbg ? (selected ? st::mediaOutFgSelected : st::mediaOutFg) : (selected ? st::mediaInFgSelected : st::mediaInFg)); p.setFont(st::normalFont); p.setPen(status); p.drawTextLeft(nameleft, statustop, width, _phone); } HistoryTextState HistoryContact::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; int32 nameleft = 0, nametop = 0, nameright = 0, statustop = 0, linktop = 0; if (_userId) { nameleft = st::msgFileThumbPadding.left() + st::msgFileThumbSize + st::msgFileThumbPadding.right(); linktop = st::msgFileThumbLinkTop; if (rtlrect(nameleft, linktop, _linkw, st::semiboldFont->height, _width).contains(x, y)) { result.link = _linkl; return result; } } if (x >= 0 && y >= 0 && x < _width && y < _height && _contact) { result.link = _contact->openLink(); return result; } return result; } QString HistoryContact::inDialogsText() const { return lang(lng_in_dlg_contact); } QString HistoryContact::selectedText(TextSelection selection) const { if (selection != FullSelection) { return QString(); } return qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.original() + '\n' + _phone; } void HistoryContact::attachToParent() { if (_userId) { App::regSharedContactItem(_userId, _parent); } } void HistoryContact::detachFromParent() { if (_userId) { App::unregSharedContactItem(_userId, _parent); } } void HistoryContact::updateSentMedia(const MTPMessageMedia &media) { if (media.type() == mtpc_messageMediaContact) { if (_userId != media.c_messageMediaContact().vuser_id.v) { detachFromParent(); _userId = media.c_messageMediaContact().vuser_id.v; attachToParent(); } } } namespace { QString siteNameFromUrl(const QString &url) { QUrl u(url); QString pretty = u.isValid() ? u.toDisplayString() : url; QRegularExpressionMatch m = QRegularExpression(qsl("^[a-zA-Z0-9]+://")).match(pretty); if (m.hasMatch()) pretty = pretty.mid(m.capturedLength()); int32 slash = pretty.indexOf('/'); if (slash > 0) pretty = pretty.mid(0, slash); QStringList components = pretty.split('.', QString::SkipEmptyParts); if (components.size() >= 2) { components = components.mid(components.size() - 2); return components.at(0).at(0).toUpper() + components.at(0).mid(1) + '.' + components.at(1); } return QString(); } int32 articleThumbWidth(PhotoData *thumb, int32 height) { int32 w = thumb->medium->width(), h = thumb->medium->height(); return qMax(qMin(height * w / h, height), 1); } int32 articleThumbHeight(PhotoData *thumb, int32 width) { return qMax(thumb->medium->height() * width / thumb->medium->width(), 1); } int32 _lineHeight = 0; } HistoryWebPage::HistoryWebPage(HistoryItem *parent, WebPageData *data) : HistoryMedia(parent) , _data(data) , _openl(0) , _attach(nullptr) , _asArticle(false) , _title(st::msgMinWidth - st::webPageLeft) , _description(st::msgMinWidth - st::webPageLeft) , _siteNameWidth(0) , _durationWidth(0) , _pixw(0) , _pixh(0) { } HistoryWebPage::HistoryWebPage(HistoryItem *parent, const HistoryWebPage &other) : HistoryMedia(parent) , _data(other._data) , _openl(0) , _attach(other._attach ? other._attach->clone(parent) : nullptr) , _asArticle(other._asArticle) , _title(other._title) , _description(other._description) , _siteNameWidth(other._siteNameWidth) , _durationWidth(other._durationWidth) , _pixw(other._pixw) , _pixh(other._pixh) { } void HistoryWebPage::initDimensions() { if (_data->pendingTill) { _maxw = _minh = _height = 0; return; } if (!_lineHeight) _lineHeight = qMax(st::webPageTitleFont->height, st::webPageDescriptionFont->height); if (!_openl && !_data->url.isEmpty()) _openl.reset(new UrlClickHandler(_data->url, true)); // init layout QString title(_data->title.isEmpty() ? _data->author : _data->title); if (!_data->description.isEmpty() && title.isEmpty() && _data->siteName.isEmpty() && !_data->url.isEmpty()) { _data->siteName = siteNameFromUrl(_data->url); } if (!_data->document && _data->photo && _data->type != WebPagePhoto && _data->type != WebPageVideo) { if (_data->type == WebPageProfile) { _asArticle = true; } else if (_data->siteName == qstr("Twitter") || _data->siteName == qstr("Facebook")) { _asArticle = false; } else { _asArticle = true; } if (_asArticle && _data->description.isEmpty() && title.isEmpty() && _data->siteName.isEmpty()) { _asArticle = false; } } else { _asArticle = false; } // init attach if (!_asArticle && !_attach) { if (_data->document) { if (_data->document->sticker()) { _attach = new HistorySticker(_parent, _data->document); } else if (_data->document->isAnimation()) { _attach = new HistoryGif(_parent, _data->document, QString()); } else if (_data->document->isVideo()) { _attach = new HistoryVideo(_parent, _data->document, QString()); } else { _attach = new HistoryDocument(_parent, _data->document, QString()); } } else if (_data->photo) { _attach = new HistoryPhoto(_parent, _data->photo, QString()); } } // init strings if (_description.isEmpty() && !_data->description.isEmpty()) { QString text = textClean(_data->description); if (text.isEmpty()) { _data->description = QString(); } else { if (!_asArticle && !_attach) { text += _parent->skipBlock(); } const TextParseOptions *opts = &_webpageDescriptionOptions; if (_data->siteName == qstr("Twitter")) { opts = &_twitterDescriptionOptions; } else if (_data->siteName == qstr("Instagram")) { opts = &_instagramDescriptionOptions; } _description.setText(st::webPageDescriptionFont, text, *opts); } } if (_title.isEmpty() && !title.isEmpty()) { title = textOneLine(textClean(title)); if (title.isEmpty()) { if (_data->title.isEmpty()) { _data->author = QString(); } else { _data->title = QString(); } } else { if (!_asArticle && !_attach && _description.isEmpty()) { title += _parent->skipBlock(); } _title.setText(st::webPageTitleFont, title, _webpageTitleOptions); } } if (!_siteNameWidth && !_data->siteName.isEmpty()) { _siteNameWidth = st::webPageTitleFont->width(_data->siteName); } // init dimensions int32 l = st::msgPadding.left() + st::webPageLeft, r = st::msgPadding.right(); int32 skipBlockWidth = _parent->skipBlockWidth(); _maxw = skipBlockWidth; _minh = 0; int32 siteNameHeight = _data->siteName.isEmpty() ? 0 : _lineHeight; int32 titleMinHeight = _title.isEmpty() ? 0 : _lineHeight; int32 descMaxLines = (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1)); int32 descriptionMinHeight = _description.isEmpty() ? 0 : qMin(_description.minHeight(), descMaxLines * _lineHeight); int32 articleMinHeight = siteNameHeight + titleMinHeight + descriptionMinHeight; int32 articlePhotoMaxWidth = 0; if (_asArticle) { articlePhotoMaxWidth = st::webPagePhotoDelta + qMax(articleThumbWidth(_data->photo, articleMinHeight), _lineHeight); } if (_siteNameWidth) { if (_title.isEmpty() && _description.isEmpty()) { _maxw = qMax(_maxw, int32(_siteNameWidth + _parent->skipBlockWidth())); } else { _maxw = qMax(_maxw, int32(_siteNameWidth + articlePhotoMaxWidth)); } _minh += _lineHeight; } if (!_title.isEmpty()) { _maxw = qMax(_maxw, int32(_title.maxWidth() + articlePhotoMaxWidth)); _minh += titleMinHeight; } if (!_description.isEmpty()) { _maxw = qMax(_maxw, int32(_description.maxWidth() + articlePhotoMaxWidth)); _minh += descriptionMinHeight; } if (_attach) { if (_minh) _minh += st::webPagePhotoSkip; _attach->initDimensions(); QMargins bubble(_attach->bubbleMargins()); _maxw = qMax(_maxw, int32(_attach->maxWidth() - bubble.left() - bubble.top() + (_attach->customInfoLayout() ? skipBlockWidth : 0))); _minh += _attach->minHeight() - bubble.top() - bubble.bottom(); } if (_data->type == WebPageVideo && _data->duration) { _duration = formatDurationText(_data->duration); _durationWidth = st::msgDateFont->width(_duration); } _maxw += st::msgPadding.left() + st::webPageLeft + st::msgPadding.right(); _minh += st::msgPadding.bottom(); if (_asArticle) { _minh = resizeGetHeight(_maxw); // hack // _minh += st::msgDateFont->height; } } int HistoryWebPage::resizeGetHeight(int width) { if (_data->pendingTill) { _width = width; _height = _minh; return _height; } _width = qMin(width, _maxw); width -= st::msgPadding.left() + st::webPageLeft + st::msgPadding.right(); int32 linesMax = 5; int32 siteNameLines = _siteNameWidth ? 1 : 0, siteNameHeight = _siteNameWidth ? _lineHeight : 0; if (_asArticle) { _pixh = linesMax * _lineHeight; do { _pixw = articleThumbWidth(_data->photo, _pixh); int32 wleft = width - st::webPagePhotoDelta - qMax(_pixw, int16(_lineHeight)); _height = siteNameHeight; if (_title.isEmpty()) { _titleLines = 0; } else { if (_title.countHeight(wleft) < 2 * st::webPageTitleFont->height) { _titleLines = 1; } else { _titleLines = 2; } _height += _titleLines * _lineHeight; } int32 descriptionHeight = _description.countHeight(wleft); if (descriptionHeight < (linesMax - siteNameLines - _titleLines) * st::webPageDescriptionFont->height) { _descriptionLines = (descriptionHeight / st::webPageDescriptionFont->height); } else { _descriptionLines = (linesMax - siteNameLines - _titleLines); } _height += _descriptionLines * _lineHeight; if (_height >= _pixh) { break; } _pixh -= _lineHeight; } while (_pixh > _lineHeight); _height += st::msgDateFont->height; } else { _height = siteNameHeight; if (_title.isEmpty()) { _titleLines = 0; } else { if (_title.countHeight(width) < 2 * st::webPageTitleFont->height) { _titleLines = 1; } else { _titleLines = 2; } _height += _titleLines * _lineHeight; } if (_description.isEmpty()) { _descriptionLines = 0; } else { int32 descriptionHeight = _description.countHeight(width); if (descriptionHeight < (linesMax - siteNameLines - _titleLines) * st::webPageDescriptionFont->height) { _descriptionLines = (descriptionHeight / st::webPageDescriptionFont->height); } else { _descriptionLines = (linesMax - siteNameLines - _titleLines); } _height += _descriptionLines * _lineHeight; } if (_attach) { if (_height) _height += st::webPagePhotoSkip; QMargins bubble(_attach->bubbleMargins()); _attach->resizeGetHeight(width + bubble.left() + bubble.right()); _height += _attach->height() - bubble.top() - bubble.bottom(); if (_attach->customInfoLayout() && _attach->currentWidth() + _parent->skipBlockWidth() > width + bubble.left() + bubble.right()) { _height += st::msgDateFont->height; } } } _height += st::msgPadding.bottom(); return _height; } void HistoryWebPage::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; int32 skipx = 0, skipy = 0, width = _width, height = _height; bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; bool selected = (selection == FullSelection); style::color barfg = (selected ? (outbg ? st::msgOutReplyBarSelColor : st::msgInReplyBarSelColor) : (outbg ? st::msgOutReplyBarColor : st::msgInReplyBarColor)); style::color semibold = (selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg)); style::color regular = (selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg)); int32 lshift = st::msgPadding.left() + st::webPageLeft, rshift = st::msgPadding.right(), bshift = st::msgPadding.bottom(); width -= lshift + rshift; QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); if (_asArticle || (_attach && _attach->customInfoLayout() && _attach->currentWidth() + _parent->skipBlockWidth() > width + bubble.left() + bubble.right())) { bshift += st::msgDateFont->height; } QRect bar(rtlrect(st::msgPadding.left(), 0, st::webPageBar, _height - bshift, _width)); p.fillRect(bar, barfg); if (_asArticle) { _data->photo->medium->load(false, false); bool full = _data->photo->medium->loaded(); QPixmap pix; int32 pw = qMax(_pixw, int16(_lineHeight)), ph = _pixh; int32 pixw = _pixw, pixh = articleThumbHeight(_data->photo, _pixw); int32 maxw = convertScale(_data->photo->medium->width()), maxh = convertScale(_data->photo->medium->height()); if (pixw * ph != pixh * pw) { float64 coef = (pixw * ph > pixh * pw) ? qMin(ph / float64(pixh), maxh / float64(pixh)) : qMin(pw / float64(pixw), maxw / float64(pixw)); pixh = qRound(pixh * coef); pixw = qRound(pixw * coef); } if (full) { pix = _data->photo->medium->pixSingle(pixw, pixh, pw, ph); } else { pix = _data->photo->thumb->pixBlurredSingle(pixw, pixh, pw, ph); } p.drawPixmapLeft(lshift + width - pw, 0, _width, pix); if (selected) { App::roundRect(p, rtlrect(lshift + width - pw, 0, pw, _pixh, _width), textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } width -= pw + st::webPagePhotoDelta; } int32 tshift = 0; if (_siteNameWidth) { p.setFont(st::webPageTitleFont); p.setPen(semibold); p.drawTextLeft(lshift, tshift, _width, (width >= _siteNameWidth) ? _data->siteName : st::webPageTitleFont->elided(_data->siteName, width)); tshift += _lineHeight; } if (_titleLines) { p.setPen(st::black); int32 endskip = 0; if (_title.hasSkipBlock()) { endskip = _parent->skipBlockWidth(); } _title.drawLeftElided(p, lshift, tshift, width, _width, _titleLines, style::al_left, 0, -1, endskip, false, selection); tshift += _titleLines * _lineHeight; } if (_descriptionLines) { p.setPen(st::black); int32 endskip = 0; if (_description.hasSkipBlock()) { endskip = _parent->skipBlockWidth(); } _description.drawLeftElided(p, lshift, tshift, width, _width, _descriptionLines, style::al_left, 0, -1, endskip, false, toDescriptionSelection(selection)); tshift += _descriptionLines * _lineHeight; } if (_attach) { if (tshift) tshift += st::webPagePhotoSkip; int32 attachLeft = lshift - bubble.left(), attachTop = tshift - bubble.top(); if (rtl()) attachLeft = _width - attachLeft - _attach->currentWidth(); p.save(); p.translate(attachLeft, attachTop); auto attachSelection = selected ? FullSelection : TextSelection{ 0, 0 }; _attach->draw(p, r.translated(-attachLeft, -attachTop), attachSelection, ms); int32 pixwidth = _attach->currentWidth(), pixheight = _attach->height(); if (_data->type == WebPageVideo) { if (_data->siteName == qstr("YouTube")) { p.drawPixmap(QPoint((pixwidth - st::youtubeIcon.pxWidth()) / 2, (pixheight - st::youtubeIcon.pxHeight()) / 2), App::sprite(), st::youtubeIcon); } else { p.drawPixmap(QPoint((pixwidth - st::videoIcon.pxWidth()) / 2, (pixheight - st::videoIcon.pxHeight()) / 2), App::sprite(), st::videoIcon); } if (_durationWidth) { int32 dateX = pixwidth - _durationWidth - st::msgDateImgDelta - 2 * st::msgDateImgPadding.x(); int32 dateY = pixheight - st::msgDateFont->height - 2 * st::msgDateImgPadding.y() - st::msgDateImgDelta; int32 dateW = pixwidth - dateX - st::msgDateImgDelta; int32 dateH = pixheight - dateY - st::msgDateImgDelta; App::roundRect(p, dateX, dateY, dateW, dateH, selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? DateSelectedCorners : DateCorners); p.setFont(st::msgDateFont); p.setPen(st::msgDateImgColor); p.drawTextLeft(dateX + st::msgDateImgPadding.x(), dateY + st::msgDateImgPadding.y(), pixwidth, _duration); } } p.restore(); } } HistoryTextState HistoryWebPage::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return result; int32 skipx = 0, skipy = 0, width = _width, height = _height; int32 lshift = st::msgPadding.left() + st::webPageLeft, rshift = st::msgPadding.right(), bshift = st::msgPadding.bottom(); width -= lshift + rshift; QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); if (_asArticle || (_attach && _attach->customInfoLayout() && _attach->currentWidth() + _parent->skipBlockWidth() > width + bubble.left() + bubble.right())) { bshift += st::msgDateFont->height; } bool inThumb = false; if (_asArticle) { int32 pw = qMax(_pixw, int16(_lineHeight)); if (rtlrect(lshift + width - pw, 0, pw, _pixh, _width).contains(x, y)) { inThumb = true; } width -= pw + st::webPagePhotoDelta; } int tshift = 0, symbolAdd = 0; if (_siteNameWidth) { tshift += _lineHeight; } if (_titleLines) { if (y >= tshift && y < tshift + _titleLines * _lineHeight) { Text::StateRequestElided titleRequest = request.forText(); titleRequest.lines = _titleLines; result = _title.getStateElidedLeft(x - lshift, y - tshift, width, _width, titleRequest); } else if (y >= tshift + _titleLines * _lineHeight) { symbolAdd += _title.length(); } tshift += _titleLines * _lineHeight; } if (_descriptionLines) { if (y >= tshift && y < tshift + _descriptionLines * _lineHeight) { Text::StateRequestElided descriptionRequest = request.forText(); descriptionRequest.lines = _descriptionLines; result = _description.getStateElidedLeft(x - lshift, y - tshift, width, _width, descriptionRequest); } else if (y >= tshift + _descriptionLines * _lineHeight) { symbolAdd += _description.length(); } tshift += _descriptionLines * _lineHeight; } if (inThumb) { result.link = _openl; } else if (_attach) { if (tshift) tshift += st::webPagePhotoSkip; if (x >= lshift && x < lshift + width && y >= tshift && y < _height - st::msgPadding.bottom()) { int32 attachLeft = lshift - bubble.left(), attachTop = tshift - bubble.top(); if (rtl()) attachLeft = _width - attachLeft - _attach->currentWidth(); result = _attach->getState(x - attachLeft, y - attachTop, request); if (result.link && !_data->document && _data->photo) { if (_data->type == WebPageProfile || _data->type == WebPageVideo) { result.link = _openl; } else if (_data->type == WebPagePhoto || _data->siteName == qstr("Twitter") || _data->siteName == qstr("Facebook")) { // leave photo link } else { result.link = _openl; } } } } result.symbol += symbolAdd; return result; } TextSelection HistoryWebPage::adjustSelection(TextSelection selection, TextSelectType type) const { if (!_descriptionLines || selection.to <= _title.length()) { return _title.adjustSelection(selection, type); } auto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type); if (selection.from >= _title.length()) { return fromDescriptionSelection(descriptionSelection); } auto titleSelection = _title.adjustSelection(selection, type); return { titleSelection.from, fromDescriptionSelection(descriptionSelection).to }; } void HistoryWebPage::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (_attach) { _attach->clickHandlerActiveChanged(p, active); } } void HistoryWebPage::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { if (_attach) { _attach->clickHandlerPressedChanged(p, pressed); } } void HistoryWebPage::attachToParent() { App::regWebPageItem(_data, _parent); if (_attach) _attach->attachToParent(); } void HistoryWebPage::detachFromParent() { App::unregWebPageItem(_data, _parent); if (_attach) _attach->detachFromParent(); } QString HistoryWebPage::inDialogsText() const { return QString(); } QString HistoryWebPage::selectedText(TextSelection selection) const { if (selection == FullSelection) { return QString(); } auto titleResult = _title.original(selection, Text::ExpandLinksAll); auto descriptionResult = _description.original(toDescriptionSelection(selection), Text::ExpandLinksAll); if (titleResult.isEmpty()) { return descriptionResult; } else if (descriptionResult.isEmpty()) { return titleResult; } return titleResult + '\n' + descriptionResult; } ImagePtr HistoryWebPage::replyPreview() { return _attach ? _attach->replyPreview() : (_data->photo ? _data->photo->makeReplyPreview() : ImagePtr()); } HistoryWebPage::~HistoryWebPage() { deleteAndMark(_attach); } namespace { LocationManager manager; } void LocationManager::init() { if (manager) delete manager; manager = new QNetworkAccessManager(); App::setProxySettings(*manager); connect(manager, SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), this, SLOT(onFailed(QNetworkReply*))); connect(manager, SIGNAL(sslErrors(QNetworkReply*, const QList&)), this, SLOT(onFailed(QNetworkReply*))); connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(onFinished(QNetworkReply*))); if (black) delete black; QImage b(cIntRetinaFactor(), cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); { QPainter p(&b); p.fillRect(QRect(0, 0, cIntRetinaFactor(), cIntRetinaFactor()), st::white->b); } QPixmap p = QPixmap::fromImage(b, Qt::ColorOnly); p.setDevicePixelRatio(cRetinaFactor()); black = new ImagePtr(p, "PNG"); } void LocationManager::reinit() { if (manager) App::setProxySettings(*manager); } void LocationManager::deinit() { if (manager) { delete manager; manager = 0; } if (black) { delete black; black = 0; } dataLoadings.clear(); imageLoadings.clear(); } void initImageLinkManager() { manager.init(); } void reinitImageLinkManager() { manager.reinit(); } void deinitImageLinkManager() { manager.deinit(); } void LocationManager::getData(LocationData *data) { if (!manager) { DEBUG_LOG(("App Error: getting image link data without manager init!")); return failed(data); } int32 w = st::locationSize.width(), h = st::locationSize.height(); int32 zoom = 13, scale = 1; if (cScale() == dbisTwo || cRetina()) { scale = 2; } else { w = convertScale(w); h = convertScale(h); } QString coords = qsl("%1,%2").arg(data->coords.lat).arg(data->coords.lon); QString url = qsl("https://maps.googleapis.com/maps/api/staticmap?center=") + coords + qsl("&zoom=%1&size=%2x%3&maptype=roadmap&scale=%4&markers=color:red|size:big|").arg(zoom).arg(w).arg(h).arg(scale) + coords + qsl("&sensor=false"); QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(url))); imageLoadings[reply] = data; } void LocationManager::onFinished(QNetworkReply *reply) { if (!manager) return; if (reply->error() != QNetworkReply::NoError) return onFailed(reply); QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); if (statusCode.isValid()) { int status = statusCode.toInt(); if (status == 301 || status == 302) { QString loc = reply->header(QNetworkRequest::LocationHeader).toString(); if (!loc.isEmpty()) { QMap::iterator i = dataLoadings.find(reply); if (i != dataLoadings.cend()) { LocationData *d = i.value(); if (serverRedirects.constFind(d) == serverRedirects.cend()) { serverRedirects.insert(d, 1); } else if (++serverRedirects[d] > MaxHttpRedirects) { DEBUG_LOG(("Network Error: Too many HTTP redirects in onFinished() for image link: %1").arg(loc)); return onFailed(reply); } dataLoadings.erase(i); dataLoadings.insert(manager->get(QNetworkRequest(loc)), d); return; } else if ((i = imageLoadings.find(reply)) != imageLoadings.cend()) { LocationData *d = i.value(); if (serverRedirects.constFind(d) == serverRedirects.cend()) { serverRedirects.insert(d, 1); } else if (++serverRedirects[d] > MaxHttpRedirects) { DEBUG_LOG(("Network Error: Too many HTTP redirects in onFinished() for image link: %1").arg(loc)); return onFailed(reply); } imageLoadings.erase(i); imageLoadings.insert(manager->get(QNetworkRequest(loc)), d); return; } } } if (status != 200) { DEBUG_LOG(("Network Error: Bad HTTP status received in onFinished() for image link: %1").arg(status)); return onFailed(reply); } } LocationData *d = 0; QMap::iterator i = dataLoadings.find(reply); if (i != dataLoadings.cend()) { d = i.value(); dataLoadings.erase(i); QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &e); if (e.error != QJsonParseError::NoError) { DEBUG_LOG(("JSON Error: Bad json received in onFinished() for image link")); return onFailed(reply); } failed(d); if (App::main()) App::main()->update(); } else { i = imageLoadings.find(reply); if (i != imageLoadings.cend()) { d = i.value(); imageLoadings.erase(i); QPixmap thumb; QByteArray format; QByteArray data(reply->readAll()); { QBuffer buffer(&data); QImageReader reader(&buffer); #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) reader.setAutoTransform(true); #endif thumb = QPixmap::fromImageReader(&reader, Qt::ColorOnly); format = reader.format(); thumb.setDevicePixelRatio(cRetinaFactor()); if (format.isEmpty()) format = QByteArray("JPG"); } d->loading = false; d->thumb = thumb.isNull() ? (*black) : ImagePtr(thumb, format); serverRedirects.remove(d); if (App::main()) App::main()->update(); } } } void LocationManager::onFailed(QNetworkReply *reply) { if (!manager) return; LocationData *d = 0; QMap::iterator i = dataLoadings.find(reply); if (i != dataLoadings.cend()) { d = i.value(); dataLoadings.erase(i); } else { i = imageLoadings.find(reply); if (i != imageLoadings.cend()) { d = i.value(); imageLoadings.erase(i); } } DEBUG_LOG(("Network Error: failed to get data for image link %1,%2 error %3").arg(d ? d->coords.lat : 0).arg(d ? d->coords.lon : 0).arg(reply->errorString())); if (d) { failed(d); } } void LocationManager::failed(LocationData *data) { data->loading = false; data->thumb = *black; serverRedirects.remove(data); } void LocationData::load() { if (!thumb->isNull()) return thumb->load(false, false); if (loading) return; loading = true; manager.getData(this); } HistoryLocation::HistoryLocation(HistoryItem *parent, const LocationCoords &coords, const QString &title, const QString &description) : HistoryMedia(parent) , _data(App::location(coords)) , _title(st::msgMinWidth) , _description(st::msgMinWidth) , _link(new LocationClickHandler(coords)) { if (!title.isEmpty()) { _title.setText(st::webPageTitleFont, textClean(title), _webpageTitleOptions); } if (!description.isEmpty()) { _description.setText(st::webPageDescriptionFont, textClean(description), _webpageDescriptionOptions); } } HistoryLocation::HistoryLocation(HistoryItem *parent, const HistoryLocation &other) : HistoryMedia(parent) , _data(other._data) , _title(other._title) , _description(other._description) , _link(new LocationClickHandler(_data->coords)) { } void HistoryLocation::initDimensions() { bool bubble = _parent->hasBubble(); int32 tw = fullWidth(), th = fullHeight(); if (tw > st::maxMediaSize) { th = (st::maxMediaSize * th) / tw; tw = st::maxMediaSize; } int32 minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); _maxw = qMax(tw, int32(minWidth)); _minh = qMax(th, int32(st::minPhotoSize)); if (bubble) { _maxw += st::mediaPadding.left() + st::mediaPadding.right(); if (!_title.isEmpty()) { _minh += qMin(_title.countHeight(_maxw - st::msgPadding.left() - st::msgPadding.right()), 2 * st::webPageTitleFont->height); } if (!_description.isEmpty()) { _maxw = qMax(_maxw, int32(st::msgPadding.left() + _description.maxWidth() + st::msgPadding.right())); _minh += qMin(_description.countHeight(_maxw - st::msgPadding.left() - st::msgPadding.right()), 3 * st::webPageDescriptionFont->height); } _minh += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_title.isEmpty() || !_description.isEmpty()) { _minh += st::webPagePhotoSkip; if (!_parent->Has() && !_parent->Has()) { _minh += st::msgPadding.top(); } } } } int HistoryLocation::resizeGetHeight(int width) { bool bubble = _parent->hasBubble(); _width = qMin(width, _maxw); if (bubble) { _width -= st::mediaPadding.left() + st::mediaPadding.right(); } int32 tw = fullWidth(), th = fullHeight(); if (tw > st::maxMediaSize) { th = (st::maxMediaSize * th) / tw; tw = st::maxMediaSize; } _height = th; if (tw > _width) { _height = (_width * _height / tw); } else { _width = tw; } int32 minWidth = qMax(st::minPhotoSize, _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); _width = qMax(_width, int32(minWidth)); _height = qMax(_height, int32(st::minPhotoSize)); if (bubble) { _width += st::mediaPadding.left() + st::mediaPadding.right(); _height += st::mediaPadding.top() + st::mediaPadding.bottom(); if (!_title.isEmpty()) { _height += qMin(_title.countHeight(_width - st::msgPadding.left() - st::msgPadding.right()), st::webPageTitleFont->height * 2); } if (!_description.isEmpty()) { _height += qMin(_description.countHeight(_width - st::msgPadding.left() - st::msgPadding.right()), st::webPageDescriptionFont->height * 3); } if (!_title.isEmpty() || !_description.isEmpty()) { _height += st::webPagePhotoSkip; if (!_parent->Has() && !_parent->Has()) { _height += st::msgPadding.top(); } } } return _height; } void HistoryLocation::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return; int32 skipx = 0, skipy = 0, width = _width, height = _height; bool bubble = _parent->hasBubble(); bool out = _parent->out(), isPost = _parent->isPost(), outbg = out && !isPost; bool selected = (selection == FullSelection); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); if (!_title.isEmpty() || !_description.isEmpty()) { if (!_parent->Has() && !_parent->Has()) { skipy += st::msgPadding.top(); } } width -= st::mediaPadding.left() + st::mediaPadding.right(); int32 textw = _width - st::msgPadding.left() - st::msgPadding.right(); p.setPen(st::black); if (!_title.isEmpty()) { _title.drawLeftElided(p, skipx + st::msgPadding.left(), skipy, textw, _width, 2, style::al_left, 0, -1, 0, false, selection); skipy += qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height); } if (!_description.isEmpty()) { _description.drawLeftElided(p, skipx + st::msgPadding.left(), skipy, textw, _width, 3, style::al_left, 0, -1, 0, false, toDescriptionSelection(selection)); skipy += qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height); } if (!_title.isEmpty() || !_description.isEmpty()) { skipy += st::webPagePhotoSkip; } height -= skipy + st::mediaPadding.bottom(); } else { App::roundShadow(p, 0, 0, width, height, selected ? st::msgInShadowSelected : st::msgInShadow, selected ? InSelectedShadowCorners : InShadowCorners); } _data->load(); QPixmap toDraw; if (_data && !_data->thumb->isNull()) { int32 w = _data->thumb->width(), h = _data->thumb->height(); QPixmap pix; if (width * h == height * w || (w == fullWidth() && h == fullHeight())) { pix = _data->thumb->pixSingle(width, height, width, height); } else if (width * h > height * w) { int32 nw = height * w / h; pix = _data->thumb->pixSingle(nw, height, width, height); } else { int32 nh = width * h / w; pix = _data->thumb->pixSingle(width, nh, width, height); } p.drawPixmap(QPoint(skipx, skipy), pix); } else { App::roundRect(p, skipx, skipy, width, height, st::white, MessageInCorners); } if (selected) { App::roundRect(p, skipx, skipy, width, height, textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } if (_parent->getMedia() == this) { int32 fullRight = skipx + width, fullBottom = _height - (skipx ? st::mediaPadding.bottom() : 0); _parent->drawInfo(p, fullRight, fullBottom, skipx * 2 + width, selected, InfoDisplayOverImage); } } HistoryTextState HistoryLocation::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; auto symbolAdd = 0; if (_width < st::msgPadding.left() + st::msgPadding.right() + 1) return result; int32 skipx = 0, skipy = 0, width = _width, height = _height; bool bubble = _parent->hasBubble(); if (bubble) { skipx = st::mediaPadding.left(); skipy = st::mediaPadding.top(); if (!_title.isEmpty() || !_description.isEmpty()) { if (!_parent->Has() && !_parent->Has()) { skipy += st::msgPadding.top(); } } width -= st::mediaPadding.left() + st::mediaPadding.right(); int32 textw = _width - st::msgPadding.left() - st::msgPadding.right(); if (!_title.isEmpty()) { auto titleh = qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height); if (y >= skipy && y < skipy + titleh) { result = _title.getStateLeft(x - skipx - st::msgPadding.left(), y - skipy, textw, _width, request.forText()); return result; } else if (y >= skipy + titleh) { symbolAdd += _title.length(); } skipy += titleh; } if (!_description.isEmpty()) { auto descriptionh = qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height); if (y >= skipy && y < skipy + descriptionh) { result = _description.getStateLeft(x - skipx - st::msgPadding.left(), y - skipy, textw, _width, request.forText()); } else if (y >= skipy + descriptionh) { symbolAdd += _description.length(); } skipy += descriptionh; } if (!_title.isEmpty() || !_description.isEmpty()) { skipy += st::webPagePhotoSkip; } height -= skipy + st::mediaPadding.bottom(); } if (x >= skipx && y >= skipy && x < skipx + width && y < skipy + height && _data) { result.link = _link; int32 fullRight = skipx + width, fullBottom = _height - (skipx ? st::mediaPadding.bottom() : 0); bool inDate = _parent->pointInTime(fullRight, fullBottom, x, y, InfoDisplayOverImage); if (inDate) { result.cursor = HistoryInDateCursorState; } } result.symbol += symbolAdd; return result; } TextSelection HistoryLocation::adjustSelection(TextSelection selection, TextSelectType type) const { if (_description.isEmpty() || selection.to <= _title.length()) { return _title.adjustSelection(selection, type); } auto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type); if (selection.from >= _title.length()) { return fromDescriptionSelection(descriptionSelection); } auto titleSelection = _title.adjustSelection(selection, type); return { titleSelection.from, fromDescriptionSelection(descriptionSelection).to }; } QString HistoryLocation::inDialogsText() const { return _title.isEmpty() ? lang(lng_maps_point) : _title.original(AllTextSelection); } QString HistoryLocation::selectedText(TextSelection selection) const { if (selection == FullSelection) { auto result = qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"); auto info = selectedText(AllTextSelection); if (!info.isEmpty()) result.append(info).append('\n'); return result + _link->text(); } auto titleResult = _title.original(selection); auto descriptionResult = _description.original(toDescriptionSelection(selection)); if (titleResult.isEmpty()) { return descriptionResult; } else if (descriptionResult.isEmpty()) { return titleResult; } return titleResult + '\n' + descriptionResult; } int32 HistoryLocation::fullWidth() const { return st::locationSize.width(); } int32 HistoryLocation::fullHeight() const { return st::locationSize.height(); } void ViaInlineBotClickHandler::onClickImpl() const { App::insertBotCommand('@' + _bot->username); } void HistoryMessageVia::create(int32 userId) { _bot = App::user(peerFromUser(userId)); _maxWidth = st::msgServiceNameFont->width(lng_inline_bot_via(lt_inline_bot, '@' + _bot->username)); _lnk.reset(new ViaInlineBotClickHandler(_bot)); } void HistoryMessageVia::resize(int32 availw) const { if (availw < 0) { _text = QString(); _width = 0; } else { _text = lng_inline_bot_via(lt_inline_bot, '@' + _bot->username); if (availw < _maxWidth) { _text = st::msgServiceNameFont->elided(_text, availw); _width = st::msgServiceNameFont->width(_text); } else if (_width < _maxWidth) { _width = _maxWidth; } } } void HistoryMessageSigned::create(UserData *from, const QDateTime &date) { QString time = qsl(", ") + date.toString(cTimeFormat()), name = App::peerName(from); int32 timew = st::msgDateFont->width(time), namew = st::msgDateFont->width(name); if (timew + namew > st::maxSignatureSize) { name = st::msgDateFont->elided(from->firstName, st::maxSignatureSize - timew); } _signature.setText(st::msgDateFont, name + time, _textNameOptions); } int HistoryMessageSigned::maxWidth() const { return _signature.maxWidth(); } void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { QString text; if (_authorOriginal != _fromOriginal) { text = lng_forwarded_signed(lt_channel, App::peerName(_authorOriginal), lt_user, App::peerName(_fromOriginal)); } else { text = App::peerName(_authorOriginal); } if (via) { if (_authorOriginal->isChannel()) { text = lng_forwarded_channel_via(lt_channel, textcmdLink(1, text), lt_inline_bot, textcmdLink(2, '@' + via->_bot->username)); } else { text = lng_forwarded_via(lt_user, textcmdLink(1, text), lt_inline_bot, textcmdLink(2, '@' + via->_bot->username)); } } else { if (_authorOriginal->isChannel()) { text = lng_forwarded_channel(lt_channel, textcmdLink(1, text)); } else { text = lng_forwarded(lt_user, textcmdLink(1, text)); } } TextParseOptions opts = { TextParseRichText, 0, 0, Qt::LayoutDirectionAuto }; textstyleSet(&st::inFwdTextStyle); _text.setText(st::msgServiceNameFont, text, opts); textstyleRestore(); _text.setLink(1, (_originalId && _authorOriginal->isChannel()) ? ClickHandlerPtr(new GoToMessageClickHandler(_authorOriginal->id, _originalId)) : _authorOriginal->openLink()); if (via) { _text.setLink(2, via->_lnk); } } bool HistoryMessageReply::updateData(HistoryMessage *holder, bool force) { if (!force) { if (replyToMsg || !replyToMsgId) { return true; } } if (!replyToMsg) { replyToMsg = App::histItemById(holder->channelId(), replyToMsgId); if (replyToMsg) { App::historyRegDependency(holder, replyToMsg); } } if (replyToMsg) { replyToText.setText(st::msgFont, replyToMsg->inReplyText(), _textDlgOptions); updateName(); replyToLnk.reset(new GoToMessageClickHandler(replyToMsg->history()->peer->id, replyToMsg->id)); if (!replyToMsg->Has()) { if (UserData *bot = replyToMsg->viaBot()) { _replyToVia.reset(new HistoryMessageVia()); _replyToVia->create(peerToUser(bot->id)); } } } else if (force) { replyToMsgId = 0; } if (force) { holder->setPendingInitDimensions(); } return (replyToMsg || !replyToMsgId); } void HistoryMessageReply::clearData(HistoryMessage *holder) { _replyToVia = nullptr; if (replyToMsg) { App::historyUnregDependency(holder, replyToMsg); replyToMsg = nullptr; } replyToMsgId = 0; } void HistoryMessageReply::checkNameUpdate() const { if (replyToMsg && replyToMsg->author()->nameVersion > replyToVersion) { updateName(); } } void HistoryMessageReply::updateName() const { if (replyToMsg) { QString name = (_replyToVia && replyToMsg->author()->isUser()) ? replyToMsg->author()->asUser()->firstName : App::peerName(replyToMsg->author()); replyToName.setText(st::msgServiceNameFont, name, _textNameOptions); replyToVersion = replyToMsg->author()->nameVersion; bool hasPreview = replyToMsg->getMedia() ? replyToMsg->getMedia()->hasReplyPreview() : false; int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; int32 w = replyToName.maxWidth(); if (_replyToVia) { w += st::msgServiceFont->spacew + _replyToVia->_maxWidth; } _maxReplyWidth = previewSkip + qMax(w, qMin(replyToText.maxWidth(), int32(st::maxSignatureSize))); } else { _maxReplyWidth = st::msgDateFont->width(lang(replyToMsgId ? lng_profile_loading : lng_deleted_message)); } _maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + _maxReplyWidth + st::msgReplyPadding.right(); } void HistoryMessageReply::resize(int width) const { if (_replyToVia) { bool hasPreview = replyToMsg->getMedia() ? replyToMsg->getMedia()->hasReplyPreview() : false; int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; _replyToVia->resize(width - st::msgReplyBarSkip - previewSkip - replyToName.maxWidth() - st::msgServiceFont->spacew); } } void HistoryMessageReply::itemRemoved(HistoryMessage *holder, HistoryItem *removed) { if (replyToMsg == removed) { clearData(holder); holder->setPendingInitDimensions(); } } void HistoryMessageReply::paint(Painter &p, const HistoryItem *holder, int x, int y, int w, PaintFlags flags) const { bool selected = (flags & PaintSelected), outbg = holder->hasOutLayout(); style::color bar; if (flags & PaintInBubble) { bar = ((flags & PaintSelected) ? (outbg ? st::msgOutReplyBarSelColor : st::msgInReplyBarSelColor) : (outbg ? st::msgOutReplyBarColor : st::msgInReplyBarColor)); } else { bar = st::white; } QRect rbar(rtlrect(x + st::msgReplyBarPos.x(), y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.width(), st::msgReplyBarSize.height(), w + 2 * x)); p.fillRect(rbar, bar); if (w > st::msgReplyBarSkip) { if (replyToMsg) { bool hasPreview = replyToMsg->getMedia() ? replyToMsg->getMedia()->hasReplyPreview() : false; int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; if (hasPreview) { ImagePtr replyPreview = replyToMsg->getMedia()->replyPreview(); if (!replyPreview->isNull()) { QRect to(rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), w + 2 * x)); p.drawPixmap(to.x(), to.y(), replyPreview->pixSingle(replyPreview->width() / cIntRetinaFactor(), replyPreview->height() / cIntRetinaFactor(), to.width(), to.height())); if (selected) { App::roundRect(p, to, textstyleCurrent()->selectOverlay, SelectedOverlayCorners); } } } if (w > st::msgReplyBarSkip + previewSkip) { if (flags & PaintInBubble) { p.setPen(selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg)); } else { p.setPen(st::white); } replyToName.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top(), w - st::msgReplyBarSkip - previewSkip, w + 2 * x); if (_replyToVia && w > st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew) { p.setFont(st::msgServiceFont); p.drawText(x + st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew, y + st::msgReplyPadding.top() + st::msgServiceFont->ascent, _replyToVia->_text); } HistoryMessage *replyToAsMsg = replyToMsg->toHistoryMessage(); if (!(flags & PaintInBubble)) { } else if ((replyToAsMsg && replyToAsMsg->emptyText()) || replyToMsg->serviceMsg()) { style::color date(outbg ? (selected ? st::msgOutDateFgSelected : st::msgOutDateFg) : (selected ? st::msgInDateFgSelected : st::msgInDateFg)); p.setPen(date); } else { p.setPen(st::msgColor); } replyToText.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top() + st::msgServiceNameFont->height, w - st::msgReplyBarSkip - previewSkip, w + 2 * x); } } else { p.setFont(st::msgDateFont); style::color date(outbg ? (selected ? st::msgOutDateFgSelected : st::msgOutDateFg) : (selected ? st::msgInDateFgSelected : st::msgInDateFg)); p.setPen((flags & PaintInBubble) ? date : st::white); p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(lang(replyToMsgId ? lng_profile_loading : lng_deleted_message), w - st::msgReplyBarSkip)); } } } void HistoryMessage::KeyboardStyle::startPaint(Painter &p) const { p.setPen(st::msgServiceColor); } style::font HistoryMessage::KeyboardStyle::textFont() const { return st::msgServiceFont; } void HistoryMessage::KeyboardStyle::repaint(const HistoryItem *item) const { Ui::repaintHistoryItem(item); } void HistoryMessage::KeyboardStyle::paintButtonBg(Painter &p, const QRect &rect, bool down, float64 howMuchOver) const { App::roundRect(p, rect, App::msgServiceBg(), ServiceCorners); if (down) { howMuchOver = 1.; } if (howMuchOver > 0) { float64 o = p.opacity(); p.setOpacity(o * (howMuchOver * st::msgBotKbOverOpacity)); App::roundRect(p, rect, st::white, WhiteCorners); p.setOpacity(o); } } void HistoryMessage::KeyboardStyle::paintButtonIcon(Painter &p, const QRect &rect, HistoryMessageReplyMarkup::Button::Type type) const { using Button = HistoryMessageReplyMarkup::Button; style::sprite sprite; switch (type) { case Button::Url: sprite = st::msgBotKbUrlIcon; break; // case Button::RequestPhone: sprite = st::msgBotKbRequestPhoneIcon; break; // case Button::RequestLocation: sprite = st::msgBotKbRequestLocationIcon; break; case Button::SwitchInline: sprite = st::msgBotKbSwitchPmIcon; break; } if (!sprite.isEmpty()) { p.drawSprite(rect.x() + rect.width() - sprite.pxWidth() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, sprite); } } void HistoryMessage::KeyboardStyle::paintButtonLoading(Painter &p, const QRect &rect) const { style::sprite sprite = st::msgInvSendingImg; p.drawSprite(rect.x() + rect.width() - sprite.pxWidth() - st::msgBotKbIconPadding, rect.y() + rect.height() - sprite.pxHeight() - st::msgBotKbIconPadding, sprite); } int HistoryMessage::KeyboardStyle::minButtonWidth(HistoryMessageReplyMarkup::Button::Type type) const { using Button = HistoryMessageReplyMarkup::Button; int result = 2 * buttonPadding(), iconWidth = 0; switch (type) { case Button::Url: iconWidth = st::msgBotKbUrlIcon.pxWidth(); break; //case Button::RequestPhone: iconWidth = st::msgBotKbRequestPhoneIcon.pxWidth(); break; //case Button::RequestLocation: iconWidth = st::msgBotKbRequestLocationIcon.pxWidth(); break; case Button::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.pxWidth(); break; case Button::Callback: iconWidth = st::msgInvSendingImg.pxWidth(); break; } if (iconWidth > 0) { result = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding)); } return result; } HistoryMessage::HistoryMessage(History *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; if (msg.has_fwd_from() && msg.vfwd_from.type() == mtpc_messageFwdHeader) { const auto &f(msg.vfwd_from.c_messageFwdHeader()); if (f.has_from_id() || f.has_channel_id()) { config.authorIdOriginal = f.has_channel_id() ? peerFromChannel(f.vchannel_id) : peerFromUser(f.vfrom_id); config.fromIdOriginal = f.has_from_id() ? peerFromUser(f.vfrom_id) : peerFromChannel(f.vchannel_id); if (f.has_channel_post()) config.originalId = f.vchannel_post.v; } } if (msg.has_reply_to_msg_id()) config.replyTo = msg.vreply_to_msg_id.v; if (msg.has_via_bot_id()) config.viaBotId = msg.vvia_bot_id.v; if (msg.has_views()) config.viewsCount = msg.vviews.v; if (msg.has_reply_markup()) config.markup = &msg.vreply_markup; createComponents(config); QString text(textClean(qs(msg.vmessage))); initMedia(msg.has_media() ? (&msg.vmedia) : 0, text); setText(text, msg.has_entities() ? entitiesFromMTP(msg.ventities.c_vector().v) : EntitiesInText()); } HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) : HistoryItem(history, id, newForwardedFlags(history->peer, from, fwd) | flags, date, from) { CreateConfig config; config.authorIdOriginal = fwd->authorOriginal()->id; config.fromIdOriginal = fwd->fromOriginal()->id; if (fwd->authorOriginal()->isChannel()) { config.originalId = fwd->id; } UserData *fwdViaBot = fwd->viaBot(); if (fwdViaBot) config.viaBotId = peerToUser(fwdViaBot->id); int fwdViewsCount = fwd->viewsCount(); if (fwdViewsCount > 0) { config.viewsCount = fwdViewsCount; } else if (isPost()) { config.viewsCount = 1; } createComponents(config); if (HistoryMedia *mediaOriginal = fwd->getMedia()) { _media.reset(mediaOriginal->clone(this)); } setText(fwd->originalText(), fwd->originalEntities()); } HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const QString &msg, const EntitiesInText &entities) : HistoryItem(history, id, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, MTPnullMarkup); setText(msg, entities); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, markup); initMediaFromDocument(doc, caption); setText(QString(), EntitiesInText()); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) : HistoryItem(history, msgId, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, markup); _media.reset(new HistoryPhoto(this, photo, caption)); setText(QString(), EntitiesInText()); } void HistoryMessage::createComponentsHelper(MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, const MTPReplyMarkup &markup) { CreateConfig config; if (flags & MTPDmessage::Flag::f_via_bot_id) config.viaBotId = viaBotId; if (flags & MTPDmessage::Flag::f_reply_to_msg_id) config.replyTo = replyTo; if (flags & MTPDmessage::Flag::f_reply_markup) config.markup = &markup; if (isPost()) config.viewsCount = 1; createComponents(config); } void HistoryMessage::createComponents(const CreateConfig &config) { uint64 mask = 0; if (config.replyTo) { mask |= HistoryMessageReply::Bit(); } if (config.viaBotId) { mask |= HistoryMessageVia::Bit(); } if (config.viewsCount >= 0) { mask |= HistoryMessageViews::Bit(); } if (isPost() && _from->isUser()) { mask |= HistoryMessageSigned::Bit(); } if (config.authorIdOriginal && config.fromIdOriginal) { mask |= HistoryMessageForwarded::Bit(); } if (config.markup) { // optimization: don't create markup component for the case // MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag if (config.markup->type() != mtpc_replyKeyboardHide || config.markup->c_replyKeyboardHide().vflags.v != 0) { mask |= HistoryMessageReplyMarkup::Bit(); } } UpdateComponents(mask); if (auto reply = Get()) { reply->replyToMsgId = config.replyTo; if (!reply->updateData(this) && App::api()) { App::api()->requestMessageData(history()->peer->asChannel(), reply->replyToMsgId, new HistoryDependentItemCallback(fullId())); } } if (auto via = Get()) { via->create(config.viaBotId); } if (auto views = Get()) { views->_views = config.viewsCount; } if (auto msgsigned = Get()) { msgsigned->create(_from->asUser(), date); } if (auto fwd = Get()) { fwd->_authorOriginal = App::peer(config.authorIdOriginal); fwd->_fromOriginal = App::peer(config.fromIdOriginal); fwd->_originalId = config.originalId; } if (auto markup = Get()) { markup->create(*config.markup); if (markup->flags & MTPDreplyKeyboardMarkup_ClientFlag::f_has_switch_inline_button) { _flags |= MTPDmessage_ClientFlag::f_has_switch_inline_button; } } initTime(); } QString formatViewsCount(int32 views) { if (views > 999999) { views /= 100000; if (views % 10) { return QString::number(views / 10) + '.' + QString::number(views % 10) + 'M'; } return QString::number(views / 10) + 'M'; } else if (views > 9999) { views /= 100; if (views % 10) { return QString::number(views / 10) + '.' + QString::number(views % 10) + 'K'; } return QString::number(views / 10) + 'K'; } else if (views > 0) { return QString::number(views); } return qsl("1"); } void HistoryMessage::initTime() { if (auto msgsigned = Get()) { _timeWidth = msgsigned->maxWidth(); } else { _timeText = date.toString(cTimeFormat()); _timeWidth = st::msgDateFont->width(_timeText); } if (auto views = Get()) { views->_viewsText = (views->_views >= 0) ? formatViewsCount(views->_views) : QString(); views->_viewsWidth = views->_viewsText.isEmpty() ? 0 : st::msgDateFont->width(views->_viewsText); } } void HistoryMessage::initMedia(const MTPMessageMedia *media, QString ¤tText) { switch (media ? media->type() : mtpc_messageMediaEmpty) { case mtpc_messageMediaContact: { const auto &d(media->c_messageMediaContact()); _media.reset(new HistoryContact(this, d.vuser_id.v, qs(d.vfirst_name), qs(d.vlast_name), qs(d.vphone_number))); } break; case mtpc_messageMediaGeo: { const auto &point(media->c_messageMediaGeo().vgeo); if (point.type() == mtpc_geoPoint) { _media.reset(new HistoryLocation(this, LocationCoords(point.c_geoPoint()))); } } break; case mtpc_messageMediaVenue: { const auto &d(media->c_messageMediaVenue()); if (d.vgeo.type() == mtpc_geoPoint) { _media.reset(new HistoryLocation(this, LocationCoords(d.vgeo.c_geoPoint()), qs(d.vtitle), qs(d.vaddress))); } } break; case mtpc_messageMediaPhoto: { const auto &photo(media->c_messageMediaPhoto()); if (photo.vphoto.type() == mtpc_photo) { _media.reset(new HistoryPhoto(this, App::feedPhoto(photo.vphoto.c_photo()), qs(photo.vcaption))); } } break; case mtpc_messageMediaDocument: { const auto &document(media->c_messageMediaDocument().vdocument); if (document.type() == mtpc_document) { return initMediaFromDocument(App::feedDocument(document), qs(media->c_messageMediaDocument().vcaption)); } } break; case mtpc_messageMediaWebPage: { const auto &d(media->c_messageMediaWebPage().vwebpage); switch (d.type()) { case mtpc_webPageEmpty: break; case mtpc_webPagePending: { _media.reset(new HistoryWebPage(this, App::feedWebPage(d.c_webPagePending()))); } break; case mtpc_webPage: { _media.reset(new HistoryWebPage(this, App::feedWebPage(d.c_webPage()))); } break; } } break; }; } void HistoryMessage::initMediaFromDocument(DocumentData *doc, const QString &caption) { if (doc->sticker()) { _media.reset(new HistorySticker(this, doc)); } else if (doc->isAnimation()) { _media.reset(new HistoryGif(this, doc, caption)); } else if (doc->isVideo()) { _media.reset(new HistoryVideo(this, doc, caption)); } else { _media.reset(new HistoryDocument(this, doc, caption)); } } int32 HistoryMessage::plainMaxWidth() const { return st::msgPadding.left() + _text.maxWidth() + st::msgPadding.right(); } void HistoryMessage::initDimensions() { if (drawBubble()) { auto fwd = Get(); auto via = Get(); if (fwd) { fwd->create(via); } if (_media) { _media->initDimensions(); if (_media->isDisplayed()) { if (_text.hasSkipBlock()) { _text.removeSkipBlock(); _textWidth = 0; _textHeight = 0; } } else if (!_text.hasSkipBlock()) { _text.setSkipBlock(skipBlockWidth(), skipBlockHeight()); _textWidth = 0; _textHeight = 0; } } _maxw = plainMaxWidth(); if (_text.isEmpty()) { _minh = 0; } else { _minh = st::msgPadding.top() + _text.minHeight() + st::msgPadding.bottom(); } if (_media && _media->isDisplayed()) { int32 maxw = _media->maxWidth(); if (maxw > _maxw) _maxw = maxw; _minh += _media->minHeight(); } if (!_media) { if (displayFromName()) { int32 namew = st::msgPadding.left() + author()->nameText.maxWidth() + st::msgPadding.right(); if (via && !fwd) { namew += st::msgServiceFont->spacew + via->_maxWidth; } if (namew > _maxw) _maxw = namew; } else if (via && !fwd) { if (st::msgPadding.left() + via->_maxWidth + st::msgPadding.right() > _maxw) { _maxw = st::msgPadding.left() + via->_maxWidth + st::msgPadding.right(); } } if (fwd) { int32 _namew = st::msgPadding.left() + fwd->_text.maxWidth() + st::msgPadding.right(); if (via) { _namew += st::msgServiceFont->spacew + via->_maxWidth; } if (_namew > _maxw) _maxw = _namew; } } } else { _media->initDimensions(); _maxw = _media->maxWidth(); _minh = _media->minHeight(); } if (auto reply = Get()) { reply->updateName(); if (!_text.isEmpty()) { int replyw = st::msgPadding.left() + reply->_maxReplyWidth - st::msgReplyPadding.left() - st::msgReplyPadding.right() + st::msgPadding.right(); if (reply->_replyToVia) { replyw += st::msgServiceFont->spacew + reply->_replyToVia->_maxWidth; } if (replyw > _maxw) _maxw = replyw; } } if (HistoryMessageReplyMarkup *markup = inlineReplyMarkup()) { if (!markup->inlineKeyboard) { markup->inlineKeyboard.reset(new ReplyKeyboard(this, std_::make_unique(st::msgBotKbButton))); } // if we have a text bubble we can resize it to fit the keyboard // but if we have only media we don't do that if (!_text.isEmpty()) { _maxw = qMax(_maxw, markup->inlineKeyboard->naturalWidth()); } } } void HistoryMessage::countPositionAndSize(int32 &left, int32 &width) const { int32 maxwidth = qMin(int(st::msgMaxWidth), _maxw), hwidth = _history->width; if (_media && _media->currentWidth() < maxwidth) { maxwidth = qMax(_media->currentWidth(), qMin(maxwidth, plainMaxWidth())); } left = (!isPost() && out() && !Adaptive::Wide()) ? st::msgMargin.right() : st::msgMargin.left(); if (hasFromPhoto()) { left += st::msgPhotoSkip; // } else if (!Adaptive::Wide() && !out() && !fromChannel() && st::msgPhotoSkip - (hmaxwidth - hwidth) > 0) { // left += st::msgPhotoSkip - (hmaxwidth - hwidth); } width = hwidth - st::msgMargin.left() - st::msgMargin.right(); if (width > maxwidth) { if (!isPost() && out() && !Adaptive::Wide()) { left += width - maxwidth; } width = maxwidth; } } void HistoryMessage::fromNameUpdated(int32 width) const { _authorNameVersion = author()->nameVersion; if (!Has()) { if (auto via = Get()) { via->resize(width - st::msgPadding.left() - st::msgPadding.right() - author()->nameText.maxWidth() - st::msgServiceFont->spacew); } } } void HistoryMessage::applyEdition(const MTPDmessage &message) { int keyboardTop = -1; if (!pendingResize()) { if (auto keyboard = inlineReplyKeyboard()) { int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); keyboardTop = _height - h + st::msgBotKbButton.margin - marginBottom(); } } EntitiesInText entities; if (message.has_entities()) { entities = entitiesFromMTP(message.ventities.c_vector().v); } setText(qs(message.vmessage), entities); setMedia(message.has_media() ? (&message.vmedia) : nullptr); setReplyMarkup(message.has_reply_markup() ? (&message.vreply_markup) : nullptr); setViewsCount(message.has_views() ? message.vviews.v : -1); setPendingInitDimensions(); if (App::main()) { App::main()->dlgUpdated(history(), id); } // invalidate cache for drawInDialog if (history()->textCachedFor == this) { history()->textCachedFor = nullptr; } if (keyboardTop >= 0) { if (auto keyboard = Get()) { keyboard->oldTop = keyboardTop; } } } void HistoryMessage::updateMedia(const MTPMessageMedia *media) { if (_flags & MTPDmessage_ClientFlag::f_from_inline_bot) { bool needReSet = true; if (media && _media) { needReSet = _media->needReSetInlineResultMedia(*media); } if (needReSet) { setMedia(media); } _flags &= ~MTPDmessage_ClientFlag::f_from_inline_bot; } else if (media && _media && _media->type() != MediaTypeWebPage) { _media->updateSentMedia(*media); } else { setMedia(media); } setPendingInitDimensions(); } int32 HistoryMessage::addToOverview(AddToOverviewMethod method) { if (!indexInOverview()) return 0; int32 result = 0; if (HistoryMedia *media = getMedia()) { MediaOverviewType type = mediaToOverviewType(media); if (type != OverviewCount) { if (history()->addToOverview(type, id, method)) { result |= (1 << type); } } } if (hasTextLinks()) { if (history()->addToOverview(OverviewLinks, id, method)) { result |= (1 << OverviewLinks); } } return result; } void HistoryMessage::eraseFromOverview() { if (HistoryMedia *media = getMedia()) { MediaOverviewType type = mediaToOverviewType(media); if (type != OverviewCount) { history()->eraseFromOverview(type, id); } } if (hasTextLinks()) { history()->eraseFromOverview(OverviewLinks, id); } } QString HistoryMessage::selectedText(TextSelection selection) const { QString result, textResult, mediaResult; if (selection == FullSelection) { textResult = _text.original(AllTextSelection, Text::ExpandLinksAll); } else { textResult = _text.original(selection, Text::ExpandLinksAll); } if (_media) { mediaResult = _media->selectedText(toMediaSelection(selection)); } if (textResult.isEmpty()) { result = mediaResult; } else if (mediaResult.isEmpty()) { result = textResult; } else { result = textResult + qstr("\n\n") + mediaResult; } if (auto fwd = Get()) { if (selection == FullSelection) { QString fwdinfo = fwd->_text.original(AllTextSelection, Text::ExpandLinksAll), wrapped; wrapped.reserve(fwdinfo.size() + 4 + result.size()); wrapped.append('[').append(fwdinfo).append(qsl("]\n")).append(result); result = wrapped; } } if (auto reply = Get()) { if (selection == FullSelection && reply->replyToMsg) { QString wrapped; wrapped.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.size()); wrapped.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n")).append(result); result = wrapped; } } return result; } QString HistoryMessage::inDialogsText() const { return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.original(AllTextSelection, Text::ExpandLinksNone); } HistoryMedia *HistoryMessage::getMedia() const { return _media.data(); } void HistoryMessage::setMedia(const MTPMessageMedia *media) { if (!_media && (!media || media->type() == mtpc_messageMediaEmpty)) return; bool mediaWasDisplayed = false; if (_media) { mediaWasDisplayed = _media->isDisplayed(); _media.clear(); } QString t; initMedia(media, t); if (_media && _media->isDisplayed() && !mediaWasDisplayed) { _text.removeSkipBlock(); _textWidth = 0; _textHeight = 0; } else if (mediaWasDisplayed && (!_media || !_media->isDisplayed())) { _text.setSkipBlock(skipBlockWidth(), skipBlockHeight()); _textWidth = 0; _textHeight = 0; } } void HistoryMessage::setText(const QString &text, const EntitiesInText &entities) { textstyleSet(&((out() && !isPost()) ? st::outTextStyle : st::inTextStyle)); if (_media && _media->isDisplayed()) { _text.setMarkedText(st::msgFont, text, entities, itemTextOptions(this)); } else { _text.setMarkedText(st::msgFont, text + skipBlock(), entities, itemTextOptions(this)); } textstyleRestore(); for (int32 i = 0, l = entities.size(); i != l; ++i) { if (entities.at(i).type == EntityInTextUrl || entities.at(i).type == EntityInTextCustomUrl || entities.at(i).type == EntityInTextEmail) { _flags |= MTPDmessage_ClientFlag::f_has_text_links; break; } } _textWidth = 0; _textHeight = 0; } void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) { if (!markup) { if (_flags & MTPDmessage::Flag::f_reply_markup) { _flags &= ~MTPDmessage::Flag::f_reply_markup; if (Has()) { RemoveComponents(HistoryMessageReplyMarkup::Bit()); } setPendingInitDimensions(); Notify::replyMarkupUpdated(this); } return; } // optimization: don't create markup component for the case // MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag if (markup->type() == mtpc_replyKeyboardHide && markup->c_replyKeyboardHide().vflags.v == 0) { bool changed = false; if (Has()) { RemoveComponents(HistoryMessageReplyMarkup::Bit()); changed = true; } if (!(_flags & MTPDmessage::Flag::f_reply_markup)) { _flags |= MTPDmessage::Flag::f_reply_markup; changed = true; } if (changed) { setPendingInitDimensions(); Notify::replyMarkupUpdated(this); } } else { if (!(_flags & MTPDmessage::Flag::f_reply_markup)) { _flags |= MTPDmessage::Flag::f_reply_markup; } if (!Has()) { AddComponents(HistoryMessageReplyMarkup::Bit()); } Get()->create(*markup); setPendingInitDimensions(); Notify::replyMarkupUpdated(this); } } QString HistoryMessage::originalText() const { return emptyText() ? QString() : _text.original(); } EntitiesInText HistoryMessage::originalEntities() const { return emptyText() ? EntitiesInText() : _text.originalEntities(); } bool HistoryMessage::textHasLinks() { return emptyText() ? false : _text.hasLinks(); } void HistoryMessage::drawInfo(Painter &p, int32 right, int32 bottom, int32 width, bool selected, InfoDisplayType type) const { p.setFont(st::msgDateFont); bool outbg = out() && !isPost(); bool invertedsprites = (type == InfoDisplayOverImage || type == InfoDisplayOverBackground); int32 infoRight = right, infoBottom = bottom; switch (type) { case InfoDisplayDefault: infoRight -= st::msgPadding.right() - st::msgDateDelta.x(); infoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y(); p.setPen(selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg)); break; case InfoDisplayOverImage: infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); p.setPen(st::msgDateImgColor); break; case InfoDisplayOverBackground: infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); p.setPen(st::msgServiceColor); break; } int32 infoW = HistoryMessage::infoWidth(); if (rtl()) infoRight = width - infoRight + infoW; int32 dateX = infoRight - infoW; int32 dateY = infoBottom - st::msgDateFont->height; if (type == InfoDisplayOverImage) { int32 dateW = infoW + 2 * st::msgDateImgPadding.x(), dateH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y(); App::roundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? DateSelectedCorners : DateCorners); } else if (type == InfoDisplayOverBackground) { int32 dateW = infoW + 2 * st::msgDateImgPadding.x(), dateH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y(); App::roundRect(p, dateX - st::msgDateImgPadding.x(), dateY - st::msgDateImgPadding.y(), dateW, dateH, selected ? App::msgServiceSelectBg() : App::msgServiceBg(), selected ? ServiceSelectedCorners : ServiceCorners); } dateX += HistoryMessage::timeLeft(); if (auto msgsigned = Get()) { msgsigned->_signature.drawElided(p, dateX, dateY, _timeWidth); } else { p.drawText(dateX, dateY + st::msgDateFont->ascent, _timeText); } QPoint iconPos; const QRect *iconRect = 0; if (auto views = Get()) { iconPos = QPoint(infoRight - infoW + st::msgViewsPos.x(), infoBottom - st::msgViewsImg.pxHeight() + st::msgViewsPos.y()); if (id > 0) { if (outbg) { iconRect = &(invertedsprites ? st::msgInvViewsImg : (selected ? st::msgSelectOutViewsImg : st::msgOutViewsImg)); } else { iconRect = &(invertedsprites ? st::msgInvViewsImg : (selected ? st::msgSelectViewsImg : st::msgViewsImg)); } p.drawText(iconPos.x() + st::msgViewsImg.pxWidth() + st::msgDateCheckSpace, infoBottom - st::msgDateFont->descent, views->_viewsText); } else { iconPos.setX(iconPos.x() + st::msgDateViewsSpace + views->_viewsWidth); if (outbg) { iconRect = &(invertedsprites ? st::msgInvSendingViewsImg : st::msgSendingOutViewsImg); } else { iconRect = &(invertedsprites ? st::msgInvSendingViewsImg : st::msgSendingViewsImg); } } p.drawPixmap(iconPos, App::sprite(), *iconRect); } else if (id < 0 && history()->peer->isSelf()) { iconPos = QPoint(infoRight - infoW, infoBottom - st::msgViewsImg.pxHeight() + st::msgViewsPos.y()); iconRect = &(invertedsprites ? st::msgInvSendingViewsImg : st::msgSendingViewsImg); p.drawPixmap(iconPos, App::sprite(), *iconRect); } if (outbg) { iconPos = QPoint(infoRight - st::msgCheckImg.pxWidth() + st::msgCheckPos.x(), infoBottom - st::msgCheckImg.pxHeight() + st::msgCheckPos.y()); if (id > 0) { if (unread()) { iconRect = &(invertedsprites ? st::msgInvCheckImg : (selected ? st::msgSelectCheckImg : st::msgCheckImg)); } else { iconRect = &(invertedsprites ? st::msgInvDblCheckImg : (selected ? st::msgSelectDblCheckImg : st::msgDblCheckImg)); } } else { iconRect = &(invertedsprites ? st::msgInvSendingImg : st::msgSendingImg); } p.drawPixmap(iconPos, App::sprite(), *iconRect); } } void HistoryMessage::setViewsCount(int32 count) { auto views = Get(); if (!views || views->_views == count || (count >= 0 && views->_views > count)) return; int32 was = views->_viewsWidth; views->_views = count; views->_viewsText = (views->_views >= 0) ? formatViewsCount(views->_views) : QString(); views->_viewsWidth = views->_viewsText.isEmpty() ? 0 : st::msgDateFont->width(views->_viewsText); if (was == views->_viewsWidth) { Ui::repaintHistoryItem(this); } else { if (_text.hasSkipBlock()) { _text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight()); _textWidth = 0; _textHeight = 0; } setPendingInitDimensions(); } } void HistoryMessage::setId(MsgId newId) { bool wasPositive = (id > 0), positive = (newId > 0); HistoryItem::setId(newId); if (wasPositive == positive) { Ui::repaintHistoryItem(this); } else { if (_text.hasSkipBlock()) { _text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight()); _textWidth = 0; _textHeight = 0; } setPendingInitDimensions(); } } void HistoryMessage::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { bool outbg = out() && !isPost(), bubble = drawBubble(), selected = (selection == FullSelection); int left = 0, width = 0, height = _height; countPositionAndSize(left, width); if (width < 1) return; int dateh = 0, unreadbarh = 0; if (auto date = Get()) { dateh = date->height(); if (r.intersects(QRect(0, 0, _history->width, dateh))) { date->paint(p, 0, _history->width); } } if (auto unreadbar = Get()) { unreadbarh = unreadbar->height(); if (r.intersects(QRect(0, dateh, _history->width, unreadbarh))) { p.translate(0, dateh); unreadbar->paint(p, 0, _history->width); p.translate(0, -dateh); } } uint64 fullAnimMs = App::main() ? App::main()->animActiveTimeStart(this) : 0; if (fullAnimMs > 0 && fullAnimMs <= ms) { int animms = ms - fullAnimMs; if (animms > st::activeFadeInDuration + st::activeFadeOutDuration) { App::main()->stopAnimActive(); } else { int skiph = marginTop() - marginBottom(); float64 dt = (animms > st::activeFadeInDuration) ? (1 - (animms - st::activeFadeInDuration) / float64(st::activeFadeOutDuration)) : (animms / float64(st::activeFadeInDuration)); float64 o = p.opacity(); p.setOpacity(o * dt); p.fillRect(0, skiph, _history->width, height - skiph, textstyleCurrent()->selectOverlay->b); p.setOpacity(o); } } textstyleSet(&(outbg ? st::outTextStyle : st::inTextStyle)); if (const ReplyKeyboard *keyboard = inlineReplyKeyboard()) { int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); height -= h; int top = height + st::msgBotKbButton.margin - marginBottom(); p.translate(left, top); keyboard->paint(p, r.translated(-left, -top)); p.translate(-left, -top); } auto reply = Get(); if (reply) { reply->checkNameUpdate(); } if (bubble) { auto fwd = Get(); auto via = Get(); if (displayFromName() && author()->nameVersion > _authorNameVersion) { fromNameUpdated(width); } int32 top = marginTop(); QRect r(left, top, width, height - top - marginBottom()); style::color bg(selected ? (outbg ? st::msgOutBgSelected : st::msgInBgSelected) : (outbg ? st::msgOutBg : st::msgInBg)); style::color sh(selected ? (outbg ? st::msgOutShadowSelected : st::msgInShadowSelected) : (outbg ? st::msgOutShadow : st::msgInShadow)); RoundCorners cors(selected ? (outbg ? MessageOutSelectedCorners : MessageInSelectedCorners) : (outbg ? MessageOutCorners : MessageInCorners)); App::roundRect(p, r, bg, cors, &sh); if (displayFromName()) { p.setFont(st::msgNameFont); if (isPost()) { p.setPen(selected ? st::msgInServiceFgSelected : st::msgInServiceFg); } else { p.setPen(author()->color); } author()->nameText.drawElided(p, r.left() + st::msgPadding.left(), r.top() + st::msgPadding.top(), width - st::msgPadding.left() - st::msgPadding.right()); if (via && !fwd && width > st::msgPadding.left() + st::msgPadding.right() + author()->nameText.maxWidth() + st::msgServiceFont->spacew) { p.setPen(selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg)); p.drawText(r.left() + st::msgPadding.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew, r.top() + st::msgPadding.top() + st::msgServiceFont->ascent, via->_text); } r.setTop(r.top() + st::msgNameFont->height); } QRect trect(r.marginsAdded(-st::msgPadding)); paintForwardedInfo(p, trect, selected); paintReplyInfo(p, trect, selected); paintViaBotIdInfo(p, trect, selected); p.setPen(st::msgColor); p.setFont(st::msgFont); _text.draw(p, trect.x(), trect.y(), trect.width(), style::al_left, 0, -1, selection); if (_media && _media->isDisplayed()) { int32 top = height - marginBottom() - _media->height(); p.translate(left, top); _media->draw(p, r.translated(-left, -top), toMediaSelection(selection), ms); p.translate(-left, -top); if (!_media->customInfoLayout()) { HistoryMessage::drawInfo(p, r.x() + r.width(), r.y() + r.height(), 2 * r.x() + r.width(), selected, InfoDisplayDefault); } } else { HistoryMessage::drawInfo(p, r.x() + r.width(), r.y() + r.height(), 2 * r.x() + r.width(), selected, InfoDisplayDefault); } } else { int32 top = marginTop(); p.translate(left, top); _media->draw(p, r.translated(-left, -top), toMediaSelection(selection), ms); p.translate(-left, -top); } textstyleRestore(); } void HistoryMessage::paintForwardedInfo(Painter &p, QRect &trect, bool selected) const { if (displayForwardedFrom()) { style::font serviceFont(st::msgServiceFont), serviceName(st::msgServiceNameFont); p.setPen(selected ? (hasOutLayout() ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (hasOutLayout() ? st::msgOutServiceFg : st::msgInServiceFg)); p.setFont(serviceFont); auto fwd = Get(); bool breakEverywhere = (fwd->_text.countHeight(trect.width()) > 2 * serviceFont->height); textstyleSet(&(selected ? (hasOutLayout() ? st::outFwdTextStyleSelected : st::inFwdTextStyleSelected) : (hasOutLayout() ? st::outFwdTextStyle : st::inFwdTextStyle))); fwd->_text.drawElided(p, trect.x(), trect.y(), trect.width(), 2, style::al_left, 0, -1, 0, breakEverywhere); textstyleSet(&(hasOutLayout() ? st::outTextStyle : st::inTextStyle)); trect.setY(trect.y() + (((fwd->_text.maxWidth() > trect.width()) ? 2 : 1) * serviceFont->height)); } } void HistoryMessage::paintReplyInfo(Painter &p, QRect &trect, bool selected) const { if (auto reply = Get()) { int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); HistoryMessageReply::PaintFlags flags = HistoryMessageReply::PaintInBubble; if (selected) { flags |= HistoryMessageReply::PaintSelected; } reply->paint(p, this, trect.x(), trect.y(), trect.width(), flags); trect.setY(trect.y() + h); } } void HistoryMessage::paintViaBotIdInfo(Painter &p, QRect &trect, bool selected) const { if (!displayFromName() && !Has()) { if (auto via = Get()) { p.setFont(st::msgServiceNameFont); p.setPen(selected ? (hasOutLayout() ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (hasOutLayout() ? st::msgOutServiceFg : st::msgInServiceFg)); p.drawTextLeft(trect.left(), trect.top(), _history->width, via->_text); trect.setY(trect.y() + st::msgServiceNameFont->height); } } } void HistoryMessage::dependencyItemRemoved(HistoryItem *dependency) { if (auto reply = Get()) { reply->itemRemoved(this, dependency); } } void HistoryMessage::destroy() { eraseFromOverview(); HistoryItem::destroy(); } int HistoryMessage::resizeGetHeight_(int width) { int result = performResizeGetHeight(width); auto keyboard = inlineReplyKeyboard(); if (auto markup = Get()) { int 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(); if (keyboardTop != oldTop) { Notify::inlineKeyboardMoved(this, oldTop, keyboardTop); } } } } return result; } int HistoryMessage::performResizeGetHeight(int width) { if (width < st::msgMinWidth) return _height; width -= st::msgMargin.left() + st::msgMargin.right(); if (width < st::msgPadding.left() + st::msgPadding.right() + 1) { width = st::msgPadding.left() + st::msgPadding.right() + 1; } else if (width > st::msgMaxWidth) { width = st::msgMaxWidth; } if (drawBubble()) { auto fwd = Get(); auto reply = Get(); auto via = Get(); bool media = (_media && _media->isDisplayed()); if (width >= _maxw) { _height = _minh; if (media) _media->resizeGetHeight(_maxw); } else { if (_text.isEmpty()) { _height = 0; } else { int32 textWidth = qMax(width - st::msgPadding.left() - st::msgPadding.right(), 1); if (textWidth != _textWidth) { textstyleSet(&((out() && !isPost()) ? st::outTextStyle : st::inTextStyle)); _textWidth = textWidth; _textHeight = _text.countHeight(textWidth); textstyleRestore(); } _height = st::msgPadding.top() + _textHeight + st::msgPadding.bottom(); } if (media) _height += _media->resizeGetHeight(width); } if (displayFromName()) { if (emptyText()) { _height += st::msgPadding.top() + st::msgNameFont->height + st::mediaHeaderSkip; } else { _height += st::msgNameFont->height; } int32 l = 0, w = 0; countPositionAndSize(l, w); fromNameUpdated(w); } else if (via && !fwd) { int32 l = 0, w = 0; countPositionAndSize(l, w); via->resize(w - st::msgPadding.left() - st::msgPadding.right()); if (emptyText() && !displayFromName()) { _height += st::msgPadding.top() + st::msgNameFont->height + st::mediaHeaderSkip; } else { _height += st::msgNameFont->height; } } if (displayForwardedFrom()) { int32 l = 0, w = 0; countPositionAndSize(l, w); int32 fwdheight = ((fwd->_text.maxWidth() > (w - st::msgPadding.left() - st::msgPadding.right())) ? 2 : 1) * st::semiboldFont->height; if (emptyText() && !displayFromName()) { _height += st::msgPadding.top() + fwdheight + st::mediaHeaderSkip; } else { _height += fwdheight; } } if (reply) { if (emptyText() && !displayFromName() && !Has()) { _height += st::msgPadding.top() + st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom() + st::mediaHeaderSkip; } else { _height += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); } reply->resize(width - st::msgPadding.left() - st::msgPadding.right()); } } else { _height = _media->resizeGetHeight(width); } if (auto keyboard = inlineReplyKeyboard()) { int32 l = 0, w = 0; countPositionAndSize(l, w); int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); _height += h; keyboard->resize(w, h - st::msgBotKbButton.margin); } _height += marginTop() + marginBottom(); return _height; } bool HistoryMessage::hasPoint(int x, int y) const { int left = 0, width = 0, height = _height; countPositionAndSize(left, width); if (width < 1) return false; if (drawBubble()) { int top = marginTop(); QRect r(left, top, width, height - top - marginBottom()); return r.contains(x, y); } else { return _media->hasPoint(x - left, y - marginTop()); } } bool HistoryMessage::pointInTime(int32 right, int32 bottom, int x, int y, InfoDisplayType type) const { int32 infoRight = right, infoBottom = bottom; switch (type) { case InfoDisplayDefault: infoRight -= st::msgPadding.right() - st::msgDateDelta.x(); infoBottom -= st::msgPadding.bottom() - st::msgDateDelta.y(); break; case InfoDisplayOverImage: infoRight -= st::msgDateImgDelta + st::msgDateImgPadding.x(); infoBottom -= st::msgDateImgDelta + st::msgDateImgPadding.y(); break; } int32 dateX = infoRight - HistoryMessage::infoWidth() + HistoryMessage::timeLeft(); int32 dateY = infoBottom - st::msgDateFont->height; return QRect(dateX, dateY, HistoryMessage::timeWidth(), st::msgDateFont->height).contains(x, y); } HistoryTextState HistoryMessage::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; int left = 0, width = 0, height = _height; countPositionAndSize(left, width); if (width < 1) return result; auto keyboard = inlineReplyKeyboard(); if (keyboard) { int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); height -= h; } if (drawBubble()) { auto fwd = Get(); auto via = Get(); auto reply = Get(); int top = marginTop(); QRect r(left, top, width, height - top - marginBottom()); QRect trect(r.marginsAdded(-st::msgPadding)); if (displayFromName()) { if (y >= trect.top() && y < trect.top() + st::msgNameFont->height) { if (x >= trect.left() && x < trect.left() + trect.width() && x < trect.left() + author()->nameText.maxWidth()) { result.link = author()->openLink(); return result; } if (via && !fwd && x >= trect.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew && x < trect.left() + author()->nameText.maxWidth() + st::msgServiceFont->spacew + via->_width) { result.link = via->_lnk; return result; } } trect.setTop(trect.top() + st::msgNameFont->height); } if (displayForwardedFrom()) { int32 fwdheight = ((fwd->_text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height; if (y >= trect.top() && y < trect.top() + fwdheight) { bool breakEverywhere = (fwd->_text.countHeight(trect.width()) > 2 * st::semiboldFont->height); auto textRequest = request.forText(); if (breakEverywhere) { textRequest.flags |= Text::StateRequest::Flag::BreakEverywhere; } textstyleSet(&st::inFwdTextStyle); result = fwd->_text.getState(x - trect.left(), y - trect.top(), trect.width(), textRequest); textstyleRestore(); result.symbol = 0; result.afterSymbol = false; if (breakEverywhere) { result.cursor = HistoryInForwardedCursorState; } else { result.cursor = HistoryDefaultCursorState; } return result; } trect.setTop(trect.top() + fwdheight); } if (via && !displayFromName() && !displayForwardedFrom()) { if (x >= trect.left() && y >= trect.top() && y < trect.top() + st::msgNameFont->height && x < trect.left() + via->_width) { result.link = via->_lnk; return result; } trect.setTop(trect.top() + st::msgNameFont->height); } if (reply) { int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); if (y >= trect.top() && y < trect.top() + h) { if (reply->replyToMsg && y >= trect.top() + st::msgReplyPadding.top() && y < trect.top() + st::msgReplyPadding.top() + st::msgReplyBarSize.height() && x >= trect.left() && x < trect.left() + trect.width()) { result.link = reply->replyToLink(); } return result; } trect.setTop(trect.top() + h); } bool inDate = false, mediaDisplayed = _media && _media->isDisplayed(); if (!mediaDisplayed || !_media->customInfoLayout()) { inDate = HistoryMessage::pointInTime(r.x() + r.width(), r.y() + r.height(), x, y, InfoDisplayDefault); } if (mediaDisplayed) { trect.setBottom(trect.bottom() - _media->height()); if (y >= r.bottom() - _media->height()) { result = _media->getState(x - r.left(), y - (r.bottom() - _media->height()), request); result.symbol += _text.length(); } } if (!mediaDisplayed || (y < r.bottom() - _media->height())) { textstyleSet(&((out() && !isPost()) ? st::outTextStyle : st::inTextStyle)); result = _text.getState(x - trect.x(), y - trect.y(), trect.width(), request.forText()); textstyleRestore(); } if (inDate) { result.cursor = HistoryInDateCursorState; } } else { result = _media->getState(x - left, y - marginTop(), request); result.symbol += _text.length(); } if (keyboard) { int top = height + st::msgBotKbButton.margin - marginBottom(); if (x >= left && x < left + width && y >= top && y < _height - marginBottom()) { result.link = keyboard->getState(x - left, y - top); return result; } } return result; } TextSelection HistoryMessage::adjustSelection(TextSelection selection, TextSelectType type) const { if (!_media || selection.to <= _text.length()) { return _text.adjustSelection(selection, type); } auto mediaSelection = _media->adjustSelection(toMediaSelection(selection), type); if (selection.from >= _text.length()) { return fromMediaSelection(mediaSelection); } auto textSelection = _text.adjustSelection(selection, type); return { textSelection.from, fromMediaSelection(mediaSelection).to }; } void HistoryMessage::drawInDialog(Painter &p, const QRect &r, bool act, const HistoryItem *&cacheFor, Text &cache) const { if (cacheFor != this) { cacheFor = this; QString msg(inDialogsText()); if ((!_history->peer->isUser() || out()) && !isPost()) { TextCustomTagsMap custom; custom.insert(QChar('c'), qMakePair(textcmdStartLink(1), textcmdStopLink())); msg = lng_message_with_from(lt_from, textRichPrepare((author() == App::self()) ? lang(lng_from_you) : author()->shortName()), lt_message, textRichPrepare(msg)); cache.setRichText(st::dlgHistFont, msg, _textDlgOptions, custom); } else { cache.setText(st::dlgHistFont, msg, _textDlgOptions); } } if (r.width()) { textstyleSet(&(act ? st::dlgActiveTextStyle : st::dlgTextStyle)); p.setFont(st::dlgHistFont->f); p.setPen((act ? st::dlgActiveColor : (emptyText() ? st::dlgSystemColor : st::dlgTextColor))->p); cache.drawElided(p, r.left(), r.top(), r.width(), r.height() / st::dlgHistFont->height); textstyleRestore(); } } QString HistoryMessage::notificationHeader() const { return (!_history->peer->isUser() && !isPost()) ? from()->name : QString(); } QString HistoryMessage::notificationText() const { QString msg(inDialogsText()); if (msg.size() > 0xFF) msg = msg.mid(0, 0xFF) + qsl("..."); return msg; } bool HistoryMessage::displayFromPhoto() const { return hasFromPhoto() && !isAttachedToPrevious(); } bool HistoryMessage::hasFromPhoto() const { return (Adaptive::Wide() || (!out() && !history()->peer->isUser())) && !isPost(); } HistoryMessage::~HistoryMessage() { _media.clear(); if (auto reply = Get()) { reply->clearData(this); } } void HistoryService::setMessageByAction(const MTPmessageAction &action) { QList links; LangString text = lang(lng_message_empty); QString from = textcmdLink(1, _from->name); switch (action.type()) { case mtpc_messageActionChatAddUser: { const auto &d(action.c_messageActionChatAddUser()); const auto &v(d.vusers.c_vector().v); bool foundSelf = false; for (int32 i = 0, l = v.size(); i < l; ++i) { if (v.at(i).v == MTP::authedId()) { foundSelf = true; break; } } if (v.size() == 1) { UserData *u = App::user(peerFromUser(v.at(0))); if (u == _from) { text = lng_action_user_joined(lt_from, from); } else { links.push_back(MakeShared(u)); text = lng_action_add_user(lt_from, from, lt_user, textcmdLink(2, u->name)); } } else if (v.isEmpty()) { text = lng_action_add_user(lt_from, from, lt_user, "somebody"); } else { for (int32 i = 0, l = v.size(); i < l; ++i) { UserData *u = App::user(peerFromUser(v.at(i))); QString linkText = textcmdLink(i + 2, u->name); if (i == 0) { text = linkText; } else if (i + 1 < l) { text = lng_action_add_users_and_one(lt_accumulated, text, lt_user, linkText); } else { text = lng_action_add_users_and_last(lt_accumulated, text, lt_user, linkText); } links.push_back(MakeShared(u)); } text = lng_action_add_users_many(lt_from, from, lt_users, text); } if (foundSelf) { if (history()->peer->isMegagroup()) { history()->peer->asChannel()->mgInfo->joinedMessageFound = true; } } } break; case mtpc_messageActionChatJoinedByLink: { const auto &d(action.c_messageActionChatJoinedByLink()); //if (true || peerFromUser(d.vinviter_id) == _from->id) { text = lng_action_user_joined_by_link(lt_from, from); //} else { // UserData *u = App::user(App::peerFromUser(d.vinviter_id)); // links.push_back(MakeShared(u)); // text = lng_action_user_joined_by_link_from(lt_from, from, lt_inviter, textcmdLink(2, u->name)); //} if (_from->isSelf() && history()->peer->isMegagroup()) { history()->peer->asChannel()->mgInfo->joinedMessageFound = true; } } break; case mtpc_messageActionChatCreate: { const auto &d(action.c_messageActionChatCreate()); text = lng_action_created_chat(lt_from, from, lt_title, textClean(qs(d.vtitle))); } break; case mtpc_messageActionChannelCreate: { const auto &d(action.c_messageActionChannelCreate()); if (isPost()) { text = lng_action_created_channel(lt_title, textClean(qs(d.vtitle))); } else { text = lng_action_created_chat(lt_from, from, lt_title, textClean(qs(d.vtitle))); } } break; case mtpc_messageActionChatDeletePhoto: { text = isPost() ? lang(lng_action_removed_photo_channel) : lng_action_removed_photo(lt_from, from); } break; case mtpc_messageActionChatDeleteUser: { const auto &d(action.c_messageActionChatDeleteUser()); if (peerFromUser(d.vuser_id) == _from->id) { text = lng_action_user_left(lt_from, from); } else { UserData *u = App::user(peerFromUser(d.vuser_id)); links.push_back(MakeShared(u)); text = lng_action_kick_user(lt_from, from, lt_user, textcmdLink(2, u->name)); } } break; case mtpc_messageActionChatEditPhoto: { const auto &d(action.c_messageActionChatEditPhoto()); if (d.vphoto.type() == mtpc_photo) { _media.reset(new HistoryPhoto(this, history()->peer, d.vphoto.c_photo(), st::msgServicePhotoWidth)); } text = isPost() ? lang(lng_action_changed_photo_channel) : lng_action_changed_photo(lt_from, from); } break; case mtpc_messageActionChatEditTitle: { const auto &d(action.c_messageActionChatEditTitle()); text = isPost() ? lng_action_changed_title_channel(lt_title, textClean(qs(d.vtitle))) : lng_action_changed_title(lt_from, from, lt_title, textClean(qs(d.vtitle))); } break; case mtpc_messageActionChatMigrateTo: { _flags |= MTPDmessage_ClientFlag::f_is_group_migrate; const auto &d(action.c_messageActionChatMigrateTo()); if (true/*PeerData *channel = App::channelLoaded(d.vchannel_id.v)*/) { text = lang(lng_action_group_migrate); } else { text = lang(lng_contacts_loading); } } break; case mtpc_messageActionChannelMigrateFrom: { _flags |= MTPDmessage_ClientFlag::f_is_group_migrate; const auto &d(action.c_messageActionChannelMigrateFrom()); if (true/*PeerData *chat = App::chatLoaded(d.vchat_id.v)*/) { text = lang(lng_action_group_migrate); } else { text = lang(lng_contacts_loading); } } break; case mtpc_messageActionPinMessage: { if (updatePinnedText(&from, &text)) { auto pinned = Get(); t_assert(pinned != nullptr); links.push_back(pinned->lnk); } } break; default: from = QString(); break; } textstyleSet(&st::serviceTextStyle); _text.setText(st::msgServiceFont, text, _historySrvOptions); textstyleRestore(); if (!from.isEmpty()) { _text.setLink(1, MakeShared(_from)); } for (int32 i = 0, l = links.size(); i < l; ++i) { _text.setLink(i + 2, links.at(i)); } } bool HistoryService::updatePinned(bool force) { auto pinned = Get(); t_assert(pinned != nullptr); if (!force) { if (!pinned->msgId || pinned->msg) { return true; } } if (!pinned->lnk) { pinned->lnk.reset(new GoToMessageClickHandler(history()->peer->id, pinned->msgId)); } bool gotDependencyItem = false; if (!pinned->msg) { pinned->msg = App::histItemById(channelId(), pinned->msgId); if (pinned->msg) { App::historyRegDependency(this, pinned->msg); gotDependencyItem = true; } } if (pinned->msg) { updatePinnedText(); } else if (force) { if (pinned->msgId > 0) { pinned->msgId = 0; gotDependencyItem = true; } updatePinnedText(); } if (force) { setPendingInitDimensions(); if (gotDependencyItem && App::wnd()) { App::wnd()->notifySettingGot(); } } return (pinned->msg || !pinned->msgId); } bool HistoryService::updatePinnedText(const QString *pfrom, QString *ptext) { bool result = false; QString from, text; if (pfrom) { from = *pfrom; } else { from = textcmdLink(1, _from->name); } ClickHandlerPtr second; auto pinned = Get(); if (pinned && pinned->msg) { HistoryMedia *media = pinned->msg->getMedia(); QString mediaText; switch (media ? media->type() : MediaTypeCount) { case MediaTypePhoto: mediaText = lang(lng_action_pinned_media_photo); break; case MediaTypeVideo: mediaText = lang(lng_action_pinned_media_video); break; case MediaTypeContact: mediaText = lang(lng_action_pinned_media_contact); break; case MediaTypeFile: mediaText = lang(lng_action_pinned_media_file); break; case MediaTypeGif: mediaText = lang(lng_action_pinned_media_gif); break; case MediaTypeSticker: mediaText = lang(lng_action_pinned_media_sticker); break; case MediaTypeLocation: mediaText = lang(lng_action_pinned_media_location); break; case MediaTypeMusicFile: mediaText = lang(lng_action_pinned_media_audio); break; case MediaTypeVoiceFile: mediaText = lang(lng_action_pinned_media_voice); break; } if (mediaText.isEmpty()) { QString original = pinned->msg->originalText(); int32 cutat = 0, limit = PinnedMessageTextLimit, size = original.size(); for (; limit > 0;) { --limit; if (cutat >= size) break; if (original.at(cutat).isLowSurrogate() && cutat + 1 < size && original.at(cutat + 1).isHighSurrogate()) { cutat += 2; } else { ++cutat; } } if (!limit && cutat + 5 < size) { original = original.mid(0, cutat) + qstr("..."); } text = lng_action_pinned_message(lt_from, from, lt_text, textcmdLink(2, original)); } else { text = lng_action_pinned_media(lt_from, from, lt_media, textcmdLink(2, mediaText)); } second = pinned->lnk; result = true; } else if (pinned && pinned->msgId) { text = lng_action_pinned_media(lt_from, from, lt_media, textcmdLink(2, lang(lng_contacts_loading))); second = pinned->lnk; result = true; } else { text = lng_action_pinned_media(lt_from, from, lt_media, lang(lng_deleted_message)); } if (ptext) { *ptext = text; } else { setServiceText(text); _text.setLink(1, MakeShared(_from)); if (second) { _text.setLink(2, second); } if (history()->textCachedFor == this) { history()->textCachedFor = 0; } if (App::main()) { App::main()->dlgUpdated(history(), id); } App::historyUpdateDependent(this); } return result; } HistoryService::HistoryService(History *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) { if (msg.has_reply_to_msg_id()) { UpdateComponents(HistoryServicePinned::Bit()); MsgId pinnedMsgId = Get()->msgId = msg.vreply_to_msg_id.v; if (!updatePinned() && App::api()) { App::api()->requestMessageData(history->peer->asChannel(), pinnedMsgId, new HistoryDependentItemCallback(fullId())); } } setMessageByAction(msg.vaction); } HistoryService::HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags, int32 from) : HistoryItem(history, msgId, flags, date, from) { _text.setText(st::msgServiceFont, msg, _historySrvOptions); } void HistoryService::initDimensions() { _maxw = _text.maxWidth() + st::msgServicePadding.left() + st::msgServicePadding.right(); _minh = _text.minHeight(); if (_media) _media->initDimensions(); } void HistoryService::countPositionAndSize(int32 &left, int32 &width) const { left = st::msgServiceMargin.left(); int32 maxwidth = _history->width; if (Adaptive::Wide()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } width = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); } QString HistoryService::selectedText(TextSelection selection) const { return _text.original((selection == FullSelection) ? AllTextSelection : selection); } QString HistoryService::inDialogsText() const { return _text.original(AllTextSelection, Text::ExpandLinksNone); } QString HistoryService::inReplyText() const { QString result = HistoryService::inDialogsText(); return result.trimmed().startsWith(author()->name) ? result.trimmed().mid(author()->name.size()).trimmed() : result; } void HistoryService::setServiceText(const QString &text) { textstyleSet(&st::serviceTextStyle); _text.setText(st::msgServiceFont, text, _historySrvOptions); textstyleRestore(); initDimensions(); } void HistoryService::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { int left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins countPositionAndSize(left, width); if (width < 1) return; int dateh = 0, unreadbarh = 0; if (auto date = Get()) { dateh = date->height(); if (r.intersects(QRect(0, 0, _history->width, dateh))) { date->paint(p, 0, _history->width); } p.translate(0, dateh); height -= dateh; } if (auto unreadbar = Get()) { unreadbarh = unreadbar->height(); if (r.intersects(QRect(0, 0, _history->width, unreadbarh))) { unreadbar->paint(p, 0, _history->width); } p.translate(0, unreadbarh); height -= unreadbarh; } uint64 fullAnimMs = App::main() ? App::main()->animActiveTimeStart(this) : 0; if (fullAnimMs > 0 && fullAnimMs <= ms) { int animms = ms - fullAnimMs; if (animms > st::activeFadeInDuration + st::activeFadeOutDuration) { App::main()->stopAnimActive(); } else { int skiph = st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); textstyleSet(&st::inTextStyle); float64 dt = (animms > st::activeFadeInDuration) ? (1 - (animms - st::activeFadeInDuration) / float64(st::activeFadeOutDuration)) : (animms / float64(st::activeFadeInDuration)); float64 o = p.opacity(); p.setOpacity(o * dt); p.fillRect(0, skiph, _history->width, _height - skiph, textstyleCurrent()->selectOverlay->b); p.setOpacity(o); } } if (_media) { height -= st::msgServiceMargin.top() + _media->height(); int32 left = st::msgServiceMargin.left() + (width - _media->maxWidth()) / 2, top = st::msgServiceMargin.top() + height + st::msgServiceMargin.top(); p.translate(left, top); _media->draw(p, r.translated(-left, -top), toMediaSelection(selection), ms); p.translate(-left, -top); } QRect trect(QRect(left, st::msgServiceMargin.top(), width, height).marginsAdded(-st::msgServicePadding)); if (width > _maxw) { left += (width - _maxw) / 2; width = _maxw; } App::roundRect(p, left, st::msgServiceMargin.top(), width, height, App::msgServiceBg(), (selection == FullSelection) ? ServiceSelectedCorners : ServiceCorners); textstyleSet(&st::serviceTextStyle); p.setBrush(Qt::NoBrush); p.setPen(st::msgServiceColor); p.setFont(st::msgServiceFont); _text.draw(p, trect.x(), trect.y(), trect.width(), Qt::AlignCenter, 0, -1, selection); textstyleRestore(); if (int skiph = dateh + unreadbarh) { p.translate(0, -skiph); } } int32 HistoryService::resizeGetHeight_(int32 width) { int32 maxwidth = _history->width; if (Adaptive::Wide()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } if (width > maxwidth) width = maxwidth; width -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins if (width < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) width = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; int32 nwidth = qMax(width - st::msgPadding.left() - st::msgPadding.right(), 0); if (nwidth != _textWidth) { _textWidth = nwidth; textstyleSet(&st::serviceTextStyle); _textHeight = _text.countHeight(nwidth); textstyleRestore(); } if (width >= _maxw) { _height = _minh; } else { _height = _textHeight; } _height += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom(); if (_media) { _height += st::msgServiceMargin.top() + _media->resizeGetHeight(_media->currentWidth()); } _height += displayedDateHeight(); if (auto unreadbar = Get()) { _height += unreadbar->height(); } return _height; } bool HistoryService::hasPoint(int x, int y) const { int left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins countPositionAndSize(left, width); if (width < 1) return false; if (int dateh = displayedDateHeight()) { y -= dateh; height -= dateh; } if (auto unreadbar = Get()) { int unreadbarh = unreadbar->height(); y -= unreadbarh; height -= unreadbarh; } if (_media) { height -= st::msgServiceMargin.top() + _media->height(); } return QRect(left, st::msgServiceMargin.top(), width, height).contains(x, y); } HistoryTextState HistoryService::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; int left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins countPositionAndSize(left, width); if (width < 1) return result; if (int dateh = displayedDateHeight()) { y -= dateh; height -= dateh; } if (auto unreadbar = Get()) { int unreadbarh = unreadbar->height(); y -= unreadbarh; height -= unreadbarh; } if (_media) { height -= st::msgServiceMargin.top() + _media->height(); } QRect trect(QRect(left, st::msgServiceMargin.top(), width, height).marginsAdded(-st::msgServicePadding)); if (trect.contains(x, y)) { textstyleSet(&st::serviceTextStyle); auto textRequest = request.forText(); textRequest.align = style::al_center; result = _text.getState(x - trect.x(), y - trect.y(), trect.width(), textRequest); textstyleRestore(); } else if (_media) { result = _media->getState(x - st::msgServiceMargin.left() - (width - _media->maxWidth()) / 2, y - st::msgServiceMargin.top() - height - st::msgServiceMargin.top(), request); } return result; } void HistoryService::drawInDialog(Painter &p, const QRect &r, bool act, const HistoryItem *&cacheFor, Text &cache) const { if (cacheFor != this) { cacheFor = this; cache.setText(st::dlgHistFont, inDialogsText(), _textDlgOptions); } QRect tr(r); p.setPen((act ? st::dlgActiveColor : st::dlgSystemColor)->p); cache.drawElided(p, tr.left(), tr.top(), tr.width(), tr.height() / st::dlgHistFont->height); } QString HistoryService::notificationText() const { QString msg = _text.original(); if (msg.size() > 0xFF) msg = msg.mid(0, 0xFF) + qsl("..."); return msg; } HistoryMedia *HistoryService::getMedia() const { return _media.data(); } HistoryService::~HistoryService() { if (auto pinned = Get()) { if (pinned->msg) { App::historyUnregDependency(this, pinned->msg); } } _media.clear(); } HistoryGroup::HistoryGroup(History *history, const MTPDmessageGroup &group, const QDateTime &date) : HistoryService(history, clientMsgId(), date, lng_channel_comments_count(lt_count, group.vcount.v)/* + qsl(" (%1 ... %2)").arg(group.vmin_id.v).arg(group.vmax_id.v)*/) , _minId(group.vmin_id.v) , _maxId(group.vmax_id.v) , _count(group.vcount.v) , _lnk(new CommentsClickHandler(this)) { } HistoryGroup::HistoryGroup(History *history, HistoryItem *newItem, const QDateTime &date) : HistoryService(history, clientMsgId(), date, lng_channel_comments_count(lt_count, 1)/* + qsl(" (%1 ... %2)").arg(newItem->id - 1).arg(newItem->id + 1)*/) , _minId(newItem->id - 1) , _maxId(newItem->id + 1) , _count(1) , _lnk(new CommentsClickHandler(this)) { } HistoryTextState HistoryGroup::getState(int x, int y, HistoryStateRequest request) const { HistoryTextState result; int32 left = 0, width = 0, height = _height - st::msgServiceMargin.top() - st::msgServiceMargin.bottom(); // two small margins countPositionAndSize(left, width); if (width < 1) return result; QRect trect(QRect(left, st::msgServiceMargin.top(), width, height).marginsAdded(-st::msgServicePadding)); if (width > _maxw) { left += (width - _maxw) / 2; width = _maxw; } if (QRect(left, st::msgServiceMargin.top(), width, height).contains(x, y)) { result.link = _lnk; } return result; } void HistoryGroup::uniteWith(MsgId minId, MsgId maxId, int32 count) { if (minId < 0 || maxId < 0) return; if (minId == _minId && maxId == _maxId && count == _count) return; if (minId < _minId) { if (maxId <= _minId + 1) { _count += count; } else if (maxId <= _maxId) { // :( smth not precise _count += qMax(0, count - (maxId - _minId - 1)); } else { // :( smth not precise _count = qMax(count, _count); _maxId = maxId; } _minId = minId; } else if (maxId > _maxId) { if (minId + 1 >= _maxId) { _count += count; } else if (minId >= _minId) { // :( smth not precise _count += qMax(0, count - (_maxId - minId - 1)); } else { // :( smth not precise _count = qMax(count, _count); _minId = minId; } _maxId = maxId; } else if (count > _count) { // :( smth not precise _count = count; } updateText(); } bool HistoryGroup::decrementCount() { if (_count > 1) { --_count; updateText(); return true; } return false; } void HistoryGroup::updateText() { setServiceText(lng_channel_comments_count(lt_count, _count)/* + qsl(" (%1 ... %2)").arg(_minId).arg(_maxId)*/); } HistoryCollapse::HistoryCollapse(History *history, MsgId wasMinId, const QDateTime &date) : HistoryService(history, clientMsgId(), date, qsl("-")) , _wasMinId(wasMinId) { } void HistoryCollapse::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { } HistoryTextState HistoryCollapse::getState(int x, int y, HistoryStateRequest request) const { return HistoryTextState(); } HistoryJoined::HistoryJoined(History *history, const QDateTime &inviteDate, UserData *inviter, MTPDmessage::Flags flags) : HistoryService(history, clientMsgId(), inviteDate, QString(), flags) { textstyleSet(&st::serviceTextStyle); if (peerToUser(inviter->id) == MTP::authedId()) { _text.setText(st::msgServiceFont, lang(history->isMegagroup() ? lng_action_you_joined_group : lng_action_you_joined), _historySrvOptions); } else { _text.setText(st::msgServiceFont, history->isMegagroup() ? lng_action_add_you_group(lt_from, textcmdLink(1, inviter->name)) : lng_action_add_you(lt_from, textcmdLink(1, inviter->name)), _historySrvOptions); _text.setLink(1, MakeShared(inviter)); } textstyleRestore(); } void GoToMessageClickHandler::onClickImpl() const { if (App::main()) { HistoryItem *current = App::mousedItem(); if (current && current->history()->peer->id == peer()) { App::main()->pushReplyReturn(current); } Ui::showPeerHistory(peer(), msgid()); } } void CommentsClickHandler::onClickImpl() const { if (App::main() && peerIsChannel(peer())) { Ui::showPeerHistory(peer(), msgid()); } }