diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 215698590..c54dd9165 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -264,6 +264,8 @@ PRIVATE calls/calls_box_controller.h calls/calls_call.cpp calls/calls_call.h + calls/calls_group_call.cpp + calls/calls_group_call.h calls/calls_emoji_fingerprint.cpp calls/calls_emoji_fingerprint.h calls/calls_instance.cpp @@ -385,6 +387,8 @@ PRIVATE data/data_file_origin.h data/data_flags.h data/data_game.h + data/data_group_call.cpp + data/data_group_call.h data/data_groups.cpp data/data_groups.h data/data_histories.cpp @@ -536,6 +540,8 @@ PRIVATE history/view/history_view_cursor_state.h history/view/history_view_element.cpp history/view/history_view_element.h + history/view/history_view_group_call_tracker.cpp + history/view/history_view_group_call_tracker.h history/view/history_view_list_widget.cpp history/view/history_view_list_widget.h history/view/history_view_message.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b6dddb980..68a98cebf 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1342,7 +1342,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_broadcast_silent_ph" = "Silent broadcast..."; "lng_send_anonymous_ph" = "Send anonymously..."; "lng_record_cancel" = "Release outside this field to cancel"; -"lng_record_lock_cancel" = "Click outside of circle to cancel"; +"lng_record_lock_cancel" = "Click outside of the circle to cancel"; "lng_record_lock_cancel_sure" = "Are you sure you want to stop recording and discard your voice message?"; "lng_record_lock_discard" = "Discard"; "lng_will_be_notified" = "Members will be notified when you post"; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index f365ecce8..9babc7347 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1796,7 +1796,9 @@ void Updates::feedUpdate(const MTPUpdate &update) { } break; case mtpc_updatePhoneCall: - case mtpc_updatePhoneCallSignalingData: { + case mtpc_updatePhoneCallSignalingData: + case mtpc_updateGroupCallParticipants: + case mtpc_updateGroupCall: { Core::App().calls().handleUpdate(&session(), update); } break; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 4ba508eca..cb552e607 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -289,7 +289,7 @@ void Call::startIncoming() { } void Call::answer() { - _delegate->requestPermissionsOrFail(crl::guard(this, [=] { + _delegate->callRequestPermissionsOrFail(crl::guard(this, [=] { actuallyAnswer(); })); } @@ -776,11 +776,11 @@ void Call::createAndStartController(const MTPDphoneCall &call) { auto callLogPath = callLogFolder + qsl("/last_call_log.txt"); auto callLogNative = QDir::toNativeSeparators(callLogPath); #ifdef Q_OS_WIN - descriptor.config.logPath = callLogNative.toStdWString(); + descriptor.config.logPath.data = callLogNative.toStdWString(); #else // Q_OS_WIN const auto callLogUtf = QFile::encodeName(callLogNative); - descriptor.config.logPath.resize(callLogUtf.size()); - ranges::copy(callLogUtf, descriptor.config.logPath.begin()); + descriptor.config.logPath.data.resize(callLogUtf.size()); + ranges::copy(callLogUtf, descriptor.config.logPath.data.begin()); #endif // Q_OS_WIN QFile(callLogPath).remove(); QDir().mkpath(callLogFolder); diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 139a037ad..2075be7ab 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -68,7 +68,7 @@ public: Ended, }; virtual void playSound(Sound sound) = 0; - virtual void requestPermissionsOrFail(Fn onSuccess) = 0; + virtual void callRequestPermissionsOrFail(Fn onSuccess) = 0; virtual auto getVideoCapture() -> std::shared_ptr = 0; diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp new file mode 100644 index 000000000..0dcd19e6b --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -0,0 +1,329 @@ +/* +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_group_call.h" + +#include "main/main_session.h" +//#include "main/main_account.h" +//#include "main/main_app_config.h" +#include "apiwrap.h" +#include "lang/lang_keys.h" +#include "boxes/confirm_box.h" +//#include "boxes/rate_call_box.h" +//#include "calls/calls_instance.h" +//#include "base/openssl_help.h" +//#include "mtproto/mtproto_dh_utils.h" +//#include "mtproto/mtproto_config.h" +//#include "core/application.h" +//#include "core/core_settings.h" +//#include "media/audio/media_audio_track.h" +//#include "base/platform/base_platform_info.h" +//#include "calls/calls_panel.h" +//#include "webrtc/webrtc_video_track.h" +//#include "webrtc/webrtc_media_devices.h" +#include "data/data_channel.h" +//#include "data/data_session.h" +//#include "facades.h" + +#include + +#include +#include +#include + +namespace tgcalls { +class GroupInstanceImpl; +} // namespace tgcalls + +namespace Calls { + +GroupCall::GroupCall( + not_null delegate, + not_null channel, + const MTPInputGroupCall &inputCall) +: _delegate(delegate) +, _channel(channel) +, _api(&_channel->session().mtp()) { + if (inputCall.c_inputGroupCall().vid().v) { + join(inputCall); + } else { + start(); + } +} + +GroupCall::~GroupCall() { + destroyController(); +} + +void GroupCall::start() { + const auto randomId = rand_value(); + _api.request(MTPphone_CreateGroupCall( + _channel->inputChannel, + MTP_int(randomId) + )).done([=](const MTPUpdates &result) { + _channel->session().api().applyUpdates(result); + }).fail([=](const RPCError &error) { + int a = error.code(); + }).send(); +} + +void GroupCall::join(const MTPInputGroupCall &inputCall) { + inputCall.match([&](const MTPDinputGroupCall &data) { + _id = data.vid().v; + _accessHash = data.vaccess_hash().v; + createAndStartController(); + const auto weak = base::make_weak(this); + _instance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) { + crl::on_main(weak, [=, payload = std::move(payload)]{ + auto fingerprints = QJsonArray(); + for (const auto print : payload.fingerprints) { + auto object = QJsonObject(); + object.insert("hash", QString::fromStdString(print.hash)); + object.insert("setup", QString::fromStdString(print.setup)); + object.insert( + "fingerprint", + QString::fromStdString(print.fingerprint)); + fingerprints.push_back(object); + } + + auto root = QJsonObject(); + root.insert("ufrag", QString::fromStdString(payload.ufrag)); + root.insert("pwd", QString::fromStdString(payload.pwd)); + root.insert("fingerprints", fingerprints); + root.insert("ssrc", int(payload.ssrc)); + + const auto json = QJsonDocument(root).toJson( + QJsonDocument::Compact); + _api.request(MTPphone_JoinGroupCall( + MTP_flags(_muted.current() + ? MTPphone_JoinGroupCall::Flag::f_muted + : MTPphone_JoinGroupCall::Flag(0)), + inputCall, + MTP_dataJSON(MTP_bytes(json)) + )).done([=](const MTPUpdates &updates) { + _channel->session().api().applyUpdates(updates); + }).fail([=](const RPCError &error) { + int a = error.code(); + }).send(); + }); + }); + }); +} + +void GroupCall::setMuted(bool mute) { + _muted = mute; + if (_instance) { + _instance->setIsMuted(mute); + } +} + +bool GroupCall::handleUpdate(const MTPGroupCall &call) { + return call.match([&](const MTPDgroupCall &data) { + if (_id != data.vid().v + || _accessHash != data.vaccess_hash().v + || !_instance) { + return false; + } + if (const auto params = data.vparams()) { + params->match([&](const MTPDdataJSON &data) { + auto error = QJsonParseError{ 0, QJsonParseError::NoError }; + const auto document = QJsonDocument::fromJson( + data.vdata().v, + &error); + if (error.error != QJsonParseError::NoError) { + LOG(("API Error: " + "Failed to parse group call params, error: %1." + ).arg(error.errorString())); + return; + } else if (!document.isObject()) { + LOG(("API Error: " + "Not an object received in group call params.")); + return; + } + const auto readString = []( + const QJsonObject &object, + const char *key) { + return object.value(key).toString().toStdString(); + }; + const auto root = document.object().value("transport").toObject(); + auto payload = tgcalls::GroupJoinResponsePayload(); + payload.ufrag = readString(root, "ufrag"); + payload.pwd = readString(root, "pwd"); + const auto prints = root.value("fingerprints").toArray(); + const auto candidates = root.value("candidates").toArray(); + for (const auto &print : prints) { + const auto object = print.toObject(); + payload.fingerprints.push_back(tgcalls::GroupJoinPayloadFingerprint{ + .hash = readString(object, "hash"), + .setup = readString(object, "setup"), + .fingerprint = readString(object, "fingerprint"), + }); + } + for (const auto &candidate : candidates) { + const auto object = candidate.toObject(); + payload.candidates.push_back(tgcalls::GroupJoinResponseCandidate{ + .port = readString(object, "port"), + .protocol = readString(object, "protocol"), + .network = readString(object, "network"), + .generation = readString(object, "generation"), + .id = readString(object, "id"), + .component = readString(object, "component"), + .foundation = readString(object, "foundation"), + .priority = readString(object, "priority"), + .ip = readString(object, "ip"), + .type = readString(object, "type"), + .tcpType = readString(object, "tcpType"), + .relAddr = readString(object, "relAddr"), + .relPort = readString(object, "relPort"), + }); + } + _instance->setJoinResponsePayload(payload); + _api.request(MTPphone_GetGroupParticipants( + inputCall(), + MTP_int(0), + MTP_int(10) + )).done([=](const MTPphone_GroupParticipants &result) { + auto sources = std::vector(); + result.match([&](const MTPDphone_groupParticipants &data) { + for (const auto &p : data.vparticipants().v) { + p.match([&](const MTPDgroupCallParticipant &data) { + if (data.vuser_id().v != _channel->session().userId()) { + sources.push_back(data.vsource().v); + } + }); + } + }); + _instance->setSsrcs(std::move(sources)); + _instance->setIsMuted(false); + }).fail([=](const RPCError &error) { + int a = error.code(); + }).send(); + }); + } + return true; + }, [&](const MTPDgroupCallPrivate &data) { + if (_instance || _id) { + return false; + } + join(MTP_inputGroupCall(data.vid(), data.vaccess_hash())); + return true; + }, [&](const MTPDgroupCallDiscarded &data) { + if (data.vid().v != _id) { + return false; + } + return true; + }); +} + +void GroupCall::createAndStartController() { + using AudioLevels = std::vector>; + + const auto weak = base::make_weak(this); + tgcalls::GroupInstanceDescriptor descriptor = { + .config = tgcalls::GroupConfig{ + }, + .networkStateUpdated = [=](bool) { + }, + .audioLevelsUpdated = [=](const AudioLevels &data) { + }, + }; + if (Logs::DebugEnabled()) { + auto callLogFolder = cWorkingDir() + qsl("DebugLogs"); + auto callLogPath = callLogFolder + qsl("/last_group_call_log.txt"); + auto callLogNative = QDir::toNativeSeparators(callLogPath); +#ifdef Q_OS_WIN + descriptor.config.logPath.data = callLogNative.toStdWString(); +#else // Q_OS_WIN + const auto callLogUtf = QFile::encodeName(callLogNative); + descriptor.config.logPath.data.resize(callLogUtf.size()); + ranges::copy(callLogUtf, descriptor.config.logPath.data.begin()); +#endif // Q_OS_WIN + QFile(callLogPath).remove(); + QDir().mkpath(callLogFolder); + } + + LOG(("Call Info: Creating group instance")); + _instance = std::make_unique( + std::move(descriptor)); + + const auto raw = _instance.get(); + if (_muted.current()) { + raw->setIsMuted(_muted.current()); + } + //raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled()); +} + +void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) { + if (_instance) { + const auto id = deviceId.toStdString(); + //if (input) { + // _instance->setAudioInputDevice(id); + //} else { + // _instance->setAudioOutputDevice(id); + //} + } +} + +void GroupCall::setAudioVolume(bool input, float level) { + if (_instance) { + //if (input) { + // _instance->setInputVolume(level); + //} else { + // _instance->setOutputVolume(level); + //} + } +} + +void GroupCall::setAudioDuckingEnabled(bool enabled) { + if (_instance) { + //_instance->setAudioOutputDuckingEnabled(enabled); + } +} + +void GroupCall::handleRequestError(const RPCError &error) { + //if (error.type() == qstr("USER_PRIVACY_RESTRICTED")) { + // Ui::show(Box(tr::lng_call_error_not_available(tr::now, lt_user, _user->name))); + //} else if (error.type() == qstr("PARTICIPANT_VERSION_OUTDATED")) { + // Ui::show(Box(tr::lng_call_error_outdated(tr::now, lt_user, _user->name))); + //} else if (error.type() == qstr("CALL_PROTOCOL_LAYER_INVALID")) { + // Ui::show(Box(Lang::Hard::CallErrorIncompatible().replace("{user}", _user->name))); + //} + //finish(FinishType::Failed); +} + +void GroupCall::handleControllerError(const QString &error) { + if (error == u"ERROR_INCOMPATIBLE"_q) { + //Ui::show(Box( + // Lang::Hard::CallErrorIncompatible().replace( + // "{user}", + // _user->name))); + } else if (error == u"ERROR_AUDIO_IO"_q) { + Ui::show(Box(tr::lng_call_error_audio_io(tr::now))); + } + //finish(FinishType::Failed); +} + +MTPInputGroupCall GroupCall::inputCall() const { + Expects(_id != 0); + + return MTP_inputGroupCall( + MTP_long(_id), + MTP_long(_accessHash)); +} + +void GroupCall::destroyController() { + if (_instance) { + //_instance->stop([](tgcalls::FinalState) { + //}); + + DEBUG_LOG(("Call Info: Destroying call controller..")); + _instance.reset(); + DEBUG_LOG(("Call Info: Call controller destroyed.")); + } +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_call.h b/Telegram/SourceFiles/calls/calls_group_call.h new file mode 100644 index 000000000..412a8e168 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_call.h @@ -0,0 +1,84 @@ +/* +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 +*/ +#pragma once + +#include "base/weak_ptr.h" +#include "base/timer.h" +#include "base/bytes.h" +#include "mtproto/sender.h" +#include "mtproto/mtproto_auth_key.h" + +namespace tgcalls { +class GroupInstanceImpl; +} // namespace tgcalls + +namespace Calls { + +class GroupCall final : public base::has_weak_ptr { +public: + class Delegate { + public: + virtual ~Delegate() = default; + + }; + + GroupCall( + not_null delegate, + not_null channel, + const MTPInputGroupCall &inputCall); + ~GroupCall(); + + [[nodiscard]] not_null channel() const { + return _channel; + } + + void start(); + void join(const MTPInputGroupCall &inputCall); + bool handleUpdate(const MTPGroupCall &call); + + void setMuted(bool mute); + [[nodiscard]] bool muted() const { + return _muted.current(); + } + [[nodiscard]] rpl::producer mutedValue() const { + return _muted.value(); + } + + void setCurrentAudioDevice(bool input, const QString &deviceId); + void setAudioVolume(bool input, float level); + void setAudioDuckingEnabled(bool enabled); + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + +private: + void handleRequestError(const RPCError &error); + void handleControllerError(const QString &error); + void createAndStartController(); + void destroyController(); + + [[nodiscard]] MTPInputGroupCall inputCall() const; + + const not_null _delegate; + const not_null _channel; + MTP::Sender _api; + crl::time _startTime = 0; + + rpl::variable _muted = false; + + uint64 _id = 0; + uint64 _accessHash = 0; + + std::unique_ptr _instance; + + rpl::lifetime _lifetime; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index e99958109..b0cd55288 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_call.h" #include "calls/calls_panel.h" #include "data/data_user.h" +#include "data/data_channel.h" #include "data/data_session.h" #include "media/audio/media_audio_track.h" #include "platform/platform_specific.h" @@ -39,8 +40,7 @@ Instance::Instance() = default; Instance::~Instance() = default; void Instance::startOutgoingCall(not_null user, bool video) { - if (alreadyInCall()) { // Already in a call. - _currentCallPanel->showAndActivate(); + if (activateCurrentCall()) { return; } if (user->callsStatus() == UserData::CallsStatus::Private) { @@ -55,6 +55,26 @@ void Instance::startOutgoingCall(not_null user, bool video) { })); } +void Instance::startGroupCall(not_null channel) { + if (activateCurrentCall()) { + return; + } + requestPermissionsOrFail(crl::guard(this, [=] { + createGroupCall(channel, MTP_inputGroupCall(MTPlong(), MTPlong())); + })); +} + +void Instance::joinGroupCall( + not_null channel, + const MTPInputGroupCall &call) { + if (activateCurrentCall()) { + return; + } + requestPermissionsOrFail(crl::guard(this, [=] { + createGroupCall(channel, call); + })); +} + void Instance::callFinished(not_null call) { crl::on_main(call, [=] { destroyCall(call); @@ -142,6 +162,40 @@ void Instance::createCall(not_null user, Call::Type type, bool video) refreshDhConfig(); } +void Instance::destroyGroupCall(not_null call) { + if (_currentGroupCall.get() == call) { + //_currentCallPanel->closeBeforeDestroy(); + //_currentCallPanel = nullptr; + + auto taken = base::take(_currentGroupCall); + _currentGroupCallChanges.fire(nullptr); + taken.reset(); + + if (App::quitting()) { + LOG(("Calls::Instance doesn't prevent quit any more.")); + } + Core::App().quitPreventFinished(); + } +} + +void Instance::createGroupCall( + not_null channel, + const MTPInputGroupCall &inputCall) { + auto call = std::make_unique( + getGroupCallDelegate(), + channel, + inputCall); + const auto raw = call.get(); + + channel->session().account().sessionChanges( + ) | rpl::start_with_next([=] { + destroyGroupCall(raw); + }, raw->lifetime()); + + _currentGroupCall = std::move(call); + _currentGroupCallChanges.fire_copy(raw); +} + void Instance::refreshDhConfig() { Expects(_currentCall != nullptr); @@ -232,6 +286,10 @@ void Instance::handleUpdate( handleCallUpdate(session, data.vphone_call()); }, [&](const MTPDupdatePhoneCallSignalingData &data) { handleSignalingData(data); + }, [&](const MTPDupdateGroupCall &data) { + handleGroupCallUpdate(session, data.vcall()); + }, [&](const MTPDupdateGroupCallParticipants &data) { + handleGroupCallUpdate(session, data); }, [](const auto &) { Unexpected("Update type in Calls::Instance::handleUpdate."); }); @@ -267,7 +325,7 @@ void Instance::handleCallUpdate( LOG(("API Error: Self found in phoneCallRequested.")); } const auto &config = session->serverConfig(); - if (alreadyInCall() || !user || user->isSelf()) { + if (inCall() || inGroupCall() || !user || user->isSelf()) { const auto flags = phoneCall.is_video() ? MTPphone_DiscardCall::Flag::f_video : MTPphone_DiscardCall::Flag(0); @@ -290,6 +348,19 @@ void Instance::handleCallUpdate( } } +void Instance::handleGroupCallUpdate( + not_null session, + const MTPGroupCall &call) { + if (_currentGroupCall) { + _currentGroupCall->handleUpdate(call); + } +} + +void Instance::handleGroupCallUpdate( + not_null session, + const MTPDupdateGroupCallParticipants &update) { +} + void Instance::handleSignalingData( const MTPDupdatePhoneCallSignalingData &data) { if (!_currentCall || !_currentCall->handleSignalingData(data)) { @@ -298,10 +369,24 @@ void Instance::handleSignalingData( } } -bool Instance::alreadyInCall() { +bool Instance::inCall() const { return (_currentCall && _currentCall->state() != Call::State::Busy); } +bool Instance::inGroupCall() const { + return (_currentGroupCall != nullptr); +} + +bool Instance::activateCurrentCall() { + if (inCall()) { + _currentCallPanel->showAndActivate(); + return true; + } else if (inGroupCall()) { + return true; + } + return false; +} + Call *Instance::currentCall() const { return _currentCall.get(); } @@ -335,9 +420,12 @@ void Instance::requestPermissionOrFail(Platform::PermissionType type, Fn } })); } else { - if (alreadyInCall()) { + if (inCall()) { _currentCall->hangup(); } + if (inGroupCall()) { + //_currentGroupCall->hangup(); // #TODO calls + } Ui::show(Box(tr::lng_no_mic_permission(tr::now), tr::lng_menu_settings(tr::now), crl::guard(this, [=] { Platform::OpenSystemSettingsForPermission(type); Ui::hideLayer(); diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index a50f99343..390292b97 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/sender.h" #include "calls/calls_call.h" +#include "calls/calls_group_call.h" namespace Platform { enum class PermissionType; @@ -30,6 +31,7 @@ class Panel; class Instance : private Call::Delegate + , private GroupCall::Delegate , private base::Subscriber , public base::has_weak_ptr { public: @@ -37,6 +39,10 @@ public: ~Instance(); void startOutgoingCall(not_null user, bool video); + void startGroupCall(not_null channel); + void joinGroupCall( + not_null channel, + const MTPInputGroupCall &call); void handleUpdate( not_null session, const MTPUpdate &update); @@ -48,20 +54,33 @@ public: [[nodiscard]] bool isQuitPrevent(); private: - not_null getCallDelegate() { + [[nodiscard]] not_null getCallDelegate() { return static_cast(this); } - DhConfig getDhConfig() const override { + [[nodiscard]] not_null getGroupCallDelegate() { + return static_cast(this); + } + [[nodiscard]] DhConfig getDhConfig() const override { return _dhConfig; } void callFinished(not_null call) override; void callFailed(not_null call) override; void callRedial(not_null call) override; + void callRequestPermissionsOrFail(Fn onSuccess) override { + requestPermissionsOrFail(std::move(onSuccess)); + } + using Sound = Call::Delegate::Sound; void playSound(Sound sound) override; void createCall(not_null user, Call::Type type, bool video); void destroyCall(not_null call); - void requestPermissionsOrFail(Fn onSuccess) override; + + void createGroupCall( + not_null channel, + const MTPInputGroupCall &inputCall); + void destroyGroupCall(not_null call); + + void requestPermissionsOrFail(Fn onSuccess); void requestPermissionOrFail(Platform::PermissionType type, Fn onSuccess); void handleSignalingData(const MTPDupdatePhoneCallSignalingData &data); @@ -70,10 +89,18 @@ private: void refreshServerConfig(not_null session); bytes::const_span updateDhConfig(const MTPmessages_DhConfig &data); - bool alreadyInCall(); + bool activateCurrentCall(); + [[nodiscard]] bool inCall() const; + [[nodiscard]] bool inGroupCall() const; void handleCallUpdate( not_null session, const MTPPhoneCall &call); + void handleGroupCallUpdate( + not_null session, + const MTPGroupCall &call); + void handleGroupCallUpdate( + not_null session, + const MTPDupdateGroupCallParticipants &update); DhConfig _dhConfig; @@ -84,8 +111,9 @@ private: std::unique_ptr _currentCall; rpl::event_stream _currentCallChanges; std::unique_ptr _currentCallPanel; - base::Observable _currentCallChanged; - base::Observable _newServiceMessage; + + std::unique_ptr _currentGroupCall; + rpl::event_stream _currentGroupCallChanges; std::unique_ptr _callConnectingTrack; std::unique_ptr _callEndedTrack; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 5bd443688..568840d82 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -86,9 +86,10 @@ struct PeerUpdate { ChannelLinkedChat = (1 << 27), ChannelLocation = (1 << 28), Slowmode = (1 << 29), + GroupCall = (1 << 30), // For iteration - LastUsedBit = (1 << 29), + LastUsedBit = (1 << 30), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 102eb637e..47a09086a 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_location.h" #include "data/data_histories.h" +#include "data/data_group_call.h" #include "base/unixtime.h" #include "history/history.h" #include "main/main_session.h" @@ -671,6 +672,32 @@ void ChannelData::privateErrorReceived() { } } +void ChannelData::setCall(const MTPInputGroupCall &call) { + call.match([&](const MTPDinputGroupCall &data) { + if (_call && _call->id() == data.vid().v) { + return; + } else if (!_call && !data.vid().v) { + return; + } else if (!data.vid().v) { + clearCall(); + return; + } + _call = std::make_unique( + this, + data.vid().v, + data.vaccess_hash().v); + session().changes().peerUpdated(this, UpdateFlag::GroupCall); + }); +} + +void ChannelData::clearCall() { + if (!_call) { + return; + } + _call = nullptr; + session().changes().peerUpdated(this, UpdateFlag::GroupCall); +} + namespace Data { void ApplyMigration( @@ -702,6 +729,12 @@ void ApplyChannelUpdate( auto canViewMembers = channel->canViewMembers(); auto canEditStickers = channel->canEditStickers(); + if (const auto call = update.vcall()) { + channel->setCall(*call); + } else { + channel->clearCall(); + } + channel->setFullFlags(update.vflags().v); channel->setUserpicPhoto(update.vchat_photo()); if (const auto migratedFrom = update.vmigrated_from_chat_id()) { diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 418e4a724..070c42502 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_pts_waiter.h" #include "data/data_location.h" +namespace Data { +class GroupCall; +} // namespace Data + struct ChannelLocation { QString address; Data::LocationPoint point; @@ -393,6 +397,12 @@ public: [[nodiscard]] QString invitePeekHash() const; void privateErrorReceived(); + [[nodiscard]] Data::GroupCall *call() const { + return _call.get(); + } + void setCall(const MTPInputGroupCall &call); + void clearCall(); + // Still public data members. uint64 access = 0; @@ -439,6 +449,8 @@ private: QString _inviteLink; std::optional _linkedChat; + std::unique_ptr _call; + int _slowmodeSeconds = 0; TimeId _slowmodeLastMessage = 0; diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp new file mode 100644 index 000000000..147c23ed4 --- /dev/null +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -0,0 +1,115 @@ +/* +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 "data/data_group_call.h" + +#include "data/data_channel.h" +#include "data/data_changes.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kRequestPerPage = 30; + +} // namespace + +GroupCall::GroupCall( + not_null channel, + uint64 id, + uint64 accessHash) +: _channel(channel) +, _id(id) +, _accessHash(accessHash) { +} + +uint64 GroupCall::id() const { + return _id; +} + +MTPInputGroupCall GroupCall::input() const { + return MTP_inputGroupCall(MTP_long(_id), MTP_long(_accessHash)); +} + +auto GroupCall::participants() const +-> const std::vector & { + return _participants; +} + +void GroupCall::requestParticipants() { + if (_participantsRequestId) { + return; + } else if (_participants.size() >= _fullCount && _allReceived) { + return; + } + const auto requestFromDate = (_allReceived || _participants.empty()) + ? TimeId(0) + : _participants.back().date; + auto &api = _channel->session().api(); + _participantsRequestId = api.request(MTPphone_GetGroupParticipants( + input(), + MTP_int(requestFromDate), + MTP_int(kRequestPerPage) + )).done([=](const MTPphone_GroupParticipants &result) { + result.match([&](const MTPDphone_groupParticipants &data) { + _fullCount = data.vcount().v; + _channel->owner().processUsers(data.vusers()); + for (const auto &p : data.vparticipants().v) { + p.match([&](const MTPDgroupCallParticipant &data) { + const auto userId = data.vuser_id().v; + const auto user = _channel->owner().user(userId); + const auto value = Participant{ + .user = user, + .date = data.vdate().v, + .source = data.vsource().v, + .muted = data.is_muted(), + .canSelfUnmute = data.is_can_self_unmute(), + .left = data.is_left() + }; + const auto i = ranges::find( + _participants, + user, + &Participant::user); + if (i == end(_participants)) { + _participants.push_back(value); + } else { + *i = value; + } + }); + } + if (!_allReceived + && (data.vparticipants().v.size() < kRequestPerPage)) { + _allReceived = true; + } + if (_allReceived) { + _fullCount = _participants.size(); + } + }); + ranges::sort(_participants, std::greater<>(), &Participant::date); + _channel->session().changes().peerUpdated( + _channel, + PeerUpdate::Flag::GroupCall); + }).fail([=](const RPCError &error) { + _allReceived = true; + _fullCount = _participants.size(); + _channel->session().changes().peerUpdated( + _channel, + PeerUpdate::Flag::GroupCall); + }).send(); +} + +int GroupCall::fullCount() const { + return _fullCount; +} + +bool GroupCall::participantsLoaded() const { + return _allReceived; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_group_call.h b/Telegram/SourceFiles/data/data_group_call.h new file mode 100644 index 000000000..c72c20de4 --- /dev/null +++ b/Telegram/SourceFiles/data/data_group_call.h @@ -0,0 +1,54 @@ +/* +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 +*/ +#pragma once + +class UserData; +class ChannelData; + +namespace Data { + +class GroupCall final { +public: + GroupCall(not_null channel, uint64 id, uint64 accessHash); + + [[nodiscard]] uint64 id() const; + [[nodiscard]] MTPInputGroupCall input() const; + + struct Participant { + not_null user; + TimeId date = 0; + int source = 0; + bool muted = false; + bool canSelfUnmute = false; + bool left = false; + }; + + [[nodiscard]] auto participants() const + -> const std::vector &; + void requestParticipants(); + [[nodiscard]] bool participantsLoaded() const; + + [[nodiscard]] int fullCount() const; + +private: + const not_null _channel; + const uint64 _id = 0; + const uint64 _accessHash = 0; + + int _version = 0; + UserId _adminId = 0; + uint64 _reflectorId = 0; + mtpRequestId _participantsRequestId = 0; + + std::vector _participants; + int _fullCount = 0; + bool _allReceived = false; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 649b144d0..db6a3c46a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_scheduled_messages.h" #include "data/data_file_origin.h" #include "data/data_histories.h" +#include "data/data_group_call.h" #include "data/stickers/data_stickers.h" #include "history/history.h" #include "history/history_item.h" @@ -74,6 +75,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_pinned_tracker.h" #include "history/view/history_view_pinned_section.h" #include "history/view/history_view_pinned_bar.h" +#include "history/view/history_view_group_call_tracker.h" #include "history/view/media/history_view_media.h" #include "profile/profile_block_group_members.h" #include "info/info_memento.h" @@ -100,6 +102,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/qthelp_regex.h" #include "ui/chat/pinned_bar.h" +#include "ui/chat/group_call_bar.h" #include "ui/widgets/popup_menu.h" #include "ui/item_text_options.h" #include "ui/unread_badge.h" @@ -118,6 +121,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "support/support_common.h" #include "support/support_autocomplete.h" #include "dialogs/dialogs_key.h" +#include "calls/calls_instance.h" #include "facades.h" #include "app.h" #include "styles/style_chat.h" @@ -1775,6 +1779,8 @@ void HistoryWidget::showHistory( destroyUnreadBarOnClose(); _pinnedBar = nullptr; _pinnedTracker = nullptr; + _groupCallBar = nullptr; + _groupCallTracker = nullptr; _membersDropdown.destroy(); _scrollToAnimation.stop(); @@ -1886,6 +1892,7 @@ void HistoryWidget::showHistory( _updateHistoryItems.cancel(); setupPinnedTracker(); + setupGroupCallTracker(); if (_history->scrollTopItem || (_migrated && _migrated->scrollTopItem) || _history->isReadyFor(_showAtMsgId)) { @@ -2093,6 +2100,9 @@ void HistoryWidget::updateControlsVisibility() { if (_pinnedBar) { _pinnedBar->show(); } + if (_groupCallBar) { + _groupCallBar->show(); + } if (_firstLoadRequest && !_scroll->isHidden()) { _scroll->hide(); } else if (!_firstLoadRequest && _scroll->isHidden()) { @@ -3037,6 +3047,9 @@ void HistoryWidget::hideChildWidgets() { if (_pinnedBar) { _pinnedBar->hide(); } + if (_groupCallBar) { + _groupCallBar->hide(); + } if (_voiceRecordBar) { _voiceRecordBar->hideFast(); } @@ -3250,6 +3263,9 @@ void HistoryWidget::showAnimated( if (_pinnedBar) { _pinnedBar->finishAnimating(); } + if (_groupCallBar) { + _groupCallBar->finishAnimating(); + } _topShadow->setVisible(params.withTopBarShadow ? false : true); _preserveScrollTop = false; @@ -3278,6 +3294,9 @@ void HistoryWidget::animationCallback() { if (_pinnedBar) { _pinnedBar->finishAnimating(); } + if (_groupCallBar) { + _groupCallBar->finishAnimating(); + } _cacheUnder = _cacheOver = QPixmap(); doneShow(); synteticScrollToY(_scroll->scrollTop()); @@ -3301,6 +3320,9 @@ void HistoryWidget::doneShow() { if (_pinnedBar) { _pinnedBar->finishAnimating(); } + if (_groupCallBar) { + _groupCallBar->finishAnimating(); + } checkHistoryActivation(); App::wnd()->setInnerFocus(); _preserveScrollTop = false; @@ -4409,7 +4431,12 @@ void HistoryWidget::updateControlsGeometry() { _pinnedBar->move(0, pinnedBarTop); _pinnedBar->resizeToWidth(width()); } - const auto contactStatusTop = pinnedBarTop + (_pinnedBar ? _pinnedBar->height() : 0); + const auto groupCallTop = pinnedBarTop + (_pinnedBar ? _pinnedBar->height() : 0); + if (_groupCallBar) { + _groupCallBar->move(0, groupCallTop); + _groupCallBar->resizeToWidth(width()); + } + const auto contactStatusTop = groupCallTop + (_groupCallBar ? _groupCallBar->height() : 0); if (_contactStatus) { _contactStatus->move(0, contactStatusTop); } @@ -4572,6 +4599,9 @@ void HistoryWidget::updateHistoryGeometry( if (_pinnedBar) { newScrollHeight -= _pinnedBar->height(); } + if (_groupCallBar) { + newScrollHeight -= _groupCallBar->height(); + } if (_contactStatus) { newScrollHeight -= _contactStatus->height(); } @@ -4804,6 +4834,7 @@ int HistoryWidget::computeMaxFieldHeight() const { - _topBar->height() - (_contactStatus ? _contactStatus->height() : 0) - (_pinnedBar ? _pinnedBar->height() : 0) + - (_groupCallBar ? _groupCallBar->height() : 0) - ((_editMsgId || replyToId() || readyToForward() @@ -5009,6 +5040,7 @@ void HistoryWidget::handlePeerMigration() { _migrated = _history->migrateFrom(); _list->notifyMigrateUpdated(); setupPinnedTracker(); + setupGroupCallTracker(); updateHistoryGeometry(); } const auto from = chat->owner().historyLoaded(chat); @@ -5341,6 +5373,68 @@ void HistoryWidget::refreshPinnedBarButton(bool many) { _pinnedBar->setRightButton(std::move(button)); } +void HistoryWidget::setupGroupCallTracker() { + Expects(_history != nullptr); + + const auto channel = _history->peer->asChannel(); + if (!channel) { + _groupCallTracker = nullptr; + _groupCallBar = nullptr; + return; + } + _groupCallTracker = std::make_unique( + channel); + _groupCallBar = std::make_unique( + this, + _groupCallTracker->content()); + + rpl::single( + rpl::empty_value() + ) | rpl::then( + base::ObservableViewer(Adaptive::Changed()) + ) | rpl::map([] { + return Adaptive::OneColumn(); + }) | rpl::start_with_next([=](bool one) { + _groupCallBar->setShadowGeometryPostprocess([=](QRect geometry) { + if (!one) { + geometry.setLeft(geometry.left() + st::lineWidth); + } + return geometry; + }); + }, _groupCallBar->lifetime()); + + rpl::merge( + _groupCallBar->barClicks(), + _groupCallBar->joinClicks() + ) | rpl::start_with_next([=] { + const auto channel = _history->peer->asChannel(); + if (!channel) { + return; + } + const auto call = channel->call(); + if (!call) { + return; + } + Core::App().calls().joinGroupCall(channel, call->input()); + }, _groupCallBar->lifetime()); + + _groupCallBarHeight = 0; + _groupCallBar->heightValue( + ) | rpl::start_with_next([=](int height) { + _topDelta = _preserveScrollTop ? 0 : (height - _groupCallBarHeight); + _groupCallBarHeight = height; + updateHistoryGeometry(); + updateControlsGeometry(); + _topDelta = 0; + }, _groupCallBar->lifetime()); + + orderWidgets(); + + if (_a_show.animating()) { + _groupCallBar->hide(); + } +} + void HistoryWidget::requestMessageData(MsgId msgId) { const auto callback = [=](ChannelData *channel, MsgId msgId) { messageDataReceived(channel, msgId); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 73d967fc7..62e61c5e7 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -61,6 +61,7 @@ class FlatButton; class LinkButton; class RoundButton; class PinnedBar; +class GroupCallBar; struct PreparedList; class SendFilesWay; namespace Toast { @@ -90,6 +91,7 @@ class TopBarWidget; class ContactStatus; class Element; class PinnedTracker; +class GroupCallTracker; namespace Controls { class RecordLock; class VoiceRecordBar; @@ -475,6 +477,8 @@ private: int wasScrollTop, int nowScrollTop); + void setupGroupCallTracker(); + void sendInlineResult(InlineBots::ResultSelected result); void drawField(Painter &p, const QRect &rect); @@ -591,10 +595,15 @@ private: std::unique_ptr _pinnedTracker; std::unique_ptr _pinnedBar; int _pinnedBarHeight = 0; - bool _preserveScrollTop = false; FullMsgId _pinnedClickedId; std::optional _minPinnedId; + std::unique_ptr _groupCallTracker; + std::unique_ptr _groupCallBar; + int _groupCallBarHeight = 0; + + bool _preserveScrollTop = false; + mtpRequestId _saveEditMsgRequestId = 0; QStringList _parsedLinks; diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp new file mode 100644 index 000000000..9712d559f --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp @@ -0,0 +1,42 @@ +/* +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 "history/view/history_view_group_call_tracker.h" + +#include "data/data_channel.h" +#include "data/data_changes.h" +#include "data/data_group_call.h" +#include "main/main_session.h" +#include "ui/chat/group_call_bar.h" + +namespace HistoryView { + +GroupCallTracker::GroupCallTracker(not_null channel) +: _channel(channel) { +} + +rpl::producer GroupCallTracker::content() const { + const auto channel = _channel; + return channel->session().changes().peerFlagsValue( + channel, + Data::PeerUpdate::Flag::GroupCall + ) | rpl::map([=]() -> Ui::GroupCallBarContent { + const auto call = channel->call(); + if (!call) { + return { .shown = false }; + } else if (!call->fullCount() && !call->participantsLoaded()) { + call->requestParticipants(); + } + return { .count = call->fullCount(), .shown = true }; + }); +} + +rpl::producer<> GroupCallTracker::joinClicks() const { + return _joinClicks.events(); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h new file mode 100644 index 000000000..1dc6c4f94 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h @@ -0,0 +1,32 @@ +/* +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 +*/ +#pragma once + +#include "ui/rp_widget.h" + +namespace Ui { +struct GroupCallBarContent; +} // namespace Ui + +namespace HistoryView { + +class GroupCallTracker final { +public: + GroupCallTracker(not_null channel); + + [[nodiscard]] rpl::producer content() const; + [[nodiscard]] rpl::producer<> joinClicks() const; + +private: + not_null _channel; + + rpl::event_stream<> _joinClicks; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index c5eee7f76..1414db537 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -200,6 +200,8 @@ void TopBarWidget::onCall() { if (const auto peer = _activeChat.key.peer()) { if (const auto user = peer->asUser()) { Core::App().calls().startOutgoingCall(user, false); + } else if (const auto megagroup = peer->asMegagroup()) { + Core::App().calls().startGroupCall(megagroup); } } } @@ -691,6 +693,8 @@ void TopBarWidget::updateControlsVisibility() { if (const auto user = peer->asUser()) { return session().serverConfig().phoneCallsEnabled.current() && user->hasCalls(); + } else if (const auto megagroup = peer->asMegagroup()) { + return true; } } return false; diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp new file mode 100644 index 000000000..497120ad6 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp @@ -0,0 +1,182 @@ +/* +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 "ui/chat/group_call_bar.h" + +#include "ui/chat/message_bar.h" +#include "ui/widgets/shadow.h" +#include "ui/widgets/buttons.h" +#include "styles/style_chat.h" +#include "styles/palette.h" + +#include + +namespace Ui { + +GroupCallBar::GroupCallBar( + not_null parent, + rpl::producer content) + : _wrap(parent, object_ptr(parent)) + , _inner(_wrap.entity()) + , _shadow(std::make_unique(_wrap.parentWidget())) { + _wrap.hide(anim::type::instant); + _shadow->hide(); + + _wrap.entity()->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg); + }, lifetime()); + _wrap.setAttribute(Qt::WA_OpaquePaintEvent); + + auto copy = std::move( + content + ) | rpl::start_spawning(_wrap.lifetime()); + + rpl::duplicate( + copy + ) | rpl::start_with_next([=](GroupCallBarContent &&content) { + _content = content; + _inner->update(); + }, lifetime()); + + std::move( + copy + ) | rpl::map([=](const GroupCallBarContent &content) { + return !content.shown; + }) | rpl::start_with_next_done([=](bool hidden) { + _shouldBeShown = !hidden; + if (!_forceHidden) { + _wrap.toggle(_shouldBeShown, anim::type::normal); + } + }, [=] { + _forceHidden = true; + _wrap.toggle(false, anim::type::normal); + }, lifetime()); + + setupInner(); +} + +GroupCallBar::~GroupCallBar() { +} + +void GroupCallBar::setupInner() { + _inner->resize(0, st::historyReplyHeight); + _inner->paintRequest( + ) | rpl::start_with_next([=](QRect rect) { + auto p = Painter(_inner); + paint(p); + }, _inner->lifetime()); + + + // Clicks. + _inner->setCursor(style::cur_pointer); + _inner->events( + ) | rpl::filter([=](not_null event) { + return (event->type() == QEvent::MouseButtonPress); + }) | rpl::map([=] { + return _inner->events( + ) | rpl::filter([=](not_null event) { + return (event->type() == QEvent::MouseButtonRelease); + }) | rpl::take(1) | rpl::filter([=](not_null event) { + return _inner->rect().contains( + static_cast(event.get())->pos()); + }); + }) | rpl::flatten_latest( + ) | rpl::map([] { + return rpl::empty_value(); + }) | rpl::start_to_stream(_barClicks, _inner->lifetime()); +} + +void GroupCallBar::paint(Painter &p) { + p.fillRect(_inner->rect(), st::historyComposeAreaBg); + p.setPen(st::defaultMessageBar.textFg); + p.setFont(st::defaultMessageBar.text.font); + p.drawText(_inner->rect(), "Voice Chat", style::al_center); +} + +void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) { + _inner->resizeToWidth(wrapGeometry.width()); + const auto hidden = _wrap.isHidden() || !wrapGeometry.height(); + if (_shadow->isHidden() != hidden) { + _shadow->setVisible(!hidden); + } +} + +void GroupCallBar::setShadowGeometryPostprocess(Fn postprocess) { + _shadowGeometryPostprocess = std::move(postprocess); + updateShadowGeometry(_wrap.geometry()); +} + +void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) { + const auto regular = QRect( + wrapGeometry.x(), + wrapGeometry.y() + wrapGeometry.height(), + wrapGeometry.width(), + st::lineWidth); + _shadow->setGeometry(_shadowGeometryPostprocess + ? _shadowGeometryPostprocess(regular) + : regular); +} + +void GroupCallBar::show() { + if (!_forceHidden) { + return; + } + _forceHidden = false; + if (_shouldBeShown) { + _wrap.show(anim::type::instant); + _shadow->show(); + } +} + +void GroupCallBar::hide() { + if (_forceHidden) { + return; + } + _forceHidden = true; + _wrap.hide(anim::type::instant); + _shadow->hide(); +} + +void GroupCallBar::raise() { + _wrap.raise(); + _shadow->raise(); +} + +void GroupCallBar::finishAnimating() { + _wrap.finishAnimating(); +} + +void GroupCallBar::move(int x, int y) { + _wrap.move(x, y); +} + +void GroupCallBar::resizeToWidth(int width) { + _wrap.entity()->resizeToWidth(width); +} + +int GroupCallBar::height() const { + return !_forceHidden + ? _wrap.height() + : _shouldBeShown + ? st::historyReplyHeight + : 0; +} + +rpl::producer GroupCallBar::heightValue() const { + return _wrap.heightValue(); +} + +rpl::producer<> GroupCallBar::barClicks() const { + return _barClicks.events(); +} + +rpl::producer<> GroupCallBar::joinClicks() const { + return rpl::never<>(); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.h b/Telegram/SourceFiles/ui/chat/group_call_bar.h new file mode 100644 index 000000000..fe0ec25ae --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.h @@ -0,0 +1,69 @@ +/* +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 +*/ +#pragma once + +#include "ui/wrap/slide_wrap.h" +#include "base/object_ptr.h" + +class Painter; + +namespace Ui { + +class PlainShadow; + +struct GroupCallBarContent { + int count = 0; + bool shown = false; + bool joined = false; + // #TODO calls userpics +}; + +class GroupCallBar final { +public: + GroupCallBar( + not_null parent, + rpl::producer content); + ~GroupCallBar(); + + void show(); + void hide(); + void raise(); + void finishAnimating(); + + void setShadowGeometryPostprocess(Fn postprocess); + + void move(int x, int y); + void resizeToWidth(int width); + [[nodiscard]] int height() const; + [[nodiscard]] rpl::producer heightValue() const; + [[nodiscard]] rpl::producer<> barClicks() const; + [[nodiscard]] rpl::producer<> joinClicks() const; + + [[nodiscard]] rpl::lifetime &lifetime() { + return _wrap.lifetime(); + } + +private: + void updateShadowGeometry(QRect wrapGeometry); + void updateControlsGeometry(QRect wrapGeometry); + void setupInner(); + void paint(Painter &p); + + Ui::SlideWrap<> _wrap; + not_null _inner; + std::unique_ptr _shadow; + rpl::event_stream<> _barClicks; + Fn _shadowGeometryPostprocess; + bool _shouldBeShown = false; + bool _forceHidden = false; + + GroupCallBarContent _content; + +}; + +} // namespace Ui diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls index e8e9dd893..176e5fff5 160000 --- a/Telegram/ThirdParty/tgcalls +++ b/Telegram/ThirdParty/tgcalls @@ -1 +1 @@ -Subproject commit e8e9dd8934fdea47b2b2b0dadf2100fc3c17c914 +Subproject commit 176e5fff55478f23deec25c3bbcdaf9d0f3e19b2 diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake index b4841e601..6e837df05 100644 --- a/Telegram/cmake/lib_tgcalls.cmake +++ b/Telegram/cmake/lib_tgcalls.cmake @@ -53,6 +53,9 @@ if (NOT DESKTOP_APP_DISABLE_WEBRTC_INTEGRATION) VideoCaptureInterfaceImpl.h VideoCapturerInterface.h + group/GroupInstanceImpl.cpp + group/GroupInstanceImpl.h + platform/PlatformInterface.h # Android diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 1c7d0888c..016c02a6d 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -78,6 +78,8 @@ PRIVATE ui/chat/attach/attach_single_file_preview.h ui/chat/attach/attach_single_media_preview.cpp ui/chat/attach/attach_single_media_preview.h + ui/chat/group_call_bar.cpp + ui/chat/group_call_bar.h ui/chat/message_bar.cpp ui/chat/message_bar.h ui/chat/pinned_bar.cpp