tdesktop/Telegram/SourceFiles/calls/calls_box_controller.cpp

545 lines
15 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/calls_box_controller.h"
#include "lang/lang_keys.h"
#include "ui/effects/ripple_animation.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/popup_menu.h"
#include "core/application.h"
#include "calls/calls_instance.h"
#include "history/history.h"
#include "history/history_item.h"
#include "mainwidget.h"
#include "window/window_session_controller.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_media_types.h"
#include "data/data_user.h"
#include "boxes/delete_messages_box.h"
#include "base/unixtime.h"
#include "api/api_updates.h"
#include "apiwrap.h"
#include "styles/style_layers.h" // st::boxLabel.
#include "styles/style_calls.h"
#include "styles/style_boxes.h"
namespace Calls {
namespace {
constexpr auto kFirstPageCount = 20;
constexpr auto kPerPageCount = 100;
} // namespace
class BoxController::Row : public PeerListRow {
public:
Row(not_null<HistoryItem*> item);
enum class Type {
Out,
In,
Missed,
};
enum class CallType {
Voice,
Video,
};
bool canAddItem(not_null<const HistoryItem*> item) const {
return (ComputeType(item) == _type)
&& (!hasItems() || _items.front()->history() == item->history())
&& (ItemDateTime(item).date() == _date);
}
void addItem(not_null<HistoryItem*> item) {
Expects(canAddItem(item));
_items.push_back(item);
ranges::sort(_items, [](not_null<HistoryItem*> a, auto b) {
return (a->id > b->id);
});
refreshStatus();
}
void itemRemoved(not_null<const HistoryItem*> item) {
if (hasItems() && item->id >= minItemId() && item->id <= maxItemId()) {
_items.erase(std::remove(_items.begin(), _items.end(), item), _items.end());
refreshStatus();
}
}
[[nodiscard]] bool hasItems() const {
return !_items.empty();
}
[[nodiscard]] MsgId minItemId() const {
Expects(hasItems());
return _items.back()->id;
}
[[nodiscard]] MsgId maxItemId() const {
Expects(hasItems());
return _items.front()->id;
}
[[nodiscard]] const std::vector<not_null<HistoryItem*>> &items() const {
return _items;
}
void paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) override;
void rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) override;
void rightActionStopLastRipple() override;
int nameIconWidth() const override {
return 0;
}
QSize rightActionSize() const override {
return peer()->isUser() ? QSize(_st->width, _st->height) : QSize();
}
QMargins rightActionMargins() const override {
return QMargins(
0,
0,
st::defaultPeerListItem.photoPosition.x(),
0);
}
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
private:
void refreshStatus() override;
static Type ComputeType(not_null<const HistoryItem*> item);
static CallType ComputeCallType(not_null<const HistoryItem*> item);
std::vector<not_null<HistoryItem*>> _items;
QDate _date;
Type _type;
not_null<const style::IconButton*> _st;
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
};
BoxController::Row::Row(not_null<HistoryItem*> item)
: PeerListRow(item->history()->peer, item->id.bare)
, _items(1, item)
, _date(ItemDateTime(item).date())
, _type(ComputeType(item))
, _st(ComputeCallType(item) == CallType::Voice
? &st::callReDial
: &st::callCameraReDial) {
refreshStatus();
}
void BoxController::Row::paintStatusText(Painter &p, const style::PeerListItem &st, int x, int y, int availableWidth, int outerWidth, bool selected) {
auto icon = ([this] {
switch (_type) {
case Type::In: return &st::callArrowIn;
case Type::Out: return &st::callArrowOut;
case Type::Missed: return &st::callArrowMissed;
}
Unexpected("_type in Calls::BoxController::Row::paintStatusText().");
})();
icon->paint(p, x + st::callArrowPosition.x(), y + st::callArrowPosition.y(), outerWidth);
auto shift = st::callArrowPosition.x() + icon->width() + st::callArrowSkip;
x += shift;
availableWidth -= shift;
PeerListRow::paintStatusText(p, st, x, y, availableWidth, outerWidth, selected);
}
void BoxController::Row::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
auto size = rightActionSize();
if (_actionRipple) {
_actionRipple->paint(
p,
x + _st->rippleAreaPosition.x(),
y + _st->rippleAreaPosition.y(),
outerWidth);
if (_actionRipple->empty()) {
_actionRipple.reset();
}
}
_st->icon.paintInCenter(
p,
style::rtlrect(x, y, size.width(), size.height(), outerWidth));
}
void BoxController::Row::refreshStatus() {
if (!hasItems()) {
return;
}
auto text = [this] {
auto time = ItemDateTime(_items.front()).time().toString(cTimeFormat());
auto today = QDateTime::currentDateTime().date();
if (_date == today) {
return tr::lng_call_box_status_today(tr::now, lt_time, time);
} else if (_date.addDays(1) == today) {
return tr::lng_call_box_status_yesterday(tr::now, lt_time, time);
}
return tr::lng_call_box_status_date(tr::now, lt_date, langDayOfMonthFull(_date), lt_time, time);
};
setCustomStatus((_items.size() > 1)
? tr::lng_call_box_status_group(
tr::now,
lt_amount,
QString::number(_items.size()),
lt_status,
text())
: text());
}
BoxController::Row::Type BoxController::Row::ComputeType(
not_null<const HistoryItem*> item) {
if (item->out()) {
return Type::Out;
} else if (auto media = item->media()) {
if (const auto call = media->call()) {
const auto reason = call->finishReason;
if (reason == Data::Call::FinishReason::Busy
|| reason == Data::Call::FinishReason::Missed) {
return Type::Missed;
}
}
}
return Type::In;
}
BoxController::Row::CallType BoxController::Row::ComputeCallType(
not_null<const HistoryItem*> item) {
if (auto media = item->media()) {
if (const auto call = media->call()) {
if (call->video) {
return CallType::Video;
}
}
}
return CallType::Voice;
}
void BoxController::Row::rightActionAddRipple(QPoint point, Fn<void()> updateCallback) {
if (!_actionRipple) {
auto mask = Ui::RippleAnimation::ellipseMask(
QSize(_st->rippleAreaSize, _st->rippleAreaSize));
_actionRipple = std::make_unique<Ui::RippleAnimation>(
_st->ripple,
std::move(mask),
std::move(updateCallback));
}
_actionRipple->add(point - _st->rippleAreaPosition);
}
void BoxController::Row::rightActionStopLastRipple() {
if (_actionRipple) {
_actionRipple->lastStop();
}
}
BoxController::BoxController(not_null<Window::SessionController*> window)
: _window(window)
, _api(&_window->session().mtp()) {
}
Main::Session &BoxController::session() const {
return _window->session();
}
void BoxController::prepare() {
session().data().itemRemoved(
) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
if (const auto row = rowForItem(item)) {
row->itemRemoved(item);
if (!row->hasItems()) {
delegate()->peerListRemoveRow(row);
if (!delegate()->peerListFullRowsCount()) {
refreshAbout();
}
}
delegate()->peerListRefreshRows();
}
}, lifetime());
session().changes().messageUpdates(
Data::MessageUpdate::Flag::NewAdded
) | rpl::filter([=](const Data::MessageUpdate &update) {
const auto media = update.item->media();
return (media != nullptr) && (media->call() != nullptr);
}) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
insertRow(update.item, InsertWay::Prepend);
}, lifetime());
delegate()->peerListSetTitle(tr::lng_call_box_title());
setDescriptionText(tr::lng_contacts_loading(tr::now));
delegate()->peerListRefreshRows();
loadMoreRows();
}
void BoxController::loadMoreRows() {
if (_loadRequestId || _allLoaded) {
return;
}
_loadRequestId = _api.request(MTPmessages_Search(
MTP_flags(0),
MTP_inputPeerEmpty(),
MTP_string(), // q
MTP_inputPeerEmpty(),
MTPint(), // top_msg_id
MTP_inputMessagesFilterPhoneCalls(MTP_flags(0)),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(_offsetId),
MTP_int(0), // add_offset
MTP_int(_offsetId ? kFirstPageCount : kPerPageCount),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0) // hash
)).done([this](const MTPmessages_Messages &result) {
_loadRequestId = 0;
auto handleResult = [&](auto &data) {
session().data().processUsers(data.vusers());
session().data().processChats(data.vchats());
receivedCalls(data.vmessages().v);
};
switch (result.type()) {
case mtpc_messages_messages: handleResult(result.c_messages_messages()); _allLoaded = true; break;
case mtpc_messages_messagesSlice: handleResult(result.c_messages_messagesSlice()); break;
case mtpc_messages_channelMessages: {
LOG(("API Error: received messages.channelMessages! (Calls::BoxController::preloadRows)"));
handleResult(result.c_messages_channelMessages());
} break;
case mtpc_messages_messagesNotModified: {
LOG(("API Error: received messages.messagesNotModified! (Calls::BoxController::preloadRows)"));
} break;
default: Unexpected("Type of messages.Messages (Calls::BoxController::preloadRows)");
}
}).fail([this] {
_loadRequestId = 0;
}).send();
}
base::unique_qptr<Ui::PopupMenu> BoxController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
const auto &items = static_cast<Row*>(row.get())->items();
const auto session = &this->session();
const auto ids = session->data().itemsToIds(items);
auto result = base::make_unique_q<Ui::PopupMenu>(parent);
result->addAction(tr::lng_context_delete_selected(tr::now), [=] {
_window->show(
Box<DeleteMessagesBox>(session, base::duplicate(ids)),
Ui::LayerOption::KeepOther);
});
return result;
}
void BoxController::refreshAbout() {
setDescriptionText(delegate()->peerListFullRowsCount() ? QString() : tr::lng_call_box_about(tr::now));
}
void BoxController::rowClicked(not_null<PeerListRow*> row) {
const auto itemsRow = static_cast<Row*>(row.get());
const auto itemId = itemsRow->maxItemId();
const auto window = _window;
crl::on_main(window, [=, peer = row->peer()] {
window->showPeerHistory(
peer,
Window::SectionShow::Way::ClearStack,
itemId);
});
}
void BoxController::rowRightActionClicked(not_null<PeerListRow*> row) {
auto user = row->peer()->asUser();
Assert(user != nullptr);
Core::App().calls().startOutgoingCall(user, false);
}
void BoxController::receivedCalls(const QVector<MTPMessage> &result) {
if (result.empty()) {
_allLoaded = true;
}
for (const auto &message : result) {
const auto msgId = IdFromMessage(message);
const auto peerId = PeerFromMessage(message);
if (const auto peer = session().data().peerLoaded(peerId)) {
const auto item = session().data().addNewMessage(
message,
MessageFlags(),
NewMessageType::Existing);
insertRow(item, InsertWay::Append);
} else {
LOG(("API Error: a search results with not loaded peer %1"
).arg(peerId.value));
}
_offsetId = msgId;
}
refreshAbout();
delegate()->peerListRefreshRows();
}
bool BoxController::insertRow(
not_null<HistoryItem*> item,
InsertWay way) {
if (auto row = rowForItem(item)) {
if (row->canAddItem(item)) {
row->addItem(item);
return false;
}
}
(way == InsertWay::Append)
? delegate()->peerListAppendRow(createRow(item))
: delegate()->peerListPrependRow(createRow(item));
delegate()->peerListSortRows([](
const PeerListRow &a,
const PeerListRow &b) {
return static_cast<const Row&>(a).maxItemId()
> static_cast<const Row&>(b).maxItemId();
});
return true;
}
BoxController::Row *BoxController::rowForItem(not_null<const HistoryItem*> item) {
auto v = delegate();
if (auto fullRowsCount = v->peerListFullRowsCount()) {
auto itemId = item->id;
auto lastRow = static_cast<Row*>(v->peerListRowAt(fullRowsCount - 1).get());
if (itemId < lastRow->minItemId()) {
return lastRow;
}
auto firstRow = static_cast<Row*>(v->peerListRowAt(0).get());
if (itemId > firstRow->maxItemId()) {
return firstRow;
}
// Binary search. Invariant:
// 1. rowAt(left)->maxItemId() >= itemId.
// 2. (left + 1 == fullRowsCount) OR rowAt(left + 1)->maxItemId() < itemId.
auto left = 0;
auto right = fullRowsCount;
while (left + 1 < right) {
auto middle = (right + left) / 2;
auto middleRow = static_cast<Row*>(v->peerListRowAt(middle).get());
if (middleRow->maxItemId() >= itemId) {
left = middle;
} else {
right = middle;
}
}
auto result = static_cast<Row*>(v->peerListRowAt(left).get());
// Check for rowAt(left)->minItemId > itemId > rowAt(left + 1)->maxItemId.
// In that case we sometimes need to return rowAt(left + 1), not rowAt(left).
if (result->minItemId() > itemId && left + 1 < fullRowsCount) {
auto possibleResult = static_cast<Row*>(v->peerListRowAt(left + 1).get());
Assert(possibleResult->maxItemId() < itemId);
if (possibleResult->canAddItem(item)) {
return possibleResult;
}
}
return result;
}
return nullptr;
}
std::unique_ptr<PeerListRow> BoxController::createRow(
not_null<HistoryItem*> item) const {
return std::make_unique<Row>(item);
}
void ClearCallsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window) {
const auto weak = Ui::MakeWeak(box);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_call_box_clear_sure(),
st::boxLabel),
st::boxPadding);
const auto revokeCheckbox = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::lng_delete_for_everyone_check(tr::now),
false,
st::defaultBoxCheckbox),
style::margins(
st::boxPadding.left(),
st::boxPadding.bottom(),
st::boxPadding.right(),
st::boxPadding.bottom()));
const auto api = &window->session().api();
const auto sendRequest = [=](bool revoke, auto self) -> void {
using Flag = MTPmessages_DeletePhoneCallHistory::Flag;
api->request(MTPmessages_DeletePhoneCallHistory(
MTP_flags(revoke ? Flag::f_revoke : Flag(0))
)).done([=](const MTPmessages_AffectedFoundMessages &result) {
result.match([&](
const MTPDmessages_affectedFoundMessages &data) {
api->applyUpdates(MTP_updates(
MTP_vector<MTPUpdate>(
1,
MTP_updateDeleteMessages(
data.vmessages(),
data.vpts(),
data.vpts_count())),
MTP_vector<MTPUser>(),
MTP_vector<MTPChat>(),
MTP_int(base::unixtime::now()),
MTP_int(0)));
const auto offset = data.voffset().v;
if (offset > 0) {
self(revoke, self);
} else {
api->session().data().destroyAllCallItems();
if (const auto strong = weak.data()) {
strong->closeBox();
}
}
});
}).send();
};
box->addButton(tr::lng_call_box_clear_button(), [=] {
sendRequest(revokeCheckbox->checked(), sendRequest);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Calls