/* 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/group/calls_group_settings.h" #include "calls/group/calls_group_call.h" #include "calls/group/calls_group_menu.h" // LeaveBox. #include "calls/group/calls_group_common.h" #include "calls/group/calls_choose_join_as.h" #include "calls/calls_instance.h" #include "ui/widgets/level_meter.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/text/text_utilities.h" #include "ui/toasts/common_toasts.h" #include "lang/lang_keys.h" #include "boxes/share_box.h" #include "history/view/history_view_schedule_box.h" #include "history/history_message.h" // GetErrorTextForSending. #include "history/history.h" #include "data/data_histories.h" #include "data/data_session.h" #include "base/timer_rpl.h" #include "base/event_filter.h" #include "base/global_shortcuts.h" #include "base/platform/base_platform_info.h" #include "base/unixtime.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_group_call.h" #include "calls/group/calls_group_rtmp.h" #include "ui/toast/toast.h" #include "data/data_changes.h" #include "core/application.h" #include "core/core_settings.h" #include "ui/boxes/single_choice_box.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_media_devices.h" #include "settings/settings_common.h" #include "settings/settings_calls.h" #include "main/main_session.h" #include "apiwrap.h" #include "api/api_invite_links.h" #include "styles/style_layers.h" #include "styles/style_calls.h" #include "styles/style_settings.h" #include namespace Calls::Group { namespace { constexpr auto kDelaysCount = 201; constexpr auto kMicrophoneTooltipAfterLoudCount = 3; constexpr auto kDropLoudAfterQuietCount = 5; constexpr auto kMicrophoneTooltipLevelThreshold = 0.2; constexpr auto kMicrophoneTooltipCheckInterval = crl::time(500); #ifdef Q_OS_MAC constexpr auto kCheckAccessibilityInterval = crl::time(500); #endif // Q_OS_MAC void SaveCallJoinMuted( not_null peer, CallId callId, bool joinMuted) { const auto call = peer->groupCall(); if (!call || call->id() != callId || !peer->canManageGroupCall() || !call->canChangeJoinMuted() || call->joinMuted() == joinMuted) { return; } call->setJoinMutedLocally(joinMuted); peer->session().api().request(MTPphone_ToggleGroupCallSettings( MTP_flags(MTPphone_ToggleGroupCallSettings::Flag::f_join_muted), call->input(), MTP_bool(joinMuted) )).send(); } [[nodiscard]] crl::time DelayByIndex(int index) { return index * crl::time(10); } [[nodiscard]] QString FormatDelay(crl::time delay) { return (delay < crl::time(1000)) ? tr::lng_group_call_ptt_delay_ms( tr::now, lt_amount, QString::number(delay)) : tr::lng_group_call_ptt_delay_s( tr::now, lt_amount, QString::number(delay / 1000., 'f', 2)); } object_ptr ShareInviteLinkBox( not_null peer, const QString &linkSpeaker, const QString &linkListener, Fn showToast) { const auto sending = std::make_shared(); const auto box = std::make_shared>(); auto bottom = linkSpeaker.isEmpty() ? nullptr : object_ptr>( nullptr, object_ptr( nullptr, tr::lng_group_call_share_speaker(tr::now), true, st::groupCallCheckbox), st::groupCallShareMutedMargin); const auto speakerCheckbox = bottom ? bottom->entity() : nullptr; const auto currentLink = [=] { return (!speakerCheckbox || !speakerCheckbox->checked()) ? linkListener : linkSpeaker; }; auto copyCallback = [=] { QGuiApplication::clipboard()->setText(currentLink()); showToast(tr::lng_group_invite_copied(tr::now)); }; auto submitCallback = [=]( std::vector> &&result, TextWithTags &&comment, Api::SendOptions options, Data::ForwardOptions) { if (*sending || result.empty()) { return; } const auto error = [&] { for (const auto thread : result) { const auto error = GetErrorTextForSending( thread, { .text = &comment }); if (!error.isEmpty()) { return std::make_pair(error, thread); } } return std::make_pair(QString(), result.front()); }(); if (!error.first.isEmpty()) { auto text = TextWithEntities(); if (result.size() > 1) { text.append( Ui::Text::Bold(error.second->chatListName()) ).append("\n\n"); } text.append(error.first); if (const auto weak = *box) { weak->getDelegate()->show(ConfirmBox({ .text = text, .inform = true, })); } return; } *sending = true; const auto link = currentLink(); if (!comment.text.isEmpty()) { comment.text = link + "\n" + comment.text; const auto add = link.size() + 1; for (auto &tag : comment.tags) { tag.offset += add; } } else { comment.text = link; } auto &api = peer->session().api(); for (const auto thread : result) { auto message = Api::MessageToSend( Api::SendAction(thread, options)); message.textWithTags = comment; message.action.clearDraft = false; api.sendMessage(std::move(message)); } if (*box) { (*box)->closeBox(); } showToast(tr::lng_share_done(tr::now)); }; auto filterCallback = [](not_null thread) { return thread->canWrite(); }; const auto scheduleStyle = [&] { auto date = Ui::ChooseDateTimeStyleArgs(); date.labelStyle = &st::groupCallBoxLabel; date.dateFieldStyle = &st::groupCallScheduleDateField; date.timeFieldStyle = &st::groupCallScheduleTimeField; date.separatorStyle = &st::callMuteButtonLabel; date.atStyle = &st::callMuteButtonLabel; date.calendarStyle = &st::groupCallCalendarColors; auto st = HistoryView::ScheduleBoxStyleArgs(); st.topButtonStyle = &st::groupCallMenuToggle; st.popupMenuStyle = &st::groupCallPopupMenu; st.chooseDateTimeArgs = std::move(date); return st; }; auto result = Box(ShareBox::Descriptor{ .session = &peer->session(), .copyCallback = std::move(copyCallback), .submitCallback = std::move(submitCallback), .filterCallback = std::move(filterCallback), .bottomWidget = std::move(bottom), .copyLinkText = rpl::conditional( (speakerCheckbox ? speakerCheckbox->checkedValue() : rpl::single(false)), tr::lng_group_call_copy_speaker_link(), tr::lng_group_call_copy_listener_link()), .stMultiSelect = &st::groupCallMultiSelect, .stComment = &st::groupCallShareBoxComment, .st = &st::groupCallShareBoxList, .stLabel = &st::groupCallField, .scheduleBoxStyle = scheduleStyle(), }); *box = result.data(); return result; } } // namespace void SettingsBox( not_null box, not_null call) { using namespace Settings; const auto weakCall = base::make_weak(call); const auto weakBox = Ui::MakeWeak(box); struct State { rpl::event_stream outputNameStream; rpl::event_stream inputNameStream; std::unique_ptr micTester; Ui::LevelMeter *micTestLevel = nullptr; float micLevel = 0.; Ui::Animations::Simple micLevelAnimation; base::Timer levelUpdateTimer; bool generatingLink = false; }; const auto peer = call->peer(); const auto state = box->lifetime().make_state(); const auto real = peer->groupCall(); const auto rtmp = call->rtmp(); const auto id = call->id(); const auto goodReal = (real && real->id() == id); const auto layout = box->verticalLayout(); const auto &settings = Core::App().settings(); const auto joinMuted = goodReal ? real->joinMuted() : false; const auto canChangeJoinMuted = !rtmp && goodReal && real->canChangeJoinMuted(); const auto addCheck = (peer->canManageGroupCall() && canChangeJoinMuted); const auto addDivider = [&] { layout->add(object_ptr( layout, st::boxDividerHeight, st::groupCallDividerBg)); }; if (addCheck) { AddSkip(layout); } const auto muteJoined = addCheck ? AddButton( layout, tr::lng_group_call_new_muted(), st::groupCallSettingsButton)->toggleOn(rpl::single(joinMuted)) : nullptr; if (addCheck) { AddSkip(layout); } AddButtonWithLabel( layout, tr::lng_group_call_speakers(), rpl::single( CurrentAudioOutputName() ) | rpl::then( state->outputNameStream.events() ), st::groupCallSettingsButton )->addClickHandler([=] { box->getDelegate()->show(ChooseAudioOutputBox(crl::guard(box, [=]( const QString &id, const QString &name) { state->outputNameStream.fire_copy(name); }), &st::groupCallCheckbox, &st::groupCallRadio)); }); if (!rtmp) { AddButtonWithLabel( layout, tr::lng_group_call_microphone(), rpl::single( CurrentAudioInputName() ) | rpl::then( state->inputNameStream.events() ), st::groupCallSettingsButton )->addClickHandler([=] { box->getDelegate()->show(ChooseAudioInputBox(crl::guard(box, [=]( const QString &id, const QString &name) { state->inputNameStream.fire_copy(name); if (state->micTester) { state->micTester->setDeviceId(id); } }), &st::groupCallCheckbox, &st::groupCallRadio)); }); state->micTestLevel = box->addRow( object_ptr( box.get(), st::groupCallLevelMeter), st::settingsLevelMeterPadding); state->micTestLevel->resize(QSize(0, st::defaultLevelMeter.height)); state->levelUpdateTimer.setCallback([=] { const auto was = state->micLevel; state->micLevel = state->micTester->getAndResetLevel(); state->micLevelAnimation.start([=] { state->micTestLevel->setValue( state->micLevelAnimation.value(state->micLevel)); }, was, state->micLevel, kMicTestAnimationDuration); }); AddSkip(layout); //AddDivider(layout); //AddSkip(layout); AddButton( layout, tr::lng_group_call_noise_suppression(), st::groupCallSettingsButton )->toggleOn(rpl::single( settings.groupCallNoiseSuppression() ))->toggledChanges( ) | rpl::start_with_next([=](bool enabled) { Core::App().settings().setGroupCallNoiseSuppression(enabled); call->setNoiseSuppression(enabled); Core::App().saveSettingsDelayed(); }, layout->lifetime()); using GlobalShortcut = base::GlobalShortcut; struct PushToTalkState { rpl::variable recordText = tr::lng_group_call_ptt_shortcut(); rpl::variable shortcutText; rpl::event_stream pushToTalkToggles; std::shared_ptr manager; GlobalShortcut shortcut; crl::time delay = 0; bool recording = false; }; if (base::GlobalShortcutsAvailable()) { const auto state = box->lifetime().make_state(); if (!base::GlobalShortcutsAllowed()) { Core::App().settings().setGroupCallPushToTalk(false); } const auto tryFillFromManager = [=] { state->shortcut = state->manager ? state->manager->shortcutFromSerialized( Core::App().settings().groupCallPushToTalkShortcut()) : nullptr; state->shortcutText = state->shortcut ? state->shortcut->toDisplayString() : QString(); }; state->manager = settings.groupCallPushToTalk() ? call->ensureGlobalShortcutManager() : nullptr; tryFillFromManager(); state->delay = settings.groupCallPushToTalkDelay(); const auto pushToTalk = AddButton( layout, tr::lng_group_call_push_to_talk(), st::groupCallSettingsButton )->toggleOn(rpl::single( settings.groupCallPushToTalk() ) | rpl::then(state->pushToTalkToggles.events())); const auto pushToTalkWrap = layout->add( object_ptr>( layout, object_ptr(layout))); const auto pushToTalkInner = pushToTalkWrap->entity(); const auto recording = AddButton( pushToTalkInner, state->recordText.value(), st::groupCallSettingsButton); CreateRightLabel( recording, state->shortcutText.value(), st::groupCallSettingsButton, state->recordText.value()); const auto applyAndSave = [=] { call->applyGlobalShortcutChanges(); Core::App().saveSettingsDelayed(); }; const auto showPrivacyRequest = [=] { #ifdef Q_OS_MAC if (!Platform::IsMac10_14OrGreater()) { return; } const auto requestInputMonitoring = Platform::IsMac10_15OrGreater(); box->getDelegate()->show(Box([=](not_null box) { box->addRow( object_ptr( box.get(), rpl::combine( tr::lng_group_call_mac_access(), (requestInputMonitoring ? tr::lng_group_call_mac_input() : tr::lng_group_call_mac_accessibility()) ) | rpl::map([](QString a, QString b) { auto result = Ui::Text::RichLangValue(a); result.append("\n\n").append(Ui::Text::RichLangValue(b)); return result; }), st::groupCallBoxLabel), style::margins( st::boxRowPadding.left(), st::boxPadding.top(), st::boxRowPadding.right(), st::boxPadding.bottom())); box->addButton(tr::lng_group_call_mac_settings(), [=] { if (requestInputMonitoring) { Platform::OpenInputMonitoringPrivacySettings(); } else { Platform::OpenAccessibilityPrivacySettings(); } }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); if (!requestInputMonitoring) { // Accessibility is enabled without app restart, so short-poll it. base::timer_each( kCheckAccessibilityInterval ) | rpl::filter([] { return base::GlobalShortcutsAllowed(); }) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); } })); #endif // Q_OS_MAC }; const auto ensureManager = [=] { if (state->manager) { return true; } else if (base::GlobalShortcutsAllowed()) { state->manager = call->ensureGlobalShortcutManager(); tryFillFromManager(); return true; } showPrivacyRequest(); return false; }; const auto stopRecording = [=] { state->recording = false; state->recordText = tr::lng_group_call_ptt_shortcut(); state->shortcutText = state->shortcut ? state->shortcut->toDisplayString() : QString(); recording->setColorOverride(std::nullopt); if (state->manager) { state->manager->stopRecording(); } }; const auto startRecording = [=] { if (!ensureManager()) { state->pushToTalkToggles.fire(false); pushToTalkWrap->hide(anim::type::instant); return; } state->recording = true; state->recordText = tr::lng_group_call_ptt_recording(); recording->setColorOverride( st::groupCallSettingsAttentionButton.textFg->c); auto progress = crl::guard(box, [=](GlobalShortcut shortcut) { state->shortcutText = shortcut->toDisplayString(); }); auto done = crl::guard(box, [=](GlobalShortcut shortcut) { state->shortcut = shortcut; Core::App().settings().setGroupCallPushToTalkShortcut(shortcut ? shortcut->serialize() : QByteArray()); applyAndSave(); stopRecording(); }); state->manager->startRecording(std::move(progress), std::move(done)); }; recording->addClickHandler([=] { if (state->recording) { stopRecording(); } else { startRecording(); } }); const auto label = pushToTalkInner->add( object_ptr( pushToTalkInner, st::groupCallDelayLabel), st::groupCallDelayLabelMargin); const auto value = std::clamp( state->delay, crl::time(0), DelayByIndex(kDelaysCount - 1)); const auto callback = [=](crl::time delay) { state->delay = delay; label->setText(tr::lng_group_call_ptt_delay( tr::now, lt_delay, FormatDelay(delay))); if (Core::App().settings().groupCallPushToTalkDelay() != delay) { Core::App().settings().setGroupCallPushToTalkDelay(delay); applyAndSave(); } }; callback(value); const auto slider = pushToTalkInner->add( object_ptr( pushToTalkInner, st::groupCallDelaySlider), st::groupCallDelayMargin); slider->resize(st::groupCallDelaySlider.seekSize); slider->setPseudoDiscrete( kDelaysCount, DelayByIndex, value, callback); pushToTalkWrap->toggle( settings.groupCallPushToTalk(), anim::type::instant); pushToTalk->toggledChanges( ) | rpl::start_with_next([=](bool toggled) { if (!toggled) { stopRecording(); } else if (!ensureManager()) { state->pushToTalkToggles.fire(false); pushToTalkWrap->hide(anim::type::instant); return; } Core::App().settings().setGroupCallPushToTalk(toggled); applyAndSave(); pushToTalkWrap->toggle(toggled, anim::type::normal); }, pushToTalk->lifetime()); auto boxKeyFilter = [=](not_null e) { return (e->type() == QEvent::KeyPress && state->recording) ? base::EventFilterResult::Cancel : base::EventFilterResult::Continue; }; box->lifetime().make_state>( base::install_event_filter(box, std::move(boxKeyFilter))); } AddSkip(layout); //AddDivider(layout); //AddSkip(layout); } auto shareLink = Fn(); if (peer->isChannel() && peer->asChannel()->hasUsername() && goodReal) { const auto showBox = crl::guard(box, [=]( object_ptr next) { box->getDelegate()->show(std::move(next)); }); const auto showToast = crl::guard(box, [=](QString text) { Ui::ShowMultilineToast({ .parentOverride = Ui::BoxShow(box).toastParent(), .text = { text }, }); }); auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction( peer, showBox, showToast); shareLink = std::move(shareLinkCallback); box->lifetime().add(std::move(shareLinkLifetime)); } else { const auto lookupLink = [=] { if (const auto group = peer->asMegagroup()) { return group->hasUsername() ? group->session().createInternalLinkFull( group->username()) : group->inviteLink(); } else if (const auto chat = peer->asChat()) { return chat->inviteLink(); } return QString(); }; const auto canCreateLink = [&] { if (const auto chat = peer->asChat()) { return chat->canHaveInviteLink(); } else if (const auto group = peer->asMegagroup()) { return group->canHaveInviteLink(); } return false; }; const auto alreadyHasLink = !lookupLink().isEmpty(); if (alreadyHasLink || canCreateLink()) { if (!alreadyHasLink) { // Request invite link. peer->session().api().requestFullPeer(peer); } const auto copyLink = [=] { const auto link = lookupLink(); if (link.isEmpty()) { return false; } QGuiApplication::clipboard()->setText(link); if (weakBox) { Ui::ShowMultilineToast({ .parentOverride = Ui::BoxShow(box).toastParent(), .text = { tr::lng_create_channel_link_copied(tr::now) }, }); } return true; }; shareLink = [=] { if (!copyLink() && !state->generatingLink) { state->generatingLink = true; peer->session().api().inviteLinks().create( peer, crl::guard(layout, [=](auto&&) { copyLink(); })); } }; } } if (shareLink) { AddButton( layout, tr::lng_group_call_share(), st::groupCallSettingsButton )->addClickHandler(std::move(shareLink)); } if (rtmp && !call->rtmpInfo().url.isEmpty()) { AddSkip(layout); addDivider(); AddSkip(layout); struct State { base::unique_qptr menu; mtpRequestId requestId; rpl::event_stream data; }; const auto top = box->addTopButton(st::groupCallMenuToggle); const auto state = top->lifetime().make_state(); const auto revokeSure = [=] { const auto session = &peer->session(); state->requestId = session->api().request( MTPphone_GetGroupCallStreamRtmpUrl( peer->input, MTP_bool(true) )).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) { auto data = result.match([&]( const MTPDphone_groupCallStreamRtmpUrl &data) { return RtmpInfo{ .url = qs(data.vurl()), .key = qs(data.vkey()), }; }); if (const auto call = weakCall.get()) { call->setRtmpInfo(data); } if (!top) { return; } state->requestId = 0; state->data.fire(std::move(data)); }).fail([=] { state->requestId = 0; }).send(); }; const auto revoke = [=] { if (state->requestId || !top) { return; } box->getDelegate()->show(Ui::MakeConfirmBox({ .text = tr::lng_group_call_rtmp_revoke_sure(), .confirmed = [=](Fn &&close) { revokeSure(); close(); }, .confirmText = tr::lng_group_invite_context_revoke(), .labelStyle = &st::groupCallBoxLabel, })); }; top->setClickedCallback([=] { state->menu = base::make_unique_q( box, st::groupCallPopupMenu); state->menu->addAction( tr::lng_group_call_rtmp_revoke(tr::now), revoke); state->menu->setForcedOrigin( Ui::PanelAnimation::Origin::TopRight); top->setForceRippled(true); const auto raw = state->menu.get(); raw->setDestroyedCallback([=] { if ((state->menu == raw) && top) { top->setForceRippled(false); } }); state->menu->popup( top->mapToGlobal(QPoint(top->width() / 2, top->height()))); return true; }); StartRtmpProcess::FillRtmpRows( layout, false, std::make_shared(box), state->data.events(), &st::groupCallBoxLabel, &st::groupCallSettingsRtmpShowButton, &st::groupCallSubsectionTitle, &st::groupCallAttentionBoxButton, &st::groupCallPopupMenu); state->data.fire(call->rtmpInfo()); addDivider(); AddSkip(layout); } if (peer->canManageGroupCall()) { AddButton( layout, (peer->isBroadcast() ? tr::lng_group_call_end_channel() : tr::lng_group_call_end()), st::groupCallSettingsAttentionButton )->addClickHandler([=] { if (const auto call = weakCall.get()) { box->getDelegate()->show(Box( LeaveBox, call, true, BoxContext::GroupCallPanel)); box->closeBox(); } }); } if (!rtmp) { box->setShowFinishedCallback([=] { // Means we finished showing the box. crl::on_main(box, [=] { state->micTester = std::make_unique( Core::App().settings().callAudioBackend(), Core::App().settings().callInputDeviceId()); state->levelUpdateTimer.callEach(kMicTestUpdateInterval); }); }); } box->setTitle(tr::lng_group_call_settings_title()); box->boxClosing( ) | rpl::start_with_next([=] { if (canChangeJoinMuted && muteJoined && muteJoined->toggled() != joinMuted) { SaveCallJoinMuted(peer, id, muteJoined->toggled()); } }, box->lifetime()); box->addButton(tr::lng_box_done(), [=] { box->closeBox(); }); } std::pair, rpl::lifetime> ShareInviteLinkAction( not_null peer, Fn)> showBox, Fn showToast) { auto lifetime = rpl::lifetime(); struct State { State(not_null session) : session(session) { } ~State() { session->api().request(linkListenerRequestId).cancel(); session->api().request(linkSpeakerRequestId).cancel(); } not_null session; std::optional linkSpeaker; QString linkListener; mtpRequestId linkListenerRequestId = 0; mtpRequestId linkSpeakerRequestId = 0; bool generatingLink = false; }; const auto state = lifetime.make_state(&peer->session()); if (!peer->canManageGroupCall()) { state->linkSpeaker = QString(); } const auto shareReady = [=] { if (!state->linkSpeaker.has_value() || state->linkListener.isEmpty()) { return false; } showBox(ShareInviteLinkBox( peer, *state->linkSpeaker, state->linkListener, showToast)); return true; }; auto callback = [=] { const auto real = peer->migrateToOrMe()->groupCall(); if (shareReady() || state->generatingLink || !real) { return; } state->generatingLink = true; state->linkListenerRequestId = peer->session().api().request( MTPphone_ExportGroupCallInvite( MTP_flags(0), real->input() ) ).done([=](const MTPphone_ExportedGroupCallInvite &result) { state->linkListenerRequestId = 0; result.match([&]( const MTPDphone_exportedGroupCallInvite &data) { state->linkListener = qs(data.vlink()); shareReady(); }); }).send(); if (real->rtmp()) { state->linkSpeaker = QString(); state->linkSpeakerRequestId = 0; shareReady(); } else if (!state->linkSpeaker.has_value()) { using Flag = MTPphone_ExportGroupCallInvite::Flag; state->linkSpeakerRequestId = peer->session().api().request( MTPphone_ExportGroupCallInvite( MTP_flags(Flag::f_can_self_unmute), real->input()) ).done([=](const MTPphone_ExportedGroupCallInvite &result) { state->linkSpeakerRequestId = 0; result.match([&]( const MTPDphone_exportedGroupCallInvite &data) { state->linkSpeaker = qs(data.vlink()); shareReady(); }); }).fail([=] { state->linkSpeakerRequestId = 0; state->linkSpeaker = QString(); shareReady(); }).send(); } }; return { std::move(callback), std::move(lifetime) }; } MicLevelTester::MicLevelTester(Fn show) : _show(std::move(show)) , _timer([=] { check(); }) , _tester( std::make_unique( Core::App().settings().callAudioBackend(), Core::App().settings().callInputDeviceId())) { _timer.callEach(kMicrophoneTooltipCheckInterval); } bool MicLevelTester::showTooltip() const { return (_loudCount >= kMicrophoneTooltipAfterLoudCount); } void MicLevelTester::check() { const auto level = _tester->getAndResetLevel(); if (level >= kMicrophoneTooltipLevelThreshold) { _quietCount = 0; if (++_loudCount >= kMicrophoneTooltipAfterLoudCount) { _show(); } } else if (_loudCount > 0 && ++_quietCount >= kDropLoudAfterQuietCount) { _quietCount = 0; _loudCount = 0; } } } // namespace Calls::Group