8575 lines
287 KiB
C++
8575 lines
287 KiB
C++
/*
|
|
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 "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<HistoryGroup*>(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<HistoryCollapse*>(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<HistoryGroup*>(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<HistoryGroup*>(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<MTPMessageRange>(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<HistoryGroup*>(_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<HistoryGroup*>(_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<HistoryGroup*>(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<HistoryGroup*>(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<HistoryGroup*>(*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<HistoryGroup*>(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<HistoryGroup*>(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<HistoryGroup*>(_otherList[otherIndex - 1]) : 0) {
|
|
if (HistoryGroup *groupNext = (_otherList[otherIndex]->type() == HistoryItemGroup) ? static_cast<HistoryGroup*>(_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<History*>(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<HistoryMessageReplyMarkup>()) {
|
|
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);
|
|
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<UserData*> *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<PeerData*> *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<MTPMessage> &slice, const QVector<MTPMessageGroup> *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<UserData*> *lastAuthors = 0;
|
|
OrderedSet<PeerData*> *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<MTPMessage> &slice, const QVector<MTPMessageGroup> *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 = nullptr;
|
|
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 <typename CreateGroup, typename UniteGroup>
|
|
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<HistoryGroup*>(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<HistoryGroup*>(first)->uniteWith(static_cast<HistoryGroup*>(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<ChannelHistory*>(this) : 0;
|
|
}
|
|
|
|
const ChannelHistory *History::asChannelHistory() const {
|
|
return isChannel() ? static_cast<const ChannelHistory*>(this) : 0;
|
|
}
|
|
|
|
void History::clear(bool leaveItems) {
|
|
if (unreadBar) {
|
|
unreadBar = nullptr;
|
|
}
|
|
if (showFrom) {
|
|
showFrom = nullptr;
|
|
}
|
|
if (lastSentMsg) {
|
|
lastSentMsg = 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<MTPMessage> *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<MTPMessage>::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->lastSentMsg == item) {
|
|
history->lastSentMsg = nullptr;
|
|
}
|
|
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<HistoryGroup*>(item);
|
|
break;
|
|
} else if (item->type() == HistoryItemCollapse) {
|
|
nextCollapse = static_cast<HistoryCollapse*>(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<HistoryGroup*>(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<HistoryMessageReplyMarkup>()) {
|
|
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<StylePtr>(s)) {
|
|
if (auto markup = item->Get<HistoryMessageReplyMarkup>()) {
|
|
_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<HistoryMessageReplyMarkup>();
|
|
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<int>(std::floor(x));
|
|
int rectw = static_cast<int>(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<MTPKeyboardButtonRow> &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<HistoryMessageReplyMarkup>()) {
|
|
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<HistoryMessageReplyMarkup>()) {
|
|
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<HistoryMessageDate>()) {
|
|
AddComponents(HistoryMessageDate::Bit());
|
|
Get<HistoryMessageDate>()->init(date);
|
|
setPendingInitDimensions();
|
|
}
|
|
} else if (Has<HistoryMessageDate>()) {
|
|
RemoveComponents(HistoryMessageDate::Bit());
|
|
setPendingInitDimensions();
|
|
}
|
|
|
|
recountAttachToPrevious();
|
|
}
|
|
|
|
void HistoryItem::recountAttachToPrevious() {
|
|
bool attach = false;
|
|
if (!isPost() && !Has<HistoryMessageDate>() && !Has<HistoryMessageUnreadBar>()) {
|
|
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 {
|
|
if (id < 0 || date.secsTo(cur) >= Global::EditTimeLimit()) return false;
|
|
|
|
if (auto msg = toHistoryMessage()) {
|
|
if (msg->Has<HistoryMessageVia>() || msg->Has<HistoryMessageForwarded>()) return false;
|
|
|
|
if (HistoryMedia *media = msg->getMedia()) {
|
|
auto type = media->type();
|
|
if (type != MediaTypePhoto &&
|
|
type != MediaTypeVideo &&
|
|
type != MediaTypeFile &&
|
|
type != MediaTypeGif &&
|
|
type != MediaTypeMusicFile &&
|
|
type != MediaTypeVoiceFile &&
|
|
type != MediaTypeWebPage) {
|
|
return false;
|
|
}
|
|
}
|
|
if (isPost()) {
|
|
auto channel = _history->peer->asChannel();
|
|
return (channel->amCreator() || (channel->amEditor() && out()));
|
|
}
|
|
return out();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void HistoryItem::destroyUnreadBar() {
|
|
if (Has<HistoryMessageUnreadBar>()) {
|
|
RemoveComponents(HistoryMessageUnreadBar::Bit());
|
|
setPendingInitDimensions();
|
|
if (_history->unreadBar == this) {
|
|
_history->unreadBar = nullptr;
|
|
}
|
|
|
|
recountAttachToPrevious();
|
|
}
|
|
}
|
|
|
|
void HistoryItem::setUnreadBarCount(int count) {
|
|
if (count > 0) {
|
|
HistoryMessageUnreadBar *bar;
|
|
if (!Has<HistoryMessageUnreadBar>()) {
|
|
AddComponents(HistoryMessageUnreadBar::Bit());
|
|
setPendingInitDimensions();
|
|
|
|
recountAttachToPrevious();
|
|
|
|
bar = Get<HistoryMessageUnreadBar>();
|
|
} else {
|
|
bar = Get<HistoryMessageUnreadBar>();
|
|
if (bar->_freezed) {
|
|
return;
|
|
}
|
|
Global::RefPendingRepaintItems().insert(this);
|
|
}
|
|
bar->init(count);
|
|
} else {
|
|
destroyUnreadBar();
|
|
}
|
|
}
|
|
|
|
void HistoryItem::setUnreadBarFreezed() {
|
|
if (auto bar = Get<HistoryMessageUnreadBar>()) {
|
|
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(AnimationCallbacks &&callbacks)
|
|
: _firstStart(0)
|
|
, _lastStart(0)
|
|
, _lastTime(0)
|
|
, _opacity(0)
|
|
, a_arcEnd(0, 0)
|
|
, a_arcStart(0, FullArcLength)
|
|
, _animation(std_::move(callbacks)) {
|
|
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
TextWithEntities captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) {
|
|
if (selection != FullSelection) {
|
|
return caption.originalTextWithEntities(selection, ExpandLinksAll);
|
|
}
|
|
|
|
TextWithEntities result, original;
|
|
if (!caption.isEmpty()) {
|
|
original = caption.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
|
|
}
|
|
result.text.reserve(5 + attachType.size() + original.text.size());
|
|
result.text.append(qstr("[ ")).append(attachType).append(qstr(" ]"));
|
|
if (!caption.isEmpty()) {
|
|
result.text.append(qstr("\n"));
|
|
appendTextWithEntities(result, std_::move(original));
|
|
}
|
|
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<HistoryFileMedia*>(this), &HistoryFileMedia::step_thumbOver),
|
|
animation(const_cast<HistoryFileMedia*>(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<PhotoOpenClickHandler>(_data), MakeShared<PhotoSaveClickHandler>(_data), MakeShared<PhotoCancelClickHandler>(_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<PhotoOpenClickHandler>(_data, chat), MakeShared<PhotoSaveClickHandler>(_data, chat), MakeShared<PhotoCancelClickHandler>(_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<PhotoOpenClickHandler>(_data), MakeShared<PhotoSaveClickHandler>(_data), MakeShared<PhotoCancelClickHandler>(_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.originalText(AllTextSelection, ExpandLinksNone);
|
|
}
|
|
|
|
TextWithEntities 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.originalText(AllTextSelection, ExpandLinksNone);
|
|
}
|
|
|
|
TextWithEntities 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<HistoryDocument*>(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<HistoryDocumentNamed>()) {
|
|
named->_name = documentName(_data);
|
|
named->_namew = st::semiboldFont->width(named->_name);
|
|
}
|
|
|
|
setDocumentLinks(_data);
|
|
|
|
setStatusSize(FileStatusSizeReady);
|
|
|
|
if (auto captioned = Get<HistoryDocumentCaptioned>()) {
|
|
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<HistoryDocumentCaptioned>();
|
|
createComponents(captioned != 0);
|
|
if (auto named = Get<HistoryDocumentNamed>()) {
|
|
if (auto othernamed = other.Get<HistoryDocumentNamed>()) {
|
|
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<HistoryDocumentCaptioned>()->_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<HistoryDocumentThumbed>()) {
|
|
thumbed->_linksavel.reset(new DocumentSaveClickHandler(_data));
|
|
thumbed->_linkcancell.reset(new DocumentCancelClickHandler(_data));
|
|
}
|
|
}
|
|
|
|
void HistoryDocument::initDimensions() {
|
|
auto captioned = Get<HistoryDocumentCaptioned>();
|
|
if (captioned && captioned->_caption.hasSkipBlock()) {
|
|
captioned->_caption.setSkipBlock(_parent->skipBlockWidth(), _parent->skipBlockHeight());
|
|
}
|
|
|
|
auto thumbed = Get<HistoryDocumentThumbed>();
|
|
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<HistoryDocumentNamed>()) {
|
|
_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<HistoryMessageSigned>()) {
|
|
_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<HistoryDocumentCaptioned>();
|
|
if (!captioned) {
|
|
return HistoryFileMedia::resizeGetHeight(width);
|
|
}
|
|
|
|
_width = qMin(width, _maxw);
|
|
if (Get<HistoryDocumentThumbed>()) {
|
|
_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<HistoryDocumentThumbed>()) {
|
|
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<HistoryDocumentVoice>()) {
|
|
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<HistoryDocumentNamed>()) {
|
|
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<HistoryDocumentCaptioned>()) {
|
|
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<HistoryDocumentThumbed>()) {
|
|
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<HistoryDocumentCaptioned>()) {
|
|
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<HistoryDocumentVoice>()) {
|
|
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<HistoryDocumentNamed>();
|
|
result = (!named || named->_name.isEmpty()) ? lang(lng_in_dlg_file) : named->_name;
|
|
}
|
|
if (auto captioned = Get<HistoryDocumentCaptioned>()) {
|
|
if (!captioned->_caption.isEmpty()) {
|
|
result.append(' ').append(captioned->_caption.originalText(AllTextSelection, ExpandLinksNone));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TextWithEntities HistoryDocument::selectedText(TextSelection selection) const {
|
|
const Text emptyCaption;
|
|
const Text *caption = &emptyCaption;
|
|
if (auto captioned = Get<HistoryDocumentCaptioned>()) {
|
|
caption = &captioned->_caption;
|
|
}
|
|
QString attachType = lang(lng_in_dlg_file);
|
|
if (Has<HistoryDocumentVoice>()) {
|
|
attachType = lang(lng_in_dlg_audio);
|
|
} else if (_data->song()) {
|
|
attachType = lang(lng_in_dlg_audio_file);
|
|
}
|
|
if (auto named = Get<HistoryDocumentNamed>()) {
|
|
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<HistoryDocumentThumbed>()) {
|
|
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<HistoryDocumentVoice>()) {
|
|
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<HistoryDocumentVoice>()) {
|
|
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<HistoryDocumentVoice>()) {
|
|
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.originalText(AllTextSelection, ExpandLinksNone)));
|
|
}
|
|
|
|
TextWithEntities 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 (_data->loaded(DocumentData::FilePathResolveChecked)) {
|
|
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<HistoryMessageVia>();
|
|
auto reply = _parent->Get<HistoryMessageReply>();
|
|
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<HistoryMessageVia>();
|
|
auto reply = childmedia ? nullptr : _parent->Get<HistoryMessageReply>();
|
|
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<HistoryMessageVia>();
|
|
auto reply = childmedia ? nullptr : _parent->Get<HistoryMessageReply>();
|
|
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);
|
|
}
|
|
|
|
TextWithEntities HistorySticker::selectedText(TextSelection selection) const {
|
|
if (selection != FullSelection) {
|
|
return TextWithEntities();
|
|
}
|
|
return { qsl("[ ") + inDialogsText() + qsl(" ]"), EntitiesInText() };
|
|
}
|
|
|
|
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<HistoryContact*>(media)->fname();
|
|
QString lname = static_cast<HistoryContact*>(media)->lname();
|
|
QString phone = static_cast<HistoryContact*>(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();
|
|
if (_parent->Has<HistoryMessageSigned>()) {
|
|
_minh += st::msgDateFont->height - st::msgDateDelta.y();
|
|
}
|
|
} 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);
|
|
}
|
|
|
|
TextWithEntities HistoryContact::selectedText(TextSelection selection) const {
|
|
if (selection != FullSelection) {
|
|
return TextWithEntities();
|
|
}
|
|
return { qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.originalText() + '\n' + _phone, EntitiesInText() };
|
|
}
|
|
|
|
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.drawSprite(QPoint((pixwidth - st::youtubeIcon.pxWidth()) / 2, (pixheight - st::youtubeIcon.pxHeight()) / 2), st::youtubeIcon);
|
|
} else {
|
|
p.drawSprite(QPoint((pixwidth - st::videoIcon.pxWidth()) / 2, (pixheight - st::videoIcon.pxHeight()) / 2), 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();
|
|
}
|
|
|
|
TextWithEntities HistoryWebPage::selectedText(TextSelection selection) const {
|
|
if (selection == FullSelection) {
|
|
return TextWithEntities();
|
|
}
|
|
auto titleResult = _title.originalTextWithEntities(selection, ExpandLinksAll);
|
|
auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection), ExpandLinksAll);
|
|
if (titleResult.text.isEmpty()) {
|
|
return descriptionResult;
|
|
} else if (descriptionResult.text.isEmpty()) {
|
|
return titleResult;
|
|
}
|
|
|
|
titleResult.text += '\n';
|
|
appendTextWithEntities(titleResult, std_::move(descriptionResult));
|
|
return titleResult;
|
|
}
|
|
|
|
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<QSslError>&)), 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<QNetworkReply*, LocationData*>::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<QNetworkReply*, LocationData*>::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<QNetworkReply*, LocationData*>::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<HistoryMessageForwarded>() && !_parent->Has<HistoryMessageReply>()) {
|
|
_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<HistoryMessageForwarded>() && !_parent->Has<HistoryMessageReply>()) {
|
|
_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<HistoryMessageForwarded>() && !_parent->Has<HistoryMessageReply>()) {
|
|
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<HistoryMessageForwarded>() && !_parent->Has<HistoryMessageReply>()) {
|
|
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.originalText(AllTextSelection);
|
|
}
|
|
|
|
TextWithEntities HistoryLocation::selectedText(TextSelection selection) const {
|
|
if (selection == FullSelection) {
|
|
TextWithEntities result = { qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"), EntitiesInText() };
|
|
auto info = selectedText(AllTextSelection);
|
|
if (!info.text.isEmpty()) {
|
|
appendTextWithEntities(result, std_::move(info));
|
|
result.text.append('\n');
|
|
}
|
|
result.text += _link->dragText();
|
|
return result;
|
|
}
|
|
|
|
auto titleResult = _title.originalTextWithEntities(selection);
|
|
auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection));
|
|
if (titleResult.text.isEmpty()) {
|
|
return descriptionResult;
|
|
} else if (descriptionResult.text.isEmpty()) {
|
|
return titleResult;
|
|
}
|
|
titleResult.text += '\n';
|
|
appendTextWithEntities(titleResult, std_::move(descriptionResult));
|
|
return titleResult;
|
|
}
|
|
|
|
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 HistoryMessageEdited::create(const QDateTime &editDate, const QDateTime &date) {
|
|
_editDate = editDate;
|
|
|
|
QString time = date.toString(cTimeFormat());
|
|
_edited.setText(st::msgDateFont, lang(lng_edited) + ' ' + time, _textNameOptions);
|
|
}
|
|
|
|
int HistoryMessageEdited::maxWidth() const {
|
|
return _edited.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<HistoryMessageForwarded>()) {
|
|
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;
|
|
if (msg.has_edit_date()) config.editDate = ::date(msg.vedit_date);
|
|
|
|
createComponents(config);
|
|
|
|
QString text(textClean(qs(msg.vmessage)));
|
|
initMedia(msg.has_media() ? (&msg.vmedia) : nullptr, text);
|
|
|
|
TextWithEntities textWithEntities = { text, EntitiesInText() };
|
|
if (msg.has_entities()) {
|
|
textWithEntities.entities = entitiesFromMTP(msg.ventities.c_vector().v);
|
|
}
|
|
setText(textWithEntities);
|
|
}
|
|
|
|
namespace {
|
|
|
|
MTPDmessage::Flags newForwardedFlags(PeerData *p, int32 from, HistoryMessage *fwd) {
|
|
MTPDmessage::Flags result = newMessageFlags(p) | MTPDmessage::Flag::f_fwd_from;
|
|
if (from) {
|
|
result |= MTPDmessage::Flag::f_from_id;
|
|
}
|
|
if (fwd->Has<HistoryMessageVia>()) {
|
|
result |= MTPDmessage::Flag::f_via_bot_id;
|
|
}
|
|
if (!p->isChannel()) {
|
|
if (HistoryMedia *media = fwd->getMedia()) {
|
|
if (media->type() == MediaTypeVoiceFile) {
|
|
result |= MTPDmessage::Flag::f_media_unread;
|
|
// } else if (media->type() == MediaTypeVideo) {
|
|
// result |= MTPDmessage::flag_media_unread;
|
|
}
|
|
}
|
|
}
|
|
if (fwd->hasViews()) {
|
|
result |= MTPDmessage::Flag::f_views;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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());
|
|
}
|
|
|
|
HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities)
|
|
: HistoryItem(history, id, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) {
|
|
createComponentsHelper(flags, replyTo, viaBotId, MTPnullMarkup);
|
|
|
|
setText(textWithEntities);
|
|
}
|
|
|
|
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(TextWithEntities());
|
|
}
|
|
|
|
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(TextWithEntities());
|
|
}
|
|
|
|
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 (wasEdited()) {
|
|
mask |= HistoryMessageEdited::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<HistoryMessageReply>()) {
|
|
reply->replyToMsgId = config.replyTo;
|
|
if (!reply->updateData(this) && App::api()) {
|
|
App::api()->requestMessageData(history()->peer->asChannel(), reply->replyToMsgId, std_::make_unique<HistoryDependentItemCallback>(fullId()));
|
|
}
|
|
}
|
|
if (auto via = Get<HistoryMessageVia>()) {
|
|
via->create(config.viaBotId);
|
|
}
|
|
if (auto views = Get<HistoryMessageViews>()) {
|
|
views->_views = config.viewsCount;
|
|
}
|
|
if (auto msgsigned = Get<HistoryMessageSigned>()) {
|
|
msgsigned->create(_from->asUser(), date);
|
|
}
|
|
if (auto edited = Get<HistoryMessageEdited>()) {
|
|
edited->create(config.editDate, date);
|
|
}
|
|
if (auto fwd = Get<HistoryMessageForwarded>()) {
|
|
fwd->_authorOriginal = App::peer(config.authorIdOriginal);
|
|
fwd->_fromOriginal = App::peer(config.fromIdOriginal);
|
|
fwd->_originalId = config.originalId;
|
|
}
|
|
if (auto markup = Get<HistoryMessageReplyMarkup>()) {
|
|
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<HistoryMessageSigned>()) {
|
|
_timeWidth = msgsigned->maxWidth();
|
|
} else if (auto edited = Get<HistoryMessageEdited>()) {
|
|
_timeWidth = edited->maxWidth();
|
|
} else {
|
|
_timeText = date.toString(cTimeFormat());
|
|
_timeWidth = st::msgDateFont->width(_timeText);
|
|
}
|
|
if (auto views = Get<HistoryMessageViews>()) {
|
|
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<HistoryMessageForwarded>();
|
|
auto via = Get<HistoryMessageVia>();
|
|
if (fwd) {
|
|
fwd->create(via);
|
|
}
|
|
|
|
if (_media) {
|
|
_media->initDimensions();
|
|
if (_media->isDisplayed()) {
|
|
if (_text.hasSkipBlock()) {
|
|
_text.removeSkipBlock();
|
|
_textWidth = -1;
|
|
_textHeight = 0;
|
|
}
|
|
} else if (!_text.hasSkipBlock()) {
|
|
_text.setSkipBlock(skipBlockWidth(), skipBlockHeight());
|
|
_textWidth = -1;
|
|
_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<HistoryMessageReply>()) {
|
|
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<KeyboardStyle>(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<HistoryMessageForwarded>()) {
|
|
if (auto via = Get<HistoryMessageVia>()) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
if (message.has_edit_date()) {
|
|
_flags |= MTPDmessage::Flag::f_edit_date;
|
|
if (!Has<HistoryMessageEdited>()) {
|
|
AddComponents(HistoryMessageEdited::Bit());
|
|
}
|
|
Get<HistoryMessageEdited>()->create(::date(message.vedit_date), date);
|
|
initTime();
|
|
}
|
|
|
|
TextWithEntities textWithEntities = { qs(message.vmessage), EntitiesInText() };
|
|
if (message.has_entities()) {
|
|
textWithEntities.entities = entitiesFromMTP(message.ventities.c_vector().v);
|
|
}
|
|
setText(textWithEntities);
|
|
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<HistoryMessageReplyMarkup>()) {
|
|
keyboard->oldTop = keyboardTop;
|
|
}
|
|
}
|
|
|
|
App::historyUpdateDependent(this);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
TextWithEntities HistoryMessage::selectedText(TextSelection selection) const {
|
|
TextWithEntities result, textResult, mediaResult;
|
|
if (selection == FullSelection) {
|
|
textResult = _text.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
|
|
} else {
|
|
textResult = _text.originalTextWithEntities(selection, ExpandLinksAll);
|
|
}
|
|
if (_media) {
|
|
mediaResult = _media->selectedText(toMediaSelection(selection));
|
|
}
|
|
if (textResult.text.isEmpty()) {
|
|
result = mediaResult;
|
|
} else if (mediaResult.text.isEmpty()) {
|
|
result = textResult;
|
|
} else {
|
|
result.text = textResult.text + qstr("\n\n");
|
|
result.entities = textResult.entities;
|
|
appendTextWithEntities(result, std_::move(mediaResult));
|
|
}
|
|
if (auto fwd = Get<HistoryMessageForwarded>()) {
|
|
if (selection == FullSelection) {
|
|
auto fwdinfo = fwd->_text.originalTextWithEntities(AllTextSelection, ExpandLinksAll);
|
|
TextWithEntities wrapped;
|
|
wrapped.text.reserve(fwdinfo.text.size() + 4 + result.text.size());
|
|
wrapped.entities.reserve(fwdinfo.entities.size() + result.entities.size());
|
|
wrapped.text.append('[');
|
|
appendTextWithEntities(wrapped, std_::move(fwdinfo));
|
|
wrapped.text.append(qsl("]\n"));
|
|
appendTextWithEntities(wrapped, std_::move(result));
|
|
result = wrapped;
|
|
}
|
|
}
|
|
if (auto reply = Get<HistoryMessageReply>()) {
|
|
if (selection == FullSelection && reply->replyToMsg) {
|
|
TextWithEntities wrapped;
|
|
wrapped.text.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.text.size());
|
|
wrapped.text.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n"));
|
|
appendTextWithEntities(wrapped, std_::move(result));
|
|
result = wrapped;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QString HistoryMessage::inDialogsText() const {
|
|
return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.originalText(AllTextSelection, 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 = -1;
|
|
_textHeight = 0;
|
|
} else if (mediaWasDisplayed && (!_media || !_media->isDisplayed())) {
|
|
_text.setSkipBlock(skipBlockWidth(), skipBlockHeight());
|
|
_textWidth = -1;
|
|
_textHeight = 0;
|
|
}
|
|
}
|
|
|
|
void HistoryMessage::setText(const TextWithEntities &textWithEntities) {
|
|
textstyleSet(&((out() && !isPost()) ? st::outTextStyle : st::inTextStyle));
|
|
if (_media && _media->isDisplayed()) {
|
|
_text.setMarkedText(st::msgFont, textWithEntities, itemTextOptions(this));
|
|
} else {
|
|
_text.setMarkedText(st::msgFont, { textWithEntities.text + skipBlock(), textWithEntities.entities }, itemTextOptions(this));
|
|
}
|
|
textstyleRestore();
|
|
|
|
for_const (auto &entity, textWithEntities.entities) {
|
|
auto type = entity.type();
|
|
if (type == EntityInTextUrl || type == EntityInTextCustomUrl || type == EntityInTextEmail) {
|
|
_flags |= MTPDmessage_ClientFlag::f_has_text_links;
|
|
break;
|
|
}
|
|
}
|
|
_textWidth = -1;
|
|
_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<HistoryMessageReplyMarkup>()) {
|
|
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<HistoryMessageReplyMarkup>()) {
|
|
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<HistoryMessageReplyMarkup>()) {
|
|
AddComponents(HistoryMessageReplyMarkup::Bit());
|
|
}
|
|
Get<HistoryMessageReplyMarkup>()->create(*markup);
|
|
setPendingInitDimensions();
|
|
|
|
Notify::replyMarkupUpdated(this);
|
|
}
|
|
}
|
|
|
|
TextWithEntities HistoryMessage::originalText() const {
|
|
if (emptyText()) {
|
|
return { QString(), EntitiesInText() };
|
|
}
|
|
return _text.originalTextWithEntities();
|
|
}
|
|
|
|
bool HistoryMessage::textHasLinks() const {
|
|
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<HistoryMessageSigned>()) {
|
|
msgsigned->_signature.drawElided(p, dateX, dateY, _timeWidth);
|
|
} else if (auto edited = Get<HistoryMessageEdited>()) {
|
|
edited->_edited.drawElided(p, dateX, dateY, _timeWidth);
|
|
} else {
|
|
p.drawText(dateX, dateY + st::msgDateFont->ascent, _timeText);
|
|
}
|
|
|
|
QPoint iconPos;
|
|
const style::sprite *iconRect = nullptr;
|
|
if (auto views = Get<HistoryMessageViews>()) {
|
|
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.drawSprite(iconPos, *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.drawSprite(iconPos, *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.drawSprite(iconPos, *iconRect);
|
|
}
|
|
}
|
|
|
|
void HistoryMessage::setViewsCount(int32 count) {
|
|
auto views = Get<HistoryMessageViews>();
|
|
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 = -1;
|
|
_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 = -1;
|
|
_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<HistoryMessageDate>()) {
|
|
dateh = date->height();
|
|
if (r.intersects(QRect(0, 0, _history->width, dateh))) {
|
|
date->paint(p, 0, _history->width);
|
|
}
|
|
}
|
|
if (auto unreadbar = Get<HistoryMessageUnreadBar>()) {
|
|
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<HistoryMessageReply>();
|
|
if (reply) {
|
|
reply->checkNameUpdate();
|
|
}
|
|
|
|
if (bubble) {
|
|
auto fwd = Get<HistoryMessageForwarded>();
|
|
auto via = Get<HistoryMessageVia>();
|
|
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<HistoryMessageForwarded>();
|
|
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<HistoryMessageReply>()) {
|
|
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<HistoryMessageForwarded>()) {
|
|
if (auto via = Get<HistoryMessageVia>()) {
|
|
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<HistoryMessageReply>()) {
|
|
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<HistoryMessageReplyMarkup>()) {
|
|
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<HistoryMessageForwarded>();
|
|
auto reply = Get<HistoryMessageReply>();
|
|
auto via = Get<HistoryMessageVia>();
|
|
|
|
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) {
|
|
int32 l = 0, w = 0;
|
|
countPositionAndSize(l, w);
|
|
|
|
if (emptyText() && !displayFromName() && !Has<HistoryMessageVia>()) {
|
|
_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(w - 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<HistoryMessageForwarded>();
|
|
auto via = Get<HistoryMessageVia>();
|
|
auto reply = Get<HistoryMessageReply>();
|
|
|
|
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<HistoryMessageReply>()) {
|
|
reply->clearData(this);
|
|
}
|
|
}
|
|
|
|
void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
|
QList<ClickHandlerPtr> 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<PeerOpenClickHandler>(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<PeerOpenClickHandler>(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<PeerOpenClickHandler>(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<PeerOpenClickHandler>(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<HistoryServicePinned>();
|
|
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<PeerOpenClickHandler>(_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<HistoryServicePinned>();
|
|
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) {
|
|
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<HistoryServicePinned>();
|
|
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().text;
|
|
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<PeerOpenClickHandler>(_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<HistoryServicePinned>()->msgId = msg.vreply_to_msg_id.v;
|
|
if (!updatePinned() && App::api()) {
|
|
App::api()->requestMessageData(history->peer->asChannel(), pinnedMsgId, std_::make_unique<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();
|
|
}
|
|
|
|
TextWithEntities HistoryService::selectedText(TextSelection selection) const {
|
|
return _text.originalTextWithEntities((selection == FullSelection) ? AllTextSelection : selection);
|
|
}
|
|
|
|
QString HistoryService::inDialogsText() const {
|
|
return _text.originalText(AllTextSelection, 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();
|
|
setPendingInitDimensions();
|
|
_textWidth = -1;
|
|
_textHeight = 0;
|
|
}
|
|
|
|
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<HistoryMessageDate>()) {
|
|
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<HistoryMessageUnreadBar>()) {
|
|
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<HistoryMessageUnreadBar>()) {
|
|
_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<HistoryMessageUnreadBar>()) {
|
|
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<HistoryMessageUnreadBar>()) {
|
|
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.originalText();
|
|
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<HistoryServicePinned>()) {
|
|
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<PeerOpenClickHandler>(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());
|
|
}
|
|
}
|