Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486424af4f | ||
|
|
f3614d6402 | ||
|
|
71151f6bf6 | ||
|
|
2c0ef9c4e9 | ||
|
|
930e971881 | ||
|
|
a576025d4f | ||
|
|
bcd2560e8f | ||
|
|
aede42b0b6 | ||
|
|
56728a066e | ||
|
|
1951b7a8a1 | ||
|
|
0dc0f588c4 | ||
|
|
7d22c631ca | ||
|
|
cf5cc3646a | ||
|
|
375820d5cf | ||
|
|
3955543699 | ||
|
|
c03da00e37 | ||
|
|
edcd462fb9 | ||
|
|
e99558abeb | ||
|
|
feff514a07 | ||
|
|
b1b25b0df9 | ||
|
|
dfee8238c6 | ||
|
|
b7216c40fc | ||
|
|
670d618439 | ||
|
|
d16bc36bae | ||
|
|
31417fd005 | ||
|
|
ae6decf70b | ||
|
|
c80da25450 | ||
|
|
7fd09084fd | ||
|
|
d7496f9824 | ||
|
|
f94280be7f | ||
|
|
0ff6c555b1 | ||
|
|
596c7892c7 | ||
|
|
902e0fc8fb | ||
|
|
91e97b3d65 | ||
|
|
92bc278052 | ||
|
|
16c7ec5b05 | ||
|
|
348712059b | ||
|
|
b3f6fe1c10 | ||
|
|
055ce1ee24 | ||
|
|
c14313d64a | ||
|
|
25665167fa | ||
|
|
af6c7c7d09 | ||
|
|
9c20cf3543 | ||
|
|
4ef2918bcc | ||
|
|
eaf9b58337 | ||
|
|
b85bbadd74 | ||
|
|
7a07acb124 | ||
|
|
2fb220985a | ||
|
|
4b41962ff6 | ||
|
|
e73b522411 | ||
|
|
c5ad7c7c89 | ||
|
|
d301601360 | ||
|
|
f8039f9b99 | ||
|
|
50ed60f443 | ||
|
|
a0b0799399 | ||
|
|
f0b8d4e62b | ||
|
|
d799dfe564 | ||
|
|
cae7e9c502 | ||
|
|
7aa12b6e07 | ||
|
|
bb8647dd4c | ||
|
|
9dc6f117a7 | ||
|
|
cabd7a276b | ||
|
|
a85be4ccd0 | ||
|
|
29e7ea9b36 | ||
|
|
d732a35904 | ||
|
|
b68d5f854d | ||
|
|
146a9c2794 | ||
|
|
00fac70140 | ||
|
|
6b5c422e95 | ||
|
|
945fa2dd4b | ||
|
|
9493a3e8d2 | ||
|
|
823409175e | ||
|
|
703b21b4e5 | ||
|
|
b550eb5ab2 | ||
|
|
aa5e8422bf | ||
|
|
15620b5c2d | ||
|
|
7f7e7b94d6 | ||
|
|
df9d19b16a | ||
|
|
af17046a76 |
1
.github/workflows/snap.yml
vendored
1
.github/workflows/snap.yml
vendored
@@ -51,6 +51,7 @@ jobs:
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: First set up.
|
||||
|
||||
@@ -32,7 +32,7 @@ Version **1.8.15** was the last that supports older systems
|
||||
|
||||
## Third-party
|
||||
|
||||
* Qt 5.12.8, 5.6.2 and 5.3.2 slightly patched ([LGPL](http://doc.qt.io/qt-5/lgpl.html))
|
||||
* Qt 5.15.2, 5.6.2 and 5.3.2 slightly patched ([LGPL](http://doc.qt.io/qt-5/lgpl.html))
|
||||
* OpenSSL 1.1.1 and 1.0.1 ([OpenSSL License](https://www.openssl.org/source/license.html))
|
||||
* WebRTC ([New BSD License](https://github.com/desktop-app/tg_owt/blob/master/LICENSE))
|
||||
* zlib 1.2.11 ([zlib License](http://www.zlib.net/zlib_license.html))
|
||||
|
||||
@@ -60,6 +60,7 @@ PRIVATE
|
||||
desktop-app::external_rlottie
|
||||
desktop-app::external_zlib
|
||||
desktop-app::external_minizip
|
||||
desktop-app::external_qt_static_plugins
|
||||
desktop-app::external_qt
|
||||
desktop-app::external_qr_code_generator
|
||||
desktop-app::external_crash_reports
|
||||
@@ -71,9 +72,6 @@ PRIVATE
|
||||
if (LINUX)
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
desktop-app::external_nimf_qt5
|
||||
desktop-app::external_qt5ct_support
|
||||
desktop-app::external_xcb_screensaver
|
||||
desktop-app::external_xcb
|
||||
desktop-app::external_glib
|
||||
)
|
||||
@@ -83,28 +81,19 @@ if (LINUX)
|
||||
PRIVATE
|
||||
desktop-app::external_statusnotifieritem
|
||||
desktop-app::external_dbusmenu_qt
|
||||
desktop-app::external_fcitx_qt5
|
||||
desktop-app::external_fcitx5_qt5
|
||||
desktop-app::external_hime_qt
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
target_link_libraries(Telegram
|
||||
if (DESKTOP_APP_USE_PACKAGED
|
||||
AND NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION
|
||||
AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
|
||||
|
||||
target_include_directories(Telegram
|
||||
PRIVATE
|
||||
desktop-app::external_materialdecoration
|
||||
${WAYLAND_CLIENT_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
if (DESKTOP_APP_USE_PACKAGED
|
||||
AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
|
||||
|
||||
target_include_directories(Telegram
|
||||
PRIVATE
|
||||
${WAYLAND_CLIENT_INCLUDE_DIRS}
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT TDESKTOP_DISABLE_GTK_INTEGRATION)
|
||||
@@ -235,6 +224,8 @@ PRIVATE
|
||||
boxes/peer_list_box.h
|
||||
boxes/peer_list_controllers.cpp
|
||||
boxes/peer_list_controllers.h
|
||||
boxes/peer_lists_box.cpp
|
||||
boxes/peer_lists_box.h
|
||||
boxes/passcode_box.cpp
|
||||
boxes/passcode_box.h
|
||||
boxes/photo_crop_box.cpp
|
||||
@@ -1098,7 +1089,6 @@ PRIVATE
|
||||
mainwidget.h
|
||||
mainwindow.cpp
|
||||
mainwindow.h
|
||||
qt_static_plugins.cpp
|
||||
settings.cpp
|
||||
settings.h
|
||||
stdafx.h
|
||||
@@ -1320,7 +1310,7 @@ endif()
|
||||
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
|
||||
include(GNUInstallDirs)
|
||||
configure_file("../lib/xdg/telegramdesktop.appdata.xml.in" "${CMAKE_CURRENT_BINARY_DIR}/telegramdesktop.appdata.xml" @ONLY)
|
||||
generate_appdata_changelog(Telegram "../changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/telegramdesktop.appdata.xml")
|
||||
generate_appdata_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/telegramdesktop.appdata.xml")
|
||||
install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "telegram.png")
|
||||
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "telegram.png")
|
||||
|
||||
BIN
Telegram/Resources/icons/calls/group_calls_invited.png
Normal file
BIN
Telegram/Resources/icons/calls/group_calls_invited.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
BIN
Telegram/Resources/icons/calls/group_calls_invited@2x.png
Normal file
BIN
Telegram/Resources/icons/calls/group_calls_invited@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/calls/group_calls_invited@3x.png
Normal file
BIN
Telegram/Resources/icons/calls/group_calls_invited@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1343,7 +1343,6 @@ 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 the circle to cancel";
|
||||
"lng_record_lock_cancel_sure" = "Are you sure you want to stop recording and discard your voice message?";
|
||||
"lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded voice message?";
|
||||
"lng_record_lock_discard" = "Discard";
|
||||
@@ -1499,6 +1498,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_add_to_group" = "Add to group";
|
||||
|
||||
"lng_context_copy_link" = "Copy Link";
|
||||
"lng_context_copy_message_link" = "Copy Message Link";
|
||||
"lng_context_copy_post_link" = "Copy Post Link";
|
||||
"lng_context_copy_email" = "Copy Email Address";
|
||||
"lng_context_copy_hashtag" = "Copy Hashtag";
|
||||
@@ -1820,8 +1820,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_inactive" = "listening";
|
||||
"lng_group_call_settings" = "Settings";
|
||||
"lng_group_call_unmute" = "Unmute";
|
||||
"lng_group_call_unmute_sub" = "or hold spacebar to talk";
|
||||
"lng_group_call_you_are_live" = "You are Live";
|
||||
"lng_group_call_force_muted" = "You are in Listen Only mode";
|
||||
"lng_group_call_force_muted" = "Muted by admin";
|
||||
"lng_group_call_force_muted_sub" = "You are in Listen Only mode";
|
||||
"lng_group_call_connecting" = "Connecting...";
|
||||
"lng_group_call_leave" = "Leave";
|
||||
"lng_group_call_leave_title" = "Leave voice chat";
|
||||
@@ -1830,8 +1832,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_create_sure" = "Do you really want to start a voice chat in this group?";
|
||||
"lng_group_call_also_end" = "End voice chat";
|
||||
"lng_group_call_settings_title" = "Settings";
|
||||
"lng_group_call_invite" = "Invite Member";
|
||||
"lng_group_call_invited_status" = "invited";
|
||||
"lng_group_call_invite_title" = "Invite members";
|
||||
"lng_group_call_invite_button" = "Invite";
|
||||
"lng_group_call_add_to_group_one" = "{user} isn't a member of «{group}» yet. Add them to the group?";
|
||||
"lng_group_call_add_to_group_some" = "Some of those users aren't members of «{group}» yet. Add them to the group?";
|
||||
"lng_group_call_add_to_group_all" = "Those users aren't members of «{group}» yet. Add them to the group?";
|
||||
"lng_group_call_invite_members" = "Group members";
|
||||
"lng_group_call_invite_search_results" = "Search results";
|
||||
"lng_group_call_new_muted" = "Mute new members";
|
||||
"lng_group_call_speakers" = "Speakers";
|
||||
"lng_group_call_microphone" = "Microphone";
|
||||
@@ -1848,8 +1857,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_invite_done_many#one" = "You invited **{count} member** to the voice chat.";
|
||||
"lng_group_call_invite_done_many#other" = "You invited **{count} members** to the voice chat.";
|
||||
"lng_group_call_no_members" = "click to join";
|
||||
"lng_group_call_members#one" = "{count} member";
|
||||
"lng_group_call_members#other" = "{count} members";
|
||||
"lng_group_call_members#one" = "{count} participant";
|
||||
"lng_group_call_members#other" = "{count} participants";
|
||||
"lng_group_call_no_anonymous" = "Sorry, anonymous group admins can't join voice chats.";
|
||||
"lng_group_call_too_many" = "Sorry, there are too many members in this voice chat. Please try again later.";
|
||||
"lng_group_call_context_mute" = "Mute";
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
<file alias="call_end.mp3">../../sounds/call_end.mp3</file>
|
||||
<file alias="call_incoming.mp3">../../sounds/call_incoming.mp3</file>
|
||||
<file alias="call_outgoing.mp3">../../sounds/call_outgoing.mp3</file>
|
||||
<file alias="group_call_start.mp3">../../sounds/group_call_start.mp3</file>
|
||||
<file alias="group_call_connect.mp3">../../sounds/group_call_connect.mp3</file>
|
||||
<file alias="group_call_end.mp3">../../sounds/group_call_end.mp3</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
BIN
Telegram/Resources/sounds/group_call_connect.mp3
Normal file
BIN
Telegram/Resources/sounds/group_call_connect.mp3
Normal file
Binary file not shown.
BIN
Telegram/Resources/sounds/group_call_end.mp3
Normal file
BIN
Telegram/Resources/sounds/group_call_end.mp3
Normal file
Binary file not shown.
BIN
Telegram/Resources/sounds/group_call_start.mp3
Normal file
BIN
Telegram/Resources/sounds/group_call_start.mp3
Normal file
Binary file not shown.
@@ -63,7 +63,7 @@ inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMe
|
||||
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
|
||||
inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia;
|
||||
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaDocument#33473058 flags:# id:InputDocument ttl_seconds:flags.0?int query:flags.1?string = InputMedia;
|
||||
inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;
|
||||
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
@@ -1195,7 +1195,7 @@ groupCall#55903081 flags:# join_muted:flags.1?true can_change_join_muted:flags.2
|
||||
|
||||
inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall;
|
||||
|
||||
groupCallParticipant#56b087c9 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true user_id:int date:int active_date:flags.3?int source:int = GroupCallParticipant;
|
||||
groupCallParticipant#56b087c9 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true user_id:int date:int active_date:flags.3?int source:int = GroupCallParticipant;
|
||||
|
||||
phone.groupCall#66ab0bfc call:GroupCall participants:Vector<GroupCallParticipant> participants_next_offset:string users:Vector<User> = phone.GroupCall;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="2.4.14.0" />
|
||||
Version="2.5.2.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,4,14,0
|
||||
PRODUCTVERSION 2,4,14,0
|
||||
FILEVERSION 2,5,2,0
|
||||
PRODUCTVERSION 2,5,2,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "2.4.14.0"
|
||||
VALUE "FileVersion", "2.5.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.14.0"
|
||||
VALUE "ProductVersion", "2.5.2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,4,14,0
|
||||
PRODUCTVERSION 2,4,14,0
|
||||
FILEVERSION 2,5,2,0
|
||||
PRODUCTVERSION 2,5,2,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "2.4.14.0"
|
||||
VALUE "FileVersion", "2.5.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.14.0"
|
||||
VALUE "ProductVersion", "2.5.2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -125,13 +125,20 @@ void Authorizations::requestTerminate(
|
||||
Fn<void(const MTPBool &result)> &&done,
|
||||
Fn<void(const RPCError &error)> &&fail,
|
||||
std::optional<uint64> hash) {
|
||||
auto request = hash
|
||||
? MTPaccount_ResetAuthorization(MTP_long(*hash))
|
||||
: MTPaccount_ResetAuthorization();
|
||||
_api.request(std::move(request))
|
||||
.done(std::move(done))
|
||||
.fail(std::move(fail))
|
||||
.send();
|
||||
const auto send = [&](auto request) {
|
||||
_api.request(
|
||||
std::move(request)
|
||||
).done(
|
||||
std::move(done)
|
||||
).fail(
|
||||
std::move(fail)
|
||||
).send();
|
||||
};
|
||||
if (hash) {
|
||||
send(MTPaccount_ResetAuthorization(MTP_long(*hash)));
|
||||
} else {
|
||||
send(MTPauth_ResetAuthorizations());
|
||||
}
|
||||
}
|
||||
|
||||
Authorizations::List Authorizations::list() const {
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace Api {
|
||||
namespace {
|
||||
|
||||
constexpr auto kCancelTypingActionTimeout = crl::time(5000);
|
||||
constexpr auto kSetMyActionForMs = 10 * crl::time(1000);
|
||||
constexpr auto kSendMySpeakingInterval = 3 * crl::time(1000);
|
||||
constexpr auto kSendMyTypingInterval = 5 * crl::time(1000);
|
||||
constexpr auto kSendTypingsToOfflineFor = TimeId(30);
|
||||
|
||||
} // namespace
|
||||
@@ -82,12 +83,15 @@ bool SendProgressManager::updated(const Key &key, bool doing) {
|
||||
const auto now = crl::now();
|
||||
const auto i = _updated.find(key);
|
||||
if (doing) {
|
||||
const auto sendEach = (key.type == SendProgressType::Speaking)
|
||||
? kSendMySpeakingInterval
|
||||
: kSendMyTypingInterval;
|
||||
if (i == end(_updated)) {
|
||||
_updated.emplace(key, now + kSetMyActionForMs);
|
||||
} else if (i->second > now + (kSetMyActionForMs / 2)) {
|
||||
_updated.emplace(key, now + 2 * sendEach);
|
||||
} else if (i->second > now + sendEach) {
|
||||
return false;
|
||||
} else {
|
||||
i->second = now + kSetMyActionForMs;
|
||||
i->second = now + 2 * sendEach;
|
||||
}
|
||||
} else {
|
||||
if (i == end(_updated)) {
|
||||
|
||||
@@ -182,7 +182,8 @@ void SendExistingDocument(
|
||||
return MTP_inputMediaDocument(
|
||||
MTP_flags(0),
|
||||
document->mtpInput(),
|
||||
MTPint());
|
||||
MTPint(), // ttl_seconds
|
||||
MTPstring()); // query
|
||||
};
|
||||
SendExistingMedia(
|
||||
std::move(message),
|
||||
|
||||
@@ -245,7 +245,10 @@ Updates::Updates(not_null<Main::Session*> session)
|
||||
for (const auto [userId, when] : *users) {
|
||||
call->applyActiveUpdate(
|
||||
userId,
|
||||
when,
|
||||
Data::LastSpokeTimes{
|
||||
.anything = when,
|
||||
.voice = when
|
||||
},
|
||||
peer->owner().userLoaded(userId));
|
||||
}
|
||||
}
|
||||
@@ -925,10 +928,16 @@ void Updates::handleSendActionUpdate(
|
||||
const auto isSpeakingInCall = (action.type()
|
||||
== mtpc_speakingInGroupCallAction);
|
||||
if (isSpeakingInCall) {
|
||||
if (!peer->isChat() && !peer->isChannel()) {
|
||||
return;
|
||||
}
|
||||
const auto call = peer->groupCall();
|
||||
const auto now = crl::now();
|
||||
if (call) {
|
||||
call->applyActiveUpdate(userId, now, user);
|
||||
call->applyActiveUpdate(
|
||||
userId,
|
||||
Data::LastSpokeTimes{ .anything = now, .voice = now },
|
||||
user);
|
||||
} else {
|
||||
const auto chat = peer->asChat();
|
||||
const auto channel = peer->asChannel();
|
||||
@@ -937,8 +946,8 @@ void Updates::handleSendActionUpdate(
|
||||
: (channel->flags() & MTPDchannel::Flag::f_call_active);
|
||||
if (active) {
|
||||
_pendingSpeakingCallMembers.emplace(
|
||||
channel).first->second[userId] = now;
|
||||
session().api().requestFullPeer(channel);
|
||||
peer).first->second[userId] = now;
|
||||
session().api().requestFullPeer(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1836,15 +1845,6 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
case mtpc_updatePhoneCallSignalingData:
|
||||
case mtpc_updateGroupCallParticipants:
|
||||
case mtpc_updateGroupCall: {
|
||||
if (update.type() == mtpc_updateGroupCall) {
|
||||
const auto &data = update.c_updateGroupCall();
|
||||
if (data.vcall().type() == mtpc_groupCallDiscarded) {
|
||||
const auto &call = data.vcall().c_groupCallDiscarded();
|
||||
session().data().groupCallDiscarded(
|
||||
call.vid().v,
|
||||
call.vduration().v);
|
||||
}
|
||||
}
|
||||
Core::App().calls().handleUpdate(&session(), update);
|
||||
} break;
|
||||
|
||||
|
||||
@@ -3458,7 +3458,8 @@ void ApiWrap::checkForUnreadMentions(
|
||||
|
||||
void ApiWrap::addChatParticipants(
|
||||
not_null<PeerData*> peer,
|
||||
const std::vector<not_null<UserData*>> &users) {
|
||||
const std::vector<not_null<UserData*>> &users,
|
||||
Fn<void(bool)> done) {
|
||||
if (const auto chat = peer->asChat()) {
|
||||
for (const auto user : users) {
|
||||
request(MTPmessages_AddChatUser(
|
||||
@@ -3467,8 +3468,10 @@ void ApiWrap::addChatParticipants(
|
||||
MTP_int(kForwardMessagesOnAdd)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
applyUpdates(result);
|
||||
if (done) done(true);
|
||||
}).fail([=](const RPCError &error) {
|
||||
ShowAddParticipantsError(error.type(), peer, { 1, user });
|
||||
if (done) done(false);
|
||||
}).afterDelay(crl::time(5)).send();
|
||||
}
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
@@ -3480,14 +3483,17 @@ void ApiWrap::addChatParticipants(
|
||||
auto list = QVector<MTPInputUser>();
|
||||
list.reserve(qMin(int(users.size()), int(kMaxUsersPerInvite)));
|
||||
const auto send = [&] {
|
||||
const auto callback = base::take(done);
|
||||
request(MTPchannels_InviteToChannel(
|
||||
channel->inputChannel,
|
||||
MTP_vector<MTPInputUser>(list)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
applyUpdates(result);
|
||||
requestParticipantsCountDelayed(channel);
|
||||
if (callback) callback(true);
|
||||
}).fail([=](const RPCError &error) {
|
||||
ShowAddParticipantsError(error.type(), peer, users);
|
||||
if (callback) callback(false);
|
||||
}).afterDelay(crl::time(5)).send();
|
||||
};
|
||||
for (const auto user : users) {
|
||||
@@ -4639,7 +4645,8 @@ void ApiWrap::uploadAlbumMedia(
|
||||
fields.vid(),
|
||||
fields.vaccess_hash(),
|
||||
fields.vfile_reference()),
|
||||
MTP_int(data.vttl_seconds().value_or_empty()));
|
||||
MTP_int(data.vttl_seconds().value_or_empty()),
|
||||
MTPstring()); // query
|
||||
sendAlbumWithUploaded(item, groupId, media);
|
||||
} break;
|
||||
}
|
||||
|
||||
@@ -366,7 +366,8 @@ public:
|
||||
Fn<void()> callbackNotModified = nullptr);
|
||||
void addChatParticipants(
|
||||
not_null<PeerData*> peer,
|
||||
const std::vector<not_null<UserData*>> &users);
|
||||
const std::vector<not_null<UserData*>> &users,
|
||||
Fn<void(bool)> done = nullptr);
|
||||
|
||||
rpl::producer<SendAction> sendActions() const {
|
||||
return _sendActions.events();
|
||||
|
||||
@@ -632,7 +632,7 @@ void GroupInfoBox::submit() {
|
||||
not_null<PeerListBox*> box) {
|
||||
auto create = [box, title, weak] {
|
||||
if (weak) {
|
||||
auto rows = box->peerListCollectSelectedRows();
|
||||
auto rows = box->collectSelectedRows();
|
||||
if (!rows.empty()) {
|
||||
weak->createGroup(box, title, rows);
|
||||
}
|
||||
@@ -643,7 +643,8 @@ void GroupInfoBox::submit() {
|
||||
};
|
||||
Ui::show(
|
||||
Box<PeerListBox>(
|
||||
std::make_unique<AddParticipantsBoxController>(_navigation),
|
||||
std::make_unique<AddParticipantsBoxController>(
|
||||
&_navigation->session()),
|
||||
std::move(initBox)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
@@ -801,7 +801,10 @@ void DeleteMessagesBox::resizeEvent(QResizeEvent *e) {
|
||||
|
||||
void DeleteMessagesBox::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
|
||||
deleteAndClear();
|
||||
// Don't make the clearing history so easy.
|
||||
if (!_wipeHistoryPeer) {
|
||||
deleteAndClear();
|
||||
}
|
||||
} else {
|
||||
BoxContent::keyPressEvent(e);
|
||||
}
|
||||
|
||||
@@ -33,38 +33,36 @@ namespace {
|
||||
class PrivacyExceptionsBoxController : public ChatsListBoxController {
|
||||
public:
|
||||
PrivacyExceptionsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<QString> title,
|
||||
const std::vector<not_null<PeerData*>> &selected);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
std::vector<not_null<PeerData*>> getResult() const;
|
||||
|
||||
protected:
|
||||
void prepareViewHook() override;
|
||||
std::unique_ptr<Row> createRow(not_null<History*> history) override;
|
||||
|
||||
private:
|
||||
not_null<Window::SessionNavigation*> _navigation;
|
||||
const not_null<Main::Session*> _session;
|
||||
rpl::producer<QString> _title;
|
||||
std::vector<not_null<PeerData*>> _selected;
|
||||
|
||||
};
|
||||
|
||||
PrivacyExceptionsBoxController::PrivacyExceptionsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<QString> title,
|
||||
const std::vector<not_null<PeerData*>> &selected)
|
||||
: ChatsListBoxController(navigation)
|
||||
, _navigation(navigation)
|
||||
: ChatsListBoxController(session)
|
||||
, _session(session)
|
||||
, _title(std::move(title))
|
||||
, _selected(selected) {
|
||||
}
|
||||
|
||||
Main::Session &PrivacyExceptionsBoxController::session() const {
|
||||
return _navigation->session();
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void PrivacyExceptionsBoxController::prepareViewHook() {
|
||||
@@ -72,10 +70,6 @@ void PrivacyExceptionsBoxController::prepareViewHook() {
|
||||
delegate()->peerListAddSelectedPeers(_selected);
|
||||
}
|
||||
|
||||
std::vector<not_null<PeerData*>> PrivacyExceptionsBoxController::getResult() const {
|
||||
return delegate()->peerListCollectSelectedRows();
|
||||
}
|
||||
|
||||
void PrivacyExceptionsBoxController::rowClicked(not_null<PeerListRow*> row) {
|
||||
const auto peer = row->peer();
|
||||
|
||||
@@ -146,13 +140,13 @@ void EditPrivacyBox::editExceptions(
|
||||
Exception exception,
|
||||
Fn<void()> done) {
|
||||
auto controller = std::make_unique<PrivacyExceptionsBoxController>(
|
||||
_window,
|
||||
&_window->session(),
|
||||
_controller->exceptionBoxTitle(exception),
|
||||
exceptions(exception));
|
||||
auto initBox = [=, controller = controller.get()](
|
||||
not_null<PeerListBox*> box) {
|
||||
box->addButton(tr::lng_settings_save(), crl::guard(this, [=] {
|
||||
exceptions(exception) = controller->getResult();
|
||||
exceptions(exception) = box->collectSelectedRows();
|
||||
const auto type = [&] {
|
||||
switch (exception) {
|
||||
case Exception::Always: return Exception::Never;
|
||||
|
||||
@@ -320,7 +320,7 @@ void EditExceptions(
|
||||
const auto include = (options & Flag::Contacts) != Flags(0);
|
||||
const auto rules = data->current();
|
||||
auto controller = std::make_unique<EditFilterChatsListController>(
|
||||
window,
|
||||
&window->session(),
|
||||
(include
|
||||
? tr::lng_filters_include_title()
|
||||
: tr::lng_filters_exclude_title()),
|
||||
@@ -331,7 +331,7 @@ void EditExceptions(
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->setCloseByOutsideClick(false);
|
||||
box->addButton(tr::lng_settings_save(), crl::guard(context, [=] {
|
||||
const auto peers = box->peerListCollectSelectedRows();
|
||||
const auto peers = box->collectSelectedRows();
|
||||
const auto rules = data->current();
|
||||
auto &&histories = ranges::view::all(
|
||||
peers
|
||||
|
||||
@@ -68,7 +68,6 @@ public:
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
|
||||
void peerListScrollToTop() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
@@ -209,11 +208,6 @@ int TypeDelegate::peerListSelectedRowsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto TypeDelegate::peerListCollectSelectedRows()
|
||||
-> std::vector<not_null<PeerData*>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListScrollToTop() {
|
||||
}
|
||||
|
||||
@@ -347,13 +341,13 @@ void PaintFilterChatsTypeIcon(
|
||||
}
|
||||
|
||||
EditFilterChatsListController::EditFilterChatsListController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<QString> title,
|
||||
Flags options,
|
||||
Flags selected,
|
||||
const base::flat_set<not_null<History*>> &peers)
|
||||
: ChatsListBoxController(navigation)
|
||||
, _navigation(navigation)
|
||||
: ChatsListBoxController(session)
|
||||
, _session(session)
|
||||
, _title(std::move(title))
|
||||
, _peers(peers)
|
||||
, _options(options)
|
||||
@@ -361,7 +355,7 @@ EditFilterChatsListController::EditFilterChatsListController(
|
||||
}
|
||||
|
||||
Main::Session &EditFilterChatsListController::session() const {
|
||||
return _navigation->session();
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void EditFilterChatsListController::rowClicked(not_null<PeerListRow*> row) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
using Flags = Data::ChatFilter::Flags;
|
||||
|
||||
EditFilterChatsListController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<QString> title,
|
||||
Flags options,
|
||||
Flags selected,
|
||||
@@ -64,7 +64,7 @@ private:
|
||||
|
||||
void updateTitle();
|
||||
|
||||
const not_null<Window::SessionNavigation*> _navigation;
|
||||
const not_null<Main::Session*> _session;
|
||||
rpl::producer<QString> _title;
|
||||
base::flat_set<not_null<History*>> _peers;
|
||||
Flags _options;
|
||||
|
||||
@@ -399,7 +399,7 @@ int PeerListBox::peerListSelectedRowsCount() {
|
||||
return _select ? _select->entity()->getItemsCount() : 0;
|
||||
}
|
||||
|
||||
auto PeerListBox::peerListCollectSelectedRows()
|
||||
auto PeerListBox::collectSelectedRows()
|
||||
-> std::vector<not_null<PeerData*>> {
|
||||
auto result = std::vector<not_null<PeerData*>>();
|
||||
auto items = _select
|
||||
@@ -982,6 +982,18 @@ void PeerListContent::setAboveWidget(object_ptr<TWidget> widget) {
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContent::setAboveSearchWidget(object_ptr<TWidget> widget) {
|
||||
_aboveSearchWidget = std::move(widget);
|
||||
if (_aboveSearchWidget) {
|
||||
_aboveSearchWidget->setParent(this);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContent::setHideEmpty(bool hide) {
|
||||
_hideEmpty = hide;
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
void PeerListContent::setBelowWidget(object_ptr<TWidget> widget) {
|
||||
_belowWidget = std::move(widget);
|
||||
if (_belowWidget) {
|
||||
@@ -990,6 +1002,9 @@ void PeerListContent::setBelowWidget(object_ptr<TWidget> widget) {
|
||||
}
|
||||
|
||||
int PeerListContent::labelHeight() const {
|
||||
if (_hideEmpty && !shownRowsCount()) {
|
||||
return 0;
|
||||
}
|
||||
auto computeLabelHeight = [](auto &label) {
|
||||
if (!label) {
|
||||
return 0;
|
||||
@@ -1082,34 +1097,45 @@ void PeerListContent::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
|
||||
int PeerListContent::resizeGetHeight(int newWidth) {
|
||||
const auto rowsCount = shownRowsCount();
|
||||
const auto hideAll = !rowsCount && _hideEmpty;
|
||||
_aboveHeight = 0;
|
||||
if (_aboveWidget) {
|
||||
_aboveWidget->resizeToWidth(newWidth);
|
||||
_aboveWidget->moveToLeft(0, 0, newWidth);
|
||||
if (showingSearch()) {
|
||||
if (hideAll || showingSearch()) {
|
||||
_aboveWidget->hide();
|
||||
} else {
|
||||
_aboveWidget->show();
|
||||
_aboveHeight = _aboveWidget->height();
|
||||
}
|
||||
}
|
||||
const auto rowsCount = shownRowsCount();
|
||||
if (_aboveSearchWidget) {
|
||||
_aboveSearchWidget->resizeToWidth(newWidth);
|
||||
_aboveSearchWidget->moveToLeft(0, 0, newWidth);
|
||||
if (hideAll || !showingSearch()) {
|
||||
_aboveSearchWidget->hide();
|
||||
} else {
|
||||
_aboveSearchWidget->show();
|
||||
_aboveHeight = _aboveSearchWidget->height();
|
||||
}
|
||||
}
|
||||
const auto labelTop = rowsTop() + qMax(1, shownRowsCount()) * _rowHeight;
|
||||
const auto labelWidth = newWidth - 2 * st::contactsPadding.left();
|
||||
if (_description) {
|
||||
_description->resizeToWidth(labelWidth);
|
||||
_description->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
|
||||
_description->setVisible(!showingSearch());
|
||||
_description->setVisible(!hideAll && !showingSearch());
|
||||
}
|
||||
if (_searchNoResults) {
|
||||
_searchNoResults->resizeToWidth(labelWidth);
|
||||
_searchNoResults->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
|
||||
_searchNoResults->setVisible(showingSearch() && _filterResults.empty() && !_controller->isSearchLoading());
|
||||
_searchNoResults->setVisible(!hideAll && showingSearch() && _filterResults.empty() && !_controller->isSearchLoading());
|
||||
}
|
||||
if (_searchLoading) {
|
||||
_searchLoading->resizeToWidth(labelWidth);
|
||||
_searchLoading->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
|
||||
_searchLoading->setVisible(showingSearch() && _filterResults.empty() && _controller->isSearchLoading());
|
||||
_searchLoading->setVisible(!hideAll && showingSearch() && _filterResults.empty() && _controller->isSearchLoading());
|
||||
}
|
||||
const auto label = labelHeight();
|
||||
const auto belowTop = (label > 0 || rowsCount > 0)
|
||||
@@ -1119,7 +1145,7 @@ int PeerListContent::resizeGetHeight(int newWidth) {
|
||||
if (_belowWidget) {
|
||||
_belowWidget->resizeToWidth(newWidth);
|
||||
_belowWidget->moveToLeft(0, belowTop, newWidth);
|
||||
if (showingSearch()) {
|
||||
if (hideAll || showingSearch()) {
|
||||
_belowWidget->hide();
|
||||
} else {
|
||||
_belowWidget->show();
|
||||
@@ -1203,33 +1229,56 @@ void PeerListContent::mousePressReleased(Qt::MouseButton button) {
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
|
||||
void PeerListContent::showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
|
||||
showRowMenu(findRowIndex(row), QCursor::pos(), std::move(destroyed));
|
||||
}
|
||||
|
||||
bool PeerListContent::showRowMenu(
|
||||
RowIndex index,
|
||||
QPoint globalPos,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
|
||||
if (_contextMenu) {
|
||||
_contextMenu->deleteLater();
|
||||
_contextMenu->setDestroyedCallback(nullptr);
|
||||
_contextMenu = nullptr;
|
||||
}
|
||||
setContexted(Selected());
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
handleMouseMove(e->globalPos());
|
||||
}
|
||||
|
||||
setContexted(_selected);
|
||||
if (_pressButton != Qt::LeftButton) {
|
||||
mousePressReleased(_pressButton);
|
||||
}
|
||||
|
||||
if (const auto row = getRow(_contexted.index)) {
|
||||
_contextMenu = _controller->rowContextMenu(this, row);
|
||||
if (_contextMenu) {
|
||||
_contextMenu->setDestroyedCallback(crl::guard(
|
||||
this,
|
||||
[this] {
|
||||
setContexted(Selected());
|
||||
handleMouseMove(QCursor::pos());
|
||||
}));
|
||||
_contextMenu->popup(e->globalPos());
|
||||
e->accept();
|
||||
}
|
||||
const auto row = getRow(index);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_contextMenu = _controller->rowContextMenu(this, row);
|
||||
const auto raw = _contextMenu.get();
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setContexted({ index, false });
|
||||
raw->setDestroyedCallback(crl::guard(
|
||||
this,
|
||||
[=] {
|
||||
setContexted(Selected());
|
||||
handleMouseMove(QCursor::pos());
|
||||
if (destroyed) {
|
||||
destroyed(raw);
|
||||
}
|
||||
}));
|
||||
raw->popup(globalPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
handleMouseMove(e->globalPos());
|
||||
}
|
||||
if (showRowMenu(_selected.index, e->globalPos())) {
|
||||
e->accept();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1351,15 +1400,18 @@ crl::time PeerListContent::paintRow(
|
||||
return (refreshStatusAt - ms);
|
||||
}
|
||||
|
||||
void PeerListContent::selectSkip(int direction) {
|
||||
if (_pressed.index.value >= 0) {
|
||||
return;
|
||||
PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
|
||||
if (hasPressed()) {
|
||||
return { _selected.index.value, _selected.index.value };
|
||||
}
|
||||
_mouseSelection = false;
|
||||
_lastMousePosition = std::nullopt;
|
||||
|
||||
auto newSelectedIndex = _selected.index.value + direction;
|
||||
|
||||
auto result = SkipResult();
|
||||
result.shouldMoveTo = newSelectedIndex;
|
||||
|
||||
auto rowsCount = shownRowsCount();
|
||||
auto index = 0;
|
||||
auto firstEnabled = -1, lastEnabled = -1;
|
||||
@@ -1415,14 +1467,36 @@ void PeerListContent::selectSkip(int direction) {
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
_selectedIndex = _selected.index.value;
|
||||
result.reallyMovedTo = _selected.index.value;
|
||||
return result;
|
||||
}
|
||||
|
||||
void PeerListContent::selectSkipPage(int height, int direction) {
|
||||
auto rowsToSkip = height / _rowHeight;
|
||||
if (!rowsToSkip) return;
|
||||
if (!rowsToSkip) {
|
||||
return;
|
||||
}
|
||||
selectSkip(rowsToSkip * direction);
|
||||
}
|
||||
|
||||
rpl::producer<int> PeerListContent::selectedIndexValue() const {
|
||||
return _selectedIndex.value();
|
||||
}
|
||||
|
||||
bool PeerListContent::hasSelection() const {
|
||||
return _selected.index.value >= 0;
|
||||
}
|
||||
|
||||
bool PeerListContent::hasPressed() const {
|
||||
return _pressed.index.value >= 0;
|
||||
}
|
||||
|
||||
void PeerListContent::clearSelection() {
|
||||
setSelected(Selected());
|
||||
}
|
||||
|
||||
void PeerListContent::loadProfilePhotos() {
|
||||
if (_visibleTop >= _visibleBottom) return;
|
||||
|
||||
@@ -1569,14 +1643,17 @@ void PeerListContent::setSearchQuery(
|
||||
clearSearchRows();
|
||||
}
|
||||
|
||||
void PeerListContent::submitted() {
|
||||
bool PeerListContent::submitted() {
|
||||
if (const auto row = getRow(_selected.index)) {
|
||||
_controller->rowClicked(row);
|
||||
return true;
|
||||
} else if (showingSearch()) {
|
||||
if (const auto row = getRow(RowIndex(0))) {
|
||||
_controller->rowClicked(row);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PeerListContent::visibleTopBottomUpdated(
|
||||
@@ -1590,11 +1667,14 @@ void PeerListContent::visibleTopBottomUpdated(
|
||||
|
||||
void PeerListContent::setSelected(Selected selected) {
|
||||
updateRow(_selected.index);
|
||||
if (_selected != selected) {
|
||||
_selected = selected;
|
||||
updateRow(_selected.index);
|
||||
setCursor(_selected.action ? style::cur_pointer : style::cur_default);
|
||||
if (_selected == selected) {
|
||||
return;
|
||||
}
|
||||
_selected = selected;
|
||||
updateRow(_selected.index);
|
||||
setCursor(_selected.action ? style::cur_pointer : style::cur_default);
|
||||
|
||||
_selectedIndex = _selected.index.value;
|
||||
}
|
||||
|
||||
void PeerListContent::setContexted(Selected contexted) {
|
||||
|
||||
@@ -254,10 +254,12 @@ class PeerListDelegate {
|
||||
public:
|
||||
virtual void peerListSetTitle(rpl::producer<QString> title) = 0;
|
||||
virtual void peerListSetAdditionalTitle(rpl::producer<QString> title) = 0;
|
||||
virtual void peerListSetHideEmpty(bool hide) = 0;
|
||||
virtual void peerListSetDescription(object_ptr<Ui::FlatLabel> description) = 0;
|
||||
virtual void peerListSetSearchLoading(object_ptr<Ui::FlatLabel> loading) = 0;
|
||||
virtual void peerListSetSearchNoResults(object_ptr<Ui::FlatLabel> noResults) = 0;
|
||||
virtual void peerListSetAboveWidget(object_ptr<TWidget> aboveWidget) = 0;
|
||||
virtual void peerListSetAboveSearchWidget(object_ptr<TWidget> aboveWidget) = 0;
|
||||
virtual void peerListSetBelowWidget(object_ptr<TWidget> belowWidget) = 0;
|
||||
virtual void peerListSetSearchMode(PeerListSearchMode mode) = 0;
|
||||
virtual void peerListAppendRow(std::unique_ptr<PeerListRow> row) = 0;
|
||||
@@ -298,8 +300,10 @@ public:
|
||||
peerListFinishSelectedRowsBunch();
|
||||
}
|
||||
|
||||
virtual void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) = 0;
|
||||
virtual int peerListSelectedRowsCount() = 0;
|
||||
virtual std::vector<not_null<PeerData*>> peerListCollectSelectedRows() = 0;
|
||||
virtual std::unique_ptr<PeerListState> peerListSaveState() const = 0;
|
||||
virtual void peerListRestoreState(
|
||||
std::unique_ptr<PeerListState> state) = 0;
|
||||
@@ -499,13 +503,20 @@ public:
|
||||
QWidget *parent,
|
||||
not_null<PeerListController*> controller);
|
||||
|
||||
void selectSkip(int direction);
|
||||
struct SkipResult {
|
||||
int shouldMoveTo = 0;
|
||||
int reallyMovedTo = 0;
|
||||
};
|
||||
SkipResult selectSkip(int direction);
|
||||
void selectSkipPage(int height, int direction);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> selectedIndexValue() const;
|
||||
[[nodiscard]] bool hasSelection() const;
|
||||
[[nodiscard]] bool hasPressed() const;
|
||||
void clearSelection();
|
||||
|
||||
void searchQueryChanged(QString query);
|
||||
void submitted();
|
||||
bool submitted();
|
||||
|
||||
// Interface for the controller.
|
||||
void appendRow(std::unique_ptr<PeerListRow> row);
|
||||
@@ -525,7 +536,9 @@ public:
|
||||
void setSearchLoading(object_ptr<Ui::FlatLabel> loading);
|
||||
void setSearchNoResults(object_ptr<Ui::FlatLabel> noResults);
|
||||
void setAboveWidget(object_ptr<TWidget> widget);
|
||||
void setAboveSearchWidget(object_ptr<TWidget> widget);
|
||||
void setBelowWidget(object_ptr<TWidget> width);
|
||||
void setHideEmpty(bool hide);
|
||||
void refreshRows();
|
||||
|
||||
void setSearchMode(PeerListSearchMode mode);
|
||||
@@ -547,6 +560,10 @@ public:
|
||||
std::unique_ptr<PeerListState> saveState() const;
|
||||
void restoreState(std::unique_ptr<PeerListState> state);
|
||||
|
||||
void showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed);
|
||||
|
||||
auto scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
@@ -630,6 +647,11 @@ private:
|
||||
RowIndex findRowIndex(not_null<PeerListRow*> row, RowIndex hint = RowIndex());
|
||||
QRect getActiveActionRect(not_null<PeerListRow*> row, RowIndex index) const;
|
||||
|
||||
bool showRowMenu(
|
||||
RowIndex index,
|
||||
QPoint globalPos,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr);
|
||||
|
||||
crl::time paintRow(Painter &p, crl::time ms, RowIndex index);
|
||||
|
||||
void addRowEntry(not_null<PeerListRow*> row);
|
||||
@@ -667,6 +689,7 @@ private:
|
||||
Selected _selected;
|
||||
Selected _pressed;
|
||||
Selected _contexted;
|
||||
rpl::variable<int> _selectedIndex = -1;
|
||||
bool _mouseSelection = false;
|
||||
std::optional<QPoint> _lastMousePosition;
|
||||
Qt::MouseButton _pressButton = Qt::LeftButton;
|
||||
@@ -685,7 +708,9 @@ private:
|
||||
|
||||
int _aboveHeight = 0;
|
||||
int _belowHeight = 0;
|
||||
bool _hideEmpty = false;
|
||||
object_ptr<TWidget> _aboveWidget = { nullptr };
|
||||
object_ptr<TWidget> _aboveSearchWidget = { nullptr };
|
||||
object_ptr<TWidget> _belowWidget = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _description = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _searchNoResults = { nullptr };
|
||||
@@ -703,6 +728,9 @@ public:
|
||||
_content = content;
|
||||
}
|
||||
|
||||
void peerListSetHideEmpty(bool hide) override {
|
||||
_content->setHideEmpty(hide);
|
||||
}
|
||||
void peerListAppendRow(
|
||||
std::unique_ptr<PeerListRow> row) override {
|
||||
_content->appendRow(std::move(row));
|
||||
@@ -767,6 +795,9 @@ public:
|
||||
void peerListSetAboveWidget(object_ptr<TWidget> aboveWidget) override {
|
||||
_content->setAboveWidget(std::move(aboveWidget));
|
||||
}
|
||||
void peerListSetAboveSearchWidget(object_ptr<TWidget> aboveWidget) override {
|
||||
_content->setAboveSearchWidget(std::move(aboveWidget));
|
||||
}
|
||||
void peerListSetBelowWidget(object_ptr<TWidget> belowWidget) override {
|
||||
_content->setBelowWidget(std::move(belowWidget));
|
||||
}
|
||||
@@ -804,6 +835,11 @@ public:
|
||||
std::unique_ptr<PeerListState> state) override {
|
||||
_content->restoreState(std::move(state));
|
||||
}
|
||||
void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) override {
|
||||
_content->showRowMenu(row, std::move(destroyed));
|
||||
}
|
||||
|
||||
protected:
|
||||
not_null<PeerListContent*> content() const {
|
||||
@@ -824,6 +860,8 @@ public:
|
||||
std::unique_ptr<PeerListController> controller,
|
||||
Fn<void(not_null<PeerListBox*>)> init);
|
||||
|
||||
[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();
|
||||
|
||||
void peerListSetTitle(rpl::producer<QString> title) override {
|
||||
setTitle(std::move(title));
|
||||
}
|
||||
@@ -840,7 +878,6 @@ public:
|
||||
anim::type animated) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
|
||||
void peerListScrollToTop() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -23,7 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lang/lang_keys.h"
|
||||
#include "history/history.h"
|
||||
#include "dialogs/dialogs_main_list.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_session_controller.h" // onShowAddContact()
|
||||
#include "facades.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_profile.h"
|
||||
@@ -115,7 +115,8 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
|
||||
[=] { controller->widget()->onShowAddContact(); });
|
||||
};
|
||||
return Box<PeerListBox>(
|
||||
std::make_unique<ContactsBoxController>(controller),
|
||||
std::make_unique<ContactsBoxController>(
|
||||
&sessionController->session()),
|
||||
std::move(delegate));
|
||||
}
|
||||
|
||||
@@ -159,9 +160,9 @@ void PeerListRowWithLink::paintAction(
|
||||
}
|
||||
|
||||
PeerListGlobalSearchController::PeerListGlobalSearchController(
|
||||
not_null<Window::SessionNavigation*> navigation)
|
||||
: _navigation(navigation)
|
||||
, _api(&_navigation->session().mtp()) {
|
||||
not_null<Main::Session*> session)
|
||||
: _session(session)
|
||||
, _api(&session->mtp()) {
|
||||
_timer.setCallback([this] { searchOnServer(); });
|
||||
}
|
||||
|
||||
@@ -210,8 +211,8 @@ void PeerListGlobalSearchController::searchDone(
|
||||
auto &contacts = result.c_contacts_found();
|
||||
auto query = _query;
|
||||
if (requestId) {
|
||||
_navigation->session().data().processUsers(contacts.vusers());
|
||||
_navigation->session().data().processChats(contacts.vchats());
|
||||
_session->data().processUsers(contacts.vusers());
|
||||
_session->data().processChats(contacts.vchats());
|
||||
auto it = _queries.find(requestId);
|
||||
if (it != _queries.cend()) {
|
||||
query = it->second;
|
||||
@@ -221,7 +222,7 @@ void PeerListGlobalSearchController::searchDone(
|
||||
}
|
||||
const auto feedList = [&](const MTPVector<MTPPeer> &list) {
|
||||
for (const auto &mtpPeer : list.v) {
|
||||
const auto peer = _navigation->session().data().peerLoaded(
|
||||
const auto peer = _session->data().peerLoaded(
|
||||
peerFromMTP(mtpPeer));
|
||||
if (peer) {
|
||||
delegate()->peerListSearchAddRow(peer);
|
||||
@@ -246,9 +247,9 @@ ChatsListBoxController::Row::Row(not_null<History*> history)
|
||||
}
|
||||
|
||||
ChatsListBoxController::ChatsListBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation)
|
||||
not_null<Main::Session*> session)
|
||||
: ChatsListBoxController(
|
||||
std::make_unique<PeerListGlobalSearchController>(navigation)) {
|
||||
std::make_unique<PeerListGlobalSearchController>(session)) {
|
||||
}
|
||||
|
||||
ChatsListBoxController::ChatsListBoxController(
|
||||
@@ -354,21 +355,21 @@ bool ChatsListBoxController::appendRow(not_null<History*> history) {
|
||||
}
|
||||
|
||||
ContactsBoxController::ContactsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation)
|
||||
: PeerListController(
|
||||
std::make_unique<PeerListGlobalSearchController>(navigation))
|
||||
, _navigation(navigation) {
|
||||
not_null<Main::Session*> session)
|
||||
: ContactsBoxController(
|
||||
session,
|
||||
std::make_unique<PeerListGlobalSearchController>(session)) {
|
||||
}
|
||||
|
||||
ContactsBoxController::ContactsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
std::unique_ptr<PeerListSearchController> searchController)
|
||||
: PeerListController(std::move(searchController))
|
||||
, _navigation(navigation) {
|
||||
, _session(session) {
|
||||
}
|
||||
|
||||
Main::Session &ContactsBoxController::session() const {
|
||||
return _navigation->session();
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void ContactsBoxController::prepare() {
|
||||
@@ -435,26 +436,24 @@ bool ContactsBoxController::appendRow(not_null<UserData*> user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> ContactsBoxController::createRow(not_null<UserData*> user) {
|
||||
std::unique_ptr<PeerListRow> ContactsBoxController::createRow(
|
||||
not_null<UserData*> user) {
|
||||
return std::make_unique<PeerListRow>(user);
|
||||
}
|
||||
|
||||
void AddBotToGroupBoxController::Start(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<UserData*> bot) {
|
||||
void AddBotToGroupBoxController::Start(not_null<UserData*> bot) {
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->addButton(tr::lng_cancel(), [box] { box->closeBox(); });
|
||||
};
|
||||
Ui::show(Box<PeerListBox>(
|
||||
std::make_unique<AddBotToGroupBoxController>(navigation, bot),
|
||||
std::make_unique<AddBotToGroupBoxController>(bot),
|
||||
std::move(initBox)));
|
||||
}
|
||||
|
||||
AddBotToGroupBoxController::AddBotToGroupBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<UserData*> bot)
|
||||
: ChatsListBoxController(SharingBotGame(bot)
|
||||
? std::make_unique<PeerListGlobalSearchController>(navigation)
|
||||
? std::make_unique<PeerListGlobalSearchController>(&bot->session())
|
||||
: nullptr)
|
||||
, _bot(bot) {
|
||||
}
|
||||
@@ -572,15 +571,15 @@ void AddBotToGroupBoxController::prepareViewHook() {
|
||||
}
|
||||
|
||||
ChooseRecipientBoxController::ChooseRecipientBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
FnMut<void(not_null<PeerData*>)> callback)
|
||||
: ChatsListBoxController(navigation)
|
||||
, _navigation(navigation)
|
||||
: ChatsListBoxController(session)
|
||||
, _session(session)
|
||||
, _callback(std::move(callback)) {
|
||||
}
|
||||
|
||||
Main::Session &ChooseRecipientBoxController::session() const {
|
||||
return _navigation->session();
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void ChooseRecipientBoxController::prepareViewHook() {
|
||||
|
||||
@@ -32,7 +32,6 @@ class History;
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
class SessionNavigation;
|
||||
} // namespace Window
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareContactsBox(
|
||||
@@ -65,8 +64,7 @@ private:
|
||||
|
||||
class PeerListGlobalSearchController : public PeerListSearchController {
|
||||
public:
|
||||
PeerListGlobalSearchController(
|
||||
not_null<Window::SessionNavigation*> navigation);
|
||||
explicit PeerListGlobalSearchController(not_null<Main::Session*> session);
|
||||
|
||||
void searchQuery(const QString &query) override;
|
||||
bool isLoading() override;
|
||||
@@ -79,7 +77,7 @@ private:
|
||||
void searchOnServer();
|
||||
void searchDone(const MTPcontacts_Found &result, mtpRequestId requestId);
|
||||
|
||||
const not_null<Window::SessionNavigation*> _navigation;
|
||||
const not_null<Main::Session*> _session;
|
||||
MTP::Sender _api;
|
||||
base::Timer _timer;
|
||||
QString _query;
|
||||
@@ -104,7 +102,7 @@ public:
|
||||
|
||||
};
|
||||
|
||||
ChatsListBoxController(not_null<Window::SessionNavigation*> navigation);
|
||||
ChatsListBoxController(not_null<Main::Session*> session);
|
||||
ChatsListBoxController(
|
||||
std::unique_ptr<PeerListSearchController> searchController);
|
||||
|
||||
@@ -127,15 +125,15 @@ private:
|
||||
|
||||
class ContactsBoxController : public PeerListController {
|
||||
public:
|
||||
explicit ContactsBoxController(not_null<Main::Session*> session);
|
||||
ContactsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation);
|
||||
ContactsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
std::unique_ptr<PeerListSearchController> searchController);
|
||||
|
||||
Main::Session &session() const override;
|
||||
[[nodiscard]] Main::Session &session() const override;
|
||||
void prepare() override final;
|
||||
std::unique_ptr<PeerListRow> createSearchRow(not_null<PeerData*> peer) override final;
|
||||
[[nodiscard]] std::unique_ptr<PeerListRow> createSearchRow(
|
||||
not_null<PeerData*> peer) override final;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
protected:
|
||||
@@ -150,7 +148,7 @@ private:
|
||||
void checkForEmptyRows();
|
||||
bool appendRow(not_null<UserData*> user);
|
||||
|
||||
const not_null<Window::SessionNavigation*> _navigation;
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
};
|
||||
|
||||
@@ -158,13 +156,9 @@ class AddBotToGroupBoxController
|
||||
: public ChatsListBoxController
|
||||
, public base::has_weak_ptr {
|
||||
public:
|
||||
static void Start(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<UserData*> bot);
|
||||
static void Start(not_null<UserData*> bot);
|
||||
|
||||
AddBotToGroupBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<UserData*> bot);
|
||||
explicit AddBotToGroupBoxController(not_null<UserData*> bot);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
@@ -186,7 +180,7 @@ private:
|
||||
void shareBotGame(not_null<PeerData*> chat);
|
||||
void addBotToGroup(not_null<PeerData*> chat);
|
||||
|
||||
not_null<UserData*> _bot;
|
||||
const not_null<UserData*> _bot;
|
||||
|
||||
};
|
||||
|
||||
@@ -195,7 +189,7 @@ class ChooseRecipientBoxController
|
||||
, public base::has_weak_ptr {
|
||||
public:
|
||||
ChooseRecipientBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<Main::Session*> session,
|
||||
FnMut<void(not_null<PeerData*>)> callback);
|
||||
|
||||
Main::Session &session() const override;
|
||||
@@ -210,7 +204,7 @@ protected:
|
||||
std::unique_ptr<Row> createRow(not_null<History*> history) override;
|
||||
|
||||
private:
|
||||
const not_null<Window::SessionNavigation*> _navigation;
|
||||
const not_null<Main::Session*> _session;
|
||||
FnMut<void(not_null<PeerData*>)> _callback;
|
||||
|
||||
};
|
||||
|
||||
429
Telegram/SourceFiles/boxes/peer_lists_box.cpp
Normal file
429
Telegram/SourceFiles/boxes/peer_lists_box.cpp
Normal file
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
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 "boxes/peer_lists_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/widgets/multi_select.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
PeerListsBox::PeerListsBox(
|
||||
QWidget*,
|
||||
std::vector<std::unique_ptr<PeerListController>> controllers,
|
||||
Fn<void(not_null<PeerListsBox*>)> init)
|
||||
: _lists(makeLists(std::move(controllers)))
|
||||
, _init(std::move(init)) {
|
||||
Expects(!_lists.empty());
|
||||
}
|
||||
|
||||
auto PeerListsBox::collectSelectedRows()
|
||||
-> std::vector<not_null<PeerData*>> {
|
||||
auto result = std::vector<not_null<PeerData*>>();
|
||||
auto items = _select
|
||||
? _select->entity()->getItems()
|
||||
: QVector<uint64>();
|
||||
if (!items.empty()) {
|
||||
result.reserve(items.size());
|
||||
const auto session = &firstController()->session();
|
||||
for (const auto itemId : items) {
|
||||
const auto foreign = [&] {
|
||||
for (const auto &list : _lists) {
|
||||
if (list.controller->isForeignRow(itemId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
if (!foreign) {
|
||||
result.push_back(session->data().peer(itemId));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
PeerListsBox::List PeerListsBox::makeList(
|
||||
std::unique_ptr<PeerListController> controller) {
|
||||
auto delegate = std::make_unique<Delegate>(this, controller.get());
|
||||
return {
|
||||
std::move(controller),
|
||||
std::move(delegate),
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<PeerListsBox::List> PeerListsBox::makeLists(
|
||||
std::vector<std::unique_ptr<PeerListController>> controllers) {
|
||||
auto result = std::vector<List>();
|
||||
result.reserve(controllers.size());
|
||||
for (auto &controller : controllers) {
|
||||
result.push_back(makeList(std::move(controller)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
not_null<PeerListController*> PeerListsBox::firstController() const {
|
||||
return _lists.front().controller.get();
|
||||
}
|
||||
|
||||
void PeerListsBox::createMultiSelect() {
|
||||
Expects(_select == nullptr);
|
||||
|
||||
auto entity = object_ptr<Ui::MultiSelect>(
|
||||
this,
|
||||
(firstController()->selectSt()
|
||||
? *firstController()->selectSt()
|
||||
: st::defaultMultiSelect),
|
||||
tr::lng_participant_filter());
|
||||
_select.create(this, std::move(entity));
|
||||
_select->heightValue(
|
||||
) | rpl::start_with_next(
|
||||
[this] { updateScrollSkips(); },
|
||||
lifetime());
|
||||
_select->entity()->setSubmittedCallback([=](Qt::KeyboardModifiers) {
|
||||
for (const auto &list : _lists) {
|
||||
if (list.content->submitted()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
_select->entity()->setQueryChangedCallback([=](const QString &query) {
|
||||
searchQueryChanged(query);
|
||||
});
|
||||
_select->entity()->setItemRemovedCallback([=](uint64 itemId) {
|
||||
for (const auto &list : _lists) {
|
||||
if (list.controller->handleDeselectForeignRow(itemId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto session = &firstController()->session();
|
||||
if (const auto peer = session->data().peerLoaded(itemId)) {
|
||||
const auto id = peer->id;
|
||||
for (const auto &list : _lists) {
|
||||
if (const auto row = list.delegate->peerListFindRow(id)) {
|
||||
list.content->changeCheckState(
|
||||
row,
|
||||
false,
|
||||
anim::type::normal);
|
||||
update();
|
||||
}
|
||||
list.controller->itemDeselectedHook(peer);
|
||||
}
|
||||
}
|
||||
});
|
||||
_select->resizeToWidth(firstController()->contentWidth());
|
||||
_select->moveToLeft(0, 0);
|
||||
}
|
||||
|
||||
int PeerListsBox::getTopScrollSkip() const {
|
||||
auto result = 0;
|
||||
if (_select && !_select->isHidden()) {
|
||||
result += _select->height();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PeerListsBox::updateScrollSkips() {
|
||||
// If we show / hide the search field scroll top is fixed.
|
||||
// If we resize search field by bubbles scroll bottom is fixed.
|
||||
setInnerTopSkip(getTopScrollSkip(), _scrollBottomFixed);
|
||||
if (!_select->animating()) {
|
||||
_scrollBottomFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::prepare() {
|
||||
auto rows = setInnerWidget(
|
||||
object_ptr<Ui::VerticalLayout>(this),
|
||||
st::boxScroll);
|
||||
for (auto &list : _lists) {
|
||||
const auto content = rows->add(object_ptr<PeerListContent>(
|
||||
rows,
|
||||
list.controller.get()));
|
||||
list.content = content;
|
||||
list.delegate->setContent(content);
|
||||
list.controller->setDelegate(list.delegate.get());
|
||||
|
||||
content->scrollToRequests(
|
||||
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
|
||||
const auto skip = content->y();
|
||||
onScrollToY(
|
||||
skip + request.ymin,
|
||||
(request.ymax >= 0) ? (skip + request.ymax) : request.ymax);
|
||||
}, lifetime());
|
||||
|
||||
content->selectedIndexValue(
|
||||
) | rpl::filter([=](int index) {
|
||||
return (index >= 0);
|
||||
}) | rpl::start_with_next([=] {
|
||||
for (const auto &list : _lists) {
|
||||
if (list.content && list.content != content) {
|
||||
list.content->clearSelection();
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
rows->resizeToWidth(firstController()->contentWidth());
|
||||
|
||||
setDimensions(firstController()->contentWidth(), st::boxMaxListHeight);
|
||||
if (_select) {
|
||||
_select->finishAnimating();
|
||||
Ui::SendPendingMoveResizeEvents(_select);
|
||||
_scrollBottomFixed = true;
|
||||
onScrollToY(0);
|
||||
}
|
||||
|
||||
if (_init) {
|
||||
_init(this);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::keyPressEvent(QKeyEvent *e) {
|
||||
const auto skipRows = [&](int rows) {
|
||||
if (rows == 0) {
|
||||
return;
|
||||
}
|
||||
for (const auto &list : _lists) {
|
||||
if (list.content->hasPressed()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto from = begin(_lists), till = end(_lists);
|
||||
auto i = from;
|
||||
for (; i != till; ++i) {
|
||||
if (i->content->hasSelection()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == till && rows < 0) {
|
||||
return;
|
||||
}
|
||||
if (rows > 0) {
|
||||
if (i == till) {
|
||||
i = from;
|
||||
}
|
||||
for (; i != till; ++i) {
|
||||
const auto result = i->content->selectSkip(rows);
|
||||
if (result.shouldMoveTo - result.reallyMovedTo >= rows) {
|
||||
continue;
|
||||
} else if (result.reallyMovedTo >= result.shouldMoveTo) {
|
||||
return;
|
||||
} else {
|
||||
rows = result.shouldMoveTo - result.reallyMovedTo;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (++i; i != from;) {
|
||||
const auto result = (--i)->content->selectSkip(rows);
|
||||
if (result.shouldMoveTo - result.reallyMovedTo <= rows) {
|
||||
continue;
|
||||
} else if (result.reallyMovedTo <= result.shouldMoveTo) {
|
||||
return;
|
||||
} else {
|
||||
rows = result.shouldMoveTo - result.reallyMovedTo;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto rowsInPage = [&] {
|
||||
const auto rowHeight = firstController()->computeListSt().item.height;
|
||||
return height() / rowHeight;
|
||||
};
|
||||
if (e->key() == Qt::Key_Down) {
|
||||
skipRows(1);
|
||||
} else if (e->key() == Qt::Key_Up) {
|
||||
skipRows(-1);
|
||||
} else if (e->key() == Qt::Key_PageDown) {
|
||||
skipRows(rowsInPage());
|
||||
} else if (e->key() == Qt::Key_PageUp) {
|
||||
skipRows(-rowsInPage());
|
||||
} else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) {
|
||||
_select->entity()->clearQuery();
|
||||
} else {
|
||||
BoxContent::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::searchQueryChanged(const QString &query) {
|
||||
onScrollToY(0);
|
||||
for (const auto &list : _lists) {
|
||||
list.content->searchQueryChanged(query);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
if (_select) {
|
||||
_select->resizeToWidth(width());
|
||||
_select->moveToLeft(0, 0);
|
||||
|
||||
updateScrollSkips();
|
||||
}
|
||||
|
||||
for (const auto &list : _lists) {
|
||||
list.content->resizeToWidth(width());
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
const auto &bg = (firstController()->listSt()
|
||||
? *firstController()->listSt()
|
||||
: st::peerListBox).bg;
|
||||
for (const auto rect : e->region()) {
|
||||
p.fillRect(rect, bg);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::setInnerFocus() {
|
||||
if (!_select || !_select->toggled()) {
|
||||
_lists.front().content->setFocus();
|
||||
} else {
|
||||
_select->entity()->setInnerFocus();
|
||||
}
|
||||
}
|
||||
|
||||
PeerListsBox::Delegate::Delegate(
|
||||
not_null<PeerListsBox*> box,
|
||||
not_null<PeerListController*> controller)
|
||||
: _box(box)
|
||||
, _controller(controller) {
|
||||
}
|
||||
|
||||
void PeerListsBox::Delegate::peerListSetTitle(rpl::producer<QString> title) {
|
||||
}
|
||||
|
||||
void PeerListsBox::Delegate::peerListSetAdditionalTitle(
|
||||
rpl::producer<QString> title) {
|
||||
}
|
||||
|
||||
void PeerListsBox::Delegate::peerListSetRowChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool checked) {
|
||||
if (checked) {
|
||||
_box->addSelectItem(row, anim::type::normal);
|
||||
PeerListContentDelegate::peerListSetRowChecked(row, checked);
|
||||
peerListUpdateRow(row);
|
||||
|
||||
// This call deletes row from _searchRows.
|
||||
_box->_select->entity()->clearQuery();
|
||||
} else {
|
||||
// The itemRemovedCallback will call changeCheckState() here.
|
||||
_box->_select->entity()->removeItem(row->id());
|
||||
peerListUpdateRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::Delegate::peerListSetForeignRowChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool checked,
|
||||
anim::type animated) {
|
||||
if (checked) {
|
||||
_box->addSelectItem(row, animated);
|
||||
|
||||
// This call deletes row from _searchRows.
|
||||
_box->_select->entity()->clearQuery();
|
||||
} else {
|
||||
// The itemRemovedCallback will call changeCheckState() here.
|
||||
_box->_select->entity()->removeItem(row->id());
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::Delegate::peerListScrollToTop() {
|
||||
_box->onScrollToY(0);
|
||||
}
|
||||
|
||||
void PeerListsBox::Delegate::peerListSetSearchMode(PeerListSearchMode mode) {
|
||||
PeerListContentDelegate::peerListSetSearchMode(mode);
|
||||
_box->setSearchMode(mode);
|
||||
}
|
||||
|
||||
void PeerListsBox::setSearchMode(PeerListSearchMode mode) {
|
||||
auto selectVisible = (mode != PeerListSearchMode::Disabled);
|
||||
if (selectVisible && !_select) {
|
||||
createMultiSelect();
|
||||
_select->toggle(!selectVisible, anim::type::instant);
|
||||
}
|
||||
if (_select) {
|
||||
_select->toggle(selectVisible, anim::type::normal);
|
||||
_scrollBottomFixed = false;
|
||||
setInnerFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListsBox::Delegate::peerListFinishSelectedRowsBunch() {
|
||||
Expects(_box->_select != nullptr);
|
||||
|
||||
_box->_select->entity()->finishItemsBunch();
|
||||
}
|
||||
|
||||
bool PeerListsBox::Delegate::peerListIsRowChecked(
|
||||
not_null<PeerListRow*> row) {
|
||||
return _box->_select
|
||||
? _box->_select->entity()->hasItem(row->id())
|
||||
: false;
|
||||
}
|
||||
|
||||
int PeerListsBox::Delegate::peerListSelectedRowsCount() {
|
||||
return _box->_select ? _box->_select->entity()->getItemsCount() : 0;
|
||||
}
|
||||
|
||||
void PeerListsBox::addSelectItem(
|
||||
not_null<PeerData*> peer,
|
||||
anim::type animated) {
|
||||
addSelectItem(
|
||||
peer->id,
|
||||
peer->shortName(),
|
||||
PaintUserpicCallback(peer, false),
|
||||
animated);
|
||||
}
|
||||
|
||||
void PeerListsBox::addSelectItem(
|
||||
not_null<PeerListRow*> row,
|
||||
anim::type animated) {
|
||||
addSelectItem(
|
||||
row->id(),
|
||||
row->generateShortName(),
|
||||
row->generatePaintUserpicCallback(),
|
||||
animated);
|
||||
}
|
||||
|
||||
void PeerListsBox::addSelectItem(
|
||||
uint64 itemId,
|
||||
const QString &text,
|
||||
Ui::MultiSelect::PaintRoundImage paintUserpic,
|
||||
anim::type animated) {
|
||||
if (!_select) {
|
||||
createMultiSelect();
|
||||
_select->hide(anim::type::instant);
|
||||
}
|
||||
const auto &activeBg = (firstController()->selectSt()
|
||||
? *firstController()->selectSt()
|
||||
: st::defaultMultiSelect).item.textActiveBg;
|
||||
if (animated == anim::type::instant) {
|
||||
_select->entity()->addItemInBunch(
|
||||
itemId,
|
||||
text,
|
||||
activeBg,
|
||||
std::move(paintUserpic));
|
||||
} else {
|
||||
_select->entity()->addItem(
|
||||
itemId,
|
||||
text,
|
||||
activeBg,
|
||||
std::move(paintUserpic));
|
||||
}
|
||||
}
|
||||
101
Telegram/SourceFiles/boxes/peer_lists_box.h
Normal file
101
Telegram/SourceFiles/boxes/peer_lists_box.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
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 "boxes/peer_list_box.h"
|
||||
|
||||
class PeerListsBox : public Ui::BoxContent {
|
||||
public:
|
||||
PeerListsBox(
|
||||
QWidget*,
|
||||
std::vector<std::unique_ptr<PeerListController>> controllers,
|
||||
Fn<void(not_null<PeerListsBox*>)> init);
|
||||
|
||||
[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
class Delegate final : public PeerListContentDelegate {
|
||||
public:
|
||||
Delegate(
|
||||
not_null<PeerListsBox*> box,
|
||||
not_null<PeerListController*> controller);
|
||||
|
||||
void peerListSetTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetSearchMode(PeerListSearchMode mode) override;
|
||||
void peerListSetRowChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool checked) override;
|
||||
void peerListSetForeignRowChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool checked,
|
||||
anim::type animated) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
void peerListScrollToTop() override;
|
||||
|
||||
void peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) override {
|
||||
_box->addSelectItem(peer, anim::type::instant);
|
||||
}
|
||||
void peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override {
|
||||
_box->addSelectItem(row, anim::type::instant);
|
||||
}
|
||||
void peerListFinishSelectedRowsBunch() override;
|
||||
|
||||
private:
|
||||
const not_null<PeerListsBox*> _box;
|
||||
const not_null<PeerListController*> _controller;
|
||||
|
||||
};
|
||||
struct List {
|
||||
std::unique_ptr<PeerListController> controller;
|
||||
std::unique_ptr<Delegate> delegate;
|
||||
PeerListContent *content = nullptr;
|
||||
};
|
||||
|
||||
friend class Delegate;
|
||||
|
||||
[[nodiscard]] List makeList(
|
||||
std::unique_ptr<PeerListController> controller);
|
||||
[[nodiscard]] std::vector<List> makeLists(
|
||||
std::vector<std::unique_ptr<PeerListController>> controllers);
|
||||
|
||||
[[nodiscard]] not_null<PeerListController*> firstController() const;
|
||||
|
||||
void addSelectItem(
|
||||
not_null<PeerData*> peer,
|
||||
anim::type animated);
|
||||
void addSelectItem(
|
||||
not_null<PeerListRow*> row,
|
||||
anim::type animated);
|
||||
void addSelectItem(
|
||||
uint64 itemId,
|
||||
const QString &text,
|
||||
PaintRoundImageCallback paintUserpic,
|
||||
anim::type animated);
|
||||
void setSearchMode(PeerListSearchMode mode);
|
||||
void createMultiSelect();
|
||||
int getTopScrollSkip() const;
|
||||
void updateScrollSkips();
|
||||
void searchQueryChanged(const QString &query);
|
||||
|
||||
object_ptr<Ui::SlideWrap<Ui::MultiSelect>> _select = { nullptr };
|
||||
|
||||
std::vector<List> _lists;
|
||||
Fn<void(PeerListsBox*)> _init;
|
||||
bool _scrollBottomFixed = false;
|
||||
|
||||
};
|
||||
@@ -51,28 +51,21 @@ base::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {
|
||||
} // namespace
|
||||
|
||||
AddParticipantsBoxController::AddParticipantsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation)
|
||||
: ContactsBoxController(
|
||||
navigation,
|
||||
std::make_unique<PeerListGlobalSearchController>(navigation)) {
|
||||
not_null<Main::Session*> session)
|
||||
: ContactsBoxController(session) {
|
||||
}
|
||||
|
||||
AddParticipantsBoxController::AddParticipantsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer)
|
||||
: AddParticipantsBoxController(
|
||||
navigation,
|
||||
peer,
|
||||
GetAlreadyInFromPeer(peer)) {
|
||||
}
|
||||
|
||||
AddParticipantsBoxController::AddParticipantsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> &&alreadyIn)
|
||||
: ContactsBoxController(
|
||||
navigation,
|
||||
std::make_unique<PeerListGlobalSearchController>(navigation))
|
||||
: ContactsBoxController(&peer->session())
|
||||
, _peer(peer)
|
||||
, _alreadyIn(std::move(alreadyIn)) {
|
||||
subscribeToMigration();
|
||||
@@ -179,7 +172,7 @@ bool AddParticipantsBoxController::inviteSelectedUsers(
|
||||
not_null<PeerListBox*> box) const {
|
||||
Expects(_peer != nullptr);
|
||||
|
||||
const auto rows = box->peerListCollectSelectedRows();
|
||||
const auto rows = box->collectSelectedRows();
|
||||
const auto users = ranges::view::all(
|
||||
rows
|
||||
) | ranges::view::transform([](not_null<PeerData*> peer) {
|
||||
@@ -198,9 +191,7 @@ bool AddParticipantsBoxController::inviteSelectedUsers(
|
||||
void AddParticipantsBoxController::Start(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<ChatData*> chat) {
|
||||
auto controller = std::make_unique<AddParticipantsBoxController>(
|
||||
navigation,
|
||||
chat);
|
||||
auto controller = std::make_unique<AddParticipantsBoxController>(chat);
|
||||
const auto weak = controller.get();
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->addButton(tr::lng_participant_invite(), [=] {
|
||||
@@ -223,7 +214,6 @@ void AddParticipantsBoxController::Start(
|
||||
base::flat_set<not_null<UserData*>> &&alreadyIn,
|
||||
bool justCreated) {
|
||||
auto controller = std::make_unique<AddParticipantsBoxController>(
|
||||
navigation,
|
||||
channel,
|
||||
std::move(alreadyIn));
|
||||
const auto weak = controller.get();
|
||||
|
||||
@@ -27,16 +27,16 @@ public:
|
||||
not_null<ChannelData*> channel,
|
||||
base::flat_set<not_null<UserData*>> &&alreadyIn);
|
||||
|
||||
explicit AddParticipantsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation);
|
||||
explicit AddParticipantsBoxController(not_null<Main::Session*> session);
|
||||
explicit AddParticipantsBoxController(not_null<PeerData*> peer);
|
||||
AddParticipantsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer);
|
||||
AddParticipantsBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> &&alreadyIn);
|
||||
|
||||
[[nodiscard]] not_null<PeerData*> peer() const {
|
||||
return _peer;
|
||||
}
|
||||
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
void itemDeselectedHook(not_null<PeerData*> peer) override;
|
||||
|
||||
|
||||
@@ -1369,9 +1369,12 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto added = false;
|
||||
_additional.fillFromPeer();
|
||||
for (const auto user : info->lastParticipants) {
|
||||
appendRow(user);
|
||||
if (appendRow(user)) {
|
||||
added = true;
|
||||
}
|
||||
|
||||
//
|
||||
// Don't count lastParticipants in _offset, because we don't know
|
||||
@@ -1383,7 +1386,7 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() {
|
||||
if (_onlineSorter) {
|
||||
_onlineSorter->sort();
|
||||
}
|
||||
return true;
|
||||
return added;
|
||||
}
|
||||
|
||||
void ParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {
|
||||
|
||||
@@ -292,7 +292,7 @@ void SessionsContent::terminateAll() {
|
||||
auto callback = [=] {
|
||||
const auto reset = crl::guard(weak, [=] {
|
||||
_authorizations->cancelCurrentRequest();
|
||||
shortPollSessions();
|
||||
_authorizations->reload();
|
||||
});
|
||||
_authorizations->requestTerminate(
|
||||
[=](const MTPBool &result) { reset(); },
|
||||
|
||||
@@ -9,6 +9,7 @@ using "ui/basic.style";
|
||||
|
||||
using "ui/widgets/widgets.style";
|
||||
using "ui/layers/layers.style";
|
||||
using "ui/chat/chat.style"; // GroupCallUserpics
|
||||
using "window/window.style";
|
||||
|
||||
CallSignalBars {
|
||||
@@ -465,8 +466,8 @@ groupCallMembersListItem: PeerListItem(defaultPeerListItem) {
|
||||
}
|
||||
height: 52px;
|
||||
photoPosition: point(12px, 6px);
|
||||
namePosition: point(68px, 7px);
|
||||
statusPosition: point(68px, 26px);
|
||||
namePosition: point(63px, 7px);
|
||||
statusPosition: point(63px, 26px);
|
||||
photoSize: 40px;
|
||||
nameFg: groupCallMembersFg;
|
||||
nameFgChecked: groupCallMembersFg;
|
||||
@@ -477,11 +478,17 @@ groupCallMembersListItem: PeerListItem(defaultPeerListItem) {
|
||||
groupCallMembersList: PeerList(defaultPeerList) {
|
||||
bg: groupCallMembersBg;
|
||||
about: FlatLabel(defaultPeerListAbout) {
|
||||
textFg: groupCallMemberInactiveStatus;
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
}
|
||||
item: groupCallMembersListItem;
|
||||
}
|
||||
groupCallInviteDividerLabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
}
|
||||
groupCallInviteDividerPadding: margins(17px, 7px, 17px, 7px);
|
||||
|
||||
groupCallInviteMembersList: PeerList(groupCallMembersList) {
|
||||
padding: margins(0px, 10px, 0px, 10px);
|
||||
item: PeerListItem(groupCallMembersListItem) {
|
||||
statusFg: groupCallMemberNotJoinedStatus;
|
||||
statusFgOver: groupCallMemberNotJoinedStatus;
|
||||
@@ -512,26 +519,36 @@ groupCallMultiSelect: MultiSelect(defaultMultiSelect) {
|
||||
}
|
||||
}
|
||||
|
||||
groupCallMembersHeader: 47px;
|
||||
groupCallMembersTop: 62px;
|
||||
groupCallTitleTop: 14px;
|
||||
groupCallSubtitleTop: 33px;
|
||||
|
||||
groupCallMembersMargin: margins(16px, 16px, 16px, 28px);
|
||||
groupCallAddMember: IconButton(defaultIconButton) {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
iconPosition: point(3px, 5px);
|
||||
icon: icon {{ "info_add_member", groupCallMemberInactiveIcon }};
|
||||
iconOver: icon {{ "info_add_member", groupCallMemberInactiveIcon }};
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 36px;
|
||||
groupCallAddMember: SettingsButton(defaultSettingsButton) {
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
textFgOver: groupCallMemberNotJoinedStatus;
|
||||
textBg: groupCallMembersBg;
|
||||
textBgOver: groupCallMembersBgOver;
|
||||
|
||||
font: semiboldFont;
|
||||
|
||||
height: 22px;
|
||||
padding: margins(63px, 17px, 22px, 11px);
|
||||
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
groupCallHeaderPosition: point(16px, 16px);
|
||||
groupCallHeaderLabel: FlatLabel(defaultFlatLabel) {
|
||||
groupCallAddMemberIcon: icon {{ "info_add_member", groupCallMemberInactiveIcon, point(0px, 3px) }};
|
||||
groupCallAddMemberIconLeft: 16px;
|
||||
groupCallSubtitleLabel: FlatLabel(defaultFlatLabel) {
|
||||
maxHeight: 18px;
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
}
|
||||
groupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) {
|
||||
textFg: groupCallMembersFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: semiboldFont;
|
||||
linkFont: semiboldFont;
|
||||
linkFontOver: semiboldFont;
|
||||
font: font(semibold 14px);
|
||||
linkFont: font(semibold 14px);
|
||||
linkFontOver: font(semibold 14px);
|
||||
}
|
||||
}
|
||||
groupCallAddButtonPosition: point(10px, 7px);
|
||||
@@ -562,6 +579,8 @@ groupCallMemberColoredCrossLine: CrossLineAnimation(groupCallMemberInactiveCross
|
||||
fg: groupCallMemberMutedIcon;
|
||||
icon: icon {{ "calls/group_calls_unmuted", groupCallMemberActiveIcon }};
|
||||
}
|
||||
groupCallMemberInvited: icon {{ "calls/group_calls_invited", groupCallMemberInactiveIcon }};
|
||||
groupCallMemberInvitedPosition: point(2px, 12px);
|
||||
|
||||
groupCallSettings: CallButton(callMicrophoneMute) {
|
||||
button: IconButton(callButton) {
|
||||
@@ -584,12 +603,15 @@ groupCallHangup: CallButton(callHangup) {
|
||||
label: callButtonLabel;
|
||||
}
|
||||
groupCallButtonSkip: 43px;
|
||||
groupCallButtonBottomSkip: 134px;
|
||||
groupCallMuteBottomSkip: 149px;
|
||||
groupCallButtonBottomSkip: 145px;
|
||||
groupCallMuteBottomSkip: 160px;
|
||||
|
||||
groupCallTopBarUserpicSize: 28px;
|
||||
groupCallTopBarUserpicShift: 8px;
|
||||
groupCallTopBarUserpicStroke: 2px;
|
||||
groupCallTopBarUserpics: GroupCallUserpics {
|
||||
size: 28px;
|
||||
shift: 8px;
|
||||
stroke: 2px;
|
||||
align: align(left);
|
||||
}
|
||||
groupCallTopBarJoin: RoundButton(defaultActiveButton) {
|
||||
width: -26px;
|
||||
height: 26px;
|
||||
@@ -722,8 +744,8 @@ groupCallTitleCloseIconOver: icon {
|
||||
};
|
||||
groupCallTitle: WindowTitle(defaultWindowTitle) {
|
||||
height: 0px;
|
||||
bg: groupCallBg;
|
||||
bgActive: groupCallBg;
|
||||
bg: transparent;
|
||||
bgActive: transparent;
|
||||
fg: transparent;
|
||||
fgActive: transparent;
|
||||
minimize: IconButton(groupCallTitleButton) {
|
||||
|
||||
@@ -368,10 +368,10 @@ void Call::setupOutgoingVideo() {
|
||||
_errors.fire({ ErrorType::NoCamera });
|
||||
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
|
||||
} else if (_state.current() != State::Established
|
||||
&& state != started
|
||||
&& !_videoCapture) {
|
||||
&& (state != Webrtc::VideoState::Inactive)
|
||||
&& (started == Webrtc::VideoState::Inactive)) {
|
||||
_errors.fire({ ErrorType::NotStartedCall });
|
||||
_videoOutgoing->setState(started);
|
||||
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
|
||||
} else if (state != Webrtc::VideoState::Inactive
|
||||
&& _instance
|
||||
&& !_instance->supportsVideo()) {
|
||||
@@ -951,20 +951,20 @@ void Call::setState(State state) {
|
||||
_startTime = crl::now();
|
||||
break;
|
||||
case State::ExchangingKeys:
|
||||
_delegate->playSound(Delegate::Sound::Connecting);
|
||||
_delegate->callPlaySound(Delegate::CallSound::Connecting);
|
||||
break;
|
||||
case State::Ended:
|
||||
_delegate->playSound(Delegate::Sound::Ended);
|
||||
_delegate->callPlaySound(Delegate::CallSound::Ended);
|
||||
[[fallthrough]];
|
||||
case State::EndedByOtherDevice:
|
||||
_delegate->callFinished(this);
|
||||
break;
|
||||
case State::Failed:
|
||||
_delegate->playSound(Delegate::Sound::Ended);
|
||||
_delegate->callPlaySound(Delegate::CallSound::Ended);
|
||||
_delegate->callFailed(this);
|
||||
break;
|
||||
case State::Busy:
|
||||
_delegate->playSound(Delegate::Sound::Busy);
|
||||
_delegate->callPlaySound(Delegate::CallSound::Busy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +62,16 @@ public:
|
||||
virtual void callFailed(not_null<Call*> call) = 0;
|
||||
virtual void callRedial(not_null<Call*> call) = 0;
|
||||
|
||||
enum class Sound {
|
||||
enum class CallSound {
|
||||
Connecting,
|
||||
Busy,
|
||||
Ended,
|
||||
};
|
||||
virtual void playSound(Sound sound) = 0;
|
||||
virtual void callPlaySound(CallSound sound) = 0;
|
||||
virtual void callRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess,
|
||||
bool video) = 0;
|
||||
|
||||
virtual auto getVideoCapture()
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_session.h"
|
||||
#include "base/global_shortcuts.h"
|
||||
#include "webrtc/webrtc_media_devices.h"
|
||||
|
||||
#include <tgcalls/group/GroupInstanceImpl.h>
|
||||
|
||||
@@ -42,6 +43,15 @@ constexpr auto kMaxInvitePerSlice = 10;
|
||||
constexpr auto kCheckLastSpokeInterval = crl::time(1000);
|
||||
constexpr auto kCheckJoinedTimeout = 4 * crl::time(1000);
|
||||
constexpr auto kUpdateSendActionEach = crl::time(500);
|
||||
constexpr auto kPlayConnectingEach = crl::time(1056) + 2 * crl::time(1000);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Webrtc::MediaDevices> CreateMediaDevices() {
|
||||
const auto &settings = Core::App().settings();
|
||||
return Webrtc::CreateMediaDevices(
|
||||
settings.callInputDeviceId(),
|
||||
settings.callOutputDeviceId(),
|
||||
settings.callVideoInputDeviceId());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -55,7 +65,9 @@ GroupCall::GroupCall(
|
||||
, _api(&peer->session().mtp())
|
||||
, _lastSpokeCheckTimer([=] { checkLastSpoke(); })
|
||||
, _checkJoinedTimer([=] { checkJoined(); })
|
||||
, _pushToTalkCancelTimer([=] { pushToTalkCancel(); }) {
|
||||
, _pushToTalkCancelTimer([=] { pushToTalkCancel(); })
|
||||
, _connectingSoundTimer([=] { playConnectingSoundOnce(); })
|
||||
, _mediaDevices(CreateMediaDevices()) {
|
||||
_muted.value(
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::start_with_next([=](MuteState previous, MuteState state) {
|
||||
@@ -81,6 +93,22 @@ GroupCall::GroupCall(
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
|
||||
_mediaDevices->audioInputId(
|
||||
) | rpl::start_with_next([=](QString id) {
|
||||
_audioInputId = id;
|
||||
if (_instance) {
|
||||
_instance->setAudioInputDevice(id.toStdString());
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
_mediaDevices->audioOutputId(
|
||||
) | rpl::start_with_next([=](QString id) {
|
||||
_audioOutputId = id;
|
||||
if (_instance) {
|
||||
_instance->setAudioOutputDevice(id.toStdString());
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
GroupCall::~GroupCall() {
|
||||
@@ -109,14 +137,22 @@ void GroupCall::setState(State state) {
|
||||
}
|
||||
_state = state;
|
||||
|
||||
if (_state.current() == State::Joined) {
|
||||
if (!_pushToTalkStarted) {
|
||||
_pushToTalkStarted = true;
|
||||
if (state == State::Joined) {
|
||||
stopConnectingSound();
|
||||
if (!_hadJoinedState) {
|
||||
_hadJoinedState = true;
|
||||
applyGlobalShortcutChanges();
|
||||
_delegate->groupCallPlaySound(Delegate::GroupCallSound::Started);
|
||||
}
|
||||
if (const auto call = _peer->groupCall(); call && call->id() == _id) {
|
||||
call->setInCall();
|
||||
}
|
||||
} else if (state == State::Connecting || state == State::Joining) {
|
||||
if (_hadJoinedState) {
|
||||
playConnectingSound();
|
||||
}
|
||||
} else {
|
||||
stopConnectingSound();
|
||||
}
|
||||
|
||||
if (false
|
||||
@@ -127,6 +163,10 @@ void GroupCall::setState(State state) {
|
||||
destroyController();
|
||||
}
|
||||
switch (state) {
|
||||
case State::HangingUp:
|
||||
case State::FailedHangingUp:
|
||||
_delegate->groupCallPlaySound(Delegate::GroupCallSound::Ended);
|
||||
break;
|
||||
case State::Ended:
|
||||
_delegate->groupCallFinished(this);
|
||||
break;
|
||||
@@ -141,6 +181,22 @@ void GroupCall::setState(State state) {
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::playConnectingSound() {
|
||||
if (_connectingSoundTimer.isActive()) {
|
||||
return;
|
||||
}
|
||||
playConnectingSoundOnce();
|
||||
_connectingSoundTimer.callEach(kPlayConnectingEach);
|
||||
}
|
||||
|
||||
void GroupCall::stopConnectingSound() {
|
||||
_connectingSoundTimer.cancel();
|
||||
}
|
||||
|
||||
void GroupCall::playConnectingSoundOnce() {
|
||||
_delegate->groupCallPlaySound(Delegate::GroupCallSound::Connecting);
|
||||
}
|
||||
|
||||
void GroupCall::start() {
|
||||
_createRequestId = _api.request(MTPphone_CreateGroupCall(
|
||||
_peer->input,
|
||||
@@ -498,31 +554,33 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) {
|
||||
}
|
||||
|
||||
void GroupCall::createAndStartController() {
|
||||
using AudioLevels = std::vector<std::pair<uint32_t, float>>;
|
||||
|
||||
const auto &settings = Core::App().settings();
|
||||
|
||||
const auto weak = base::make_weak(this);
|
||||
const auto myLevel = std::make_shared<float>();
|
||||
const auto myLevel = std::make_shared<tgcalls::GroupLevelValue>();
|
||||
tgcalls::GroupInstanceDescriptor descriptor = {
|
||||
.config = tgcalls::GroupConfig{
|
||||
},
|
||||
.networkStateUpdated = [=](bool connected) {
|
||||
crl::on_main(weak, [=] { setInstanceConnected(connected); });
|
||||
},
|
||||
.audioLevelsUpdated = [=](const AudioLevels &data) {
|
||||
if (!data.empty()) {
|
||||
crl::on_main(weak, [=] { audioLevelsUpdated(data); });
|
||||
.audioLevelsUpdated = [=](const tgcalls::GroupLevelsUpdate &data) {
|
||||
const auto &updates = data.updates;
|
||||
if (updates.empty()) {
|
||||
return;
|
||||
} else if (updates.size() == 1 && !updates.front().ssrc) {
|
||||
const auto &value = updates.front().value;
|
||||
// Don't send many 0 while we're muted.
|
||||
if (myLevel->level == value.level
|
||||
&& myLevel->voice == value.voice) {
|
||||
return;
|
||||
}
|
||||
*myLevel = updates.front().value;
|
||||
}
|
||||
crl::on_main(weak, [=] { audioLevelsUpdated(data); });
|
||||
},
|
||||
.myAudioLevelUpdated = [=](float level) {
|
||||
if (*myLevel != level) { // Don't send many 0 while we're muted.
|
||||
*myLevel = level;
|
||||
crl::on_main(weak, [=] { myLevelUpdated(level); });
|
||||
}
|
||||
},
|
||||
.initialInputDeviceId = settings.callInputDeviceId().toStdString(),
|
||||
.initialOutputDeviceId = settings.callOutputDeviceId().toStdString(),
|
||||
.initialInputDeviceId = _audioInputId.toStdString(),
|
||||
.initialOutputDeviceId = _audioOutputId.toStdString(),
|
||||
};
|
||||
if (Logs::DebugEnabled()) {
|
||||
auto callLogFolder = cWorkingDir() + qsl("DebugLogs");
|
||||
@@ -542,6 +600,7 @@ void GroupCall::createAndStartController() {
|
||||
LOG(("Call Info: Creating group instance"));
|
||||
_instance = std::make_unique<tgcalls::GroupInstanceImpl>(
|
||||
std::move(descriptor));
|
||||
|
||||
updateInstanceMuteState();
|
||||
|
||||
//raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());
|
||||
@@ -555,24 +614,28 @@ void GroupCall::updateInstanceMuteState() {
|
||||
&& state != MuteState::PushToTalk);
|
||||
}
|
||||
|
||||
void GroupCall::handleLevelsUpdated(
|
||||
gsl::span<const std::pair<std::uint32_t, float>> data) {
|
||||
Expects(!data.empty());
|
||||
void GroupCall::audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data) {
|
||||
Expects(!data.updates.empty());
|
||||
|
||||
auto check = false;
|
||||
auto checkNow = false;
|
||||
const auto now = crl::now();
|
||||
for (const auto &[ssrc, level] : data) {
|
||||
for (const auto &[ssrcOrZero, value] : data.updates) {
|
||||
const auto ssrc = ssrcOrZero ? ssrcOrZero : _mySsrc;
|
||||
const auto level = value.level;
|
||||
const auto voice = value.voice;
|
||||
const auto self = (ssrc == _mySsrc);
|
||||
_levelUpdates.fire(LevelUpdate{
|
||||
.ssrc = ssrc,
|
||||
.value = level,
|
||||
.voice = voice,
|
||||
.self = self
|
||||
});
|
||||
if (level <= kSpeakLevelThreshold) {
|
||||
continue;
|
||||
}
|
||||
if (self
|
||||
&& voice
|
||||
&& (!_lastSendProgressUpdate
|
||||
|| _lastSendProgressUpdate + kUpdateSendActionEach < now)) {
|
||||
_lastSendProgressUpdate = now;
|
||||
@@ -584,13 +647,21 @@ void GroupCall::handleLevelsUpdated(
|
||||
check = true;
|
||||
const auto i = _lastSpoke.find(ssrc);
|
||||
if (i == _lastSpoke.end()) {
|
||||
_lastSpoke.emplace(ssrc, now);
|
||||
_lastSpoke.emplace(ssrc, Data::LastSpokeTimes{
|
||||
.anything = now,
|
||||
.voice = voice ? now : 0,
|
||||
});
|
||||
checkNow = true;
|
||||
} else {
|
||||
if (i->second + kCheckLastSpokeInterval / 3 <= now) {
|
||||
if ((i->second.anything + kCheckLastSpokeInterval / 3 <= now)
|
||||
|| (voice
|
||||
&& i->second.voice + kCheckLastSpokeInterval / 3 <= now)) {
|
||||
checkNow = true;
|
||||
}
|
||||
i->second = now;
|
||||
i->second.anything = now;
|
||||
if (voice) {
|
||||
i->second.voice = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checkNow) {
|
||||
@@ -600,16 +671,6 @@ void GroupCall::handleLevelsUpdated(
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::myLevelUpdated(float level) {
|
||||
const auto pair = std::pair<std::uint32_t, float>{ _mySsrc, level };
|
||||
handleLevelsUpdated({ &pair, &pair + 1 });
|
||||
}
|
||||
|
||||
void GroupCall::audioLevelsUpdated(
|
||||
const std::vector<std::pair<std::uint32_t, float>> &data) {
|
||||
handleLevelsUpdated(gsl::make_span(data));
|
||||
}
|
||||
|
||||
void GroupCall::checkLastSpoke() {
|
||||
const auto real = _peer->groupCall();
|
||||
if (!real || real->id() != _id) {
|
||||
@@ -621,7 +682,7 @@ void GroupCall::checkLastSpoke() {
|
||||
auto list = base::take(_lastSpoke);
|
||||
for (auto i = list.begin(); i != list.end();) {
|
||||
const auto [ssrc, when] = *i;
|
||||
if (when + kCheckLastSpokeInterval >= now) {
|
||||
if (when.anything + kCheckLastSpokeInterval >= now) {
|
||||
hasRecent = true;
|
||||
++i;
|
||||
} else {
|
||||
@@ -705,14 +766,20 @@ void GroupCall::sendMutedUpdate() {
|
||||
}).send();
|
||||
}
|
||||
|
||||
rpl::producer<bool> GroupCall::connectingValue() const {
|
||||
using namespace rpl::mappers;
|
||||
return _state.value() | rpl::map(
|
||||
_1 == State::Creating
|
||||
|| _1 == State::Joining
|
||||
|| _1 == State::Connecting
|
||||
) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) {
|
||||
if (_instance) {
|
||||
const auto id = deviceId.toStdString();
|
||||
if (input) {
|
||||
_instance->setAudioInputDevice(id);
|
||||
} else {
|
||||
_instance->setAudioOutputDevice(id);
|
||||
}
|
||||
if (input) {
|
||||
_mediaDevices->switchToAudioInput(deviceId);
|
||||
} else {
|
||||
_mediaDevices->switchToAudioOutput(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,21 +891,26 @@ void GroupCall::applyGlobalShortcutChanges() {
|
||||
}
|
||||
_pushToTalk = shortcut;
|
||||
_shortcutManager->startWatching(_pushToTalk, [=](bool pressed) {
|
||||
const auto delay = Core::App().settings().groupCallPushToTalkDelay();
|
||||
if (muted() == MuteState::ForceMuted
|
||||
|| muted() == MuteState::Active) {
|
||||
return;
|
||||
} else if (pressed) {
|
||||
_pushToTalkCancelTimer.cancel();
|
||||
setMuted(MuteState::PushToTalk);
|
||||
} else if (delay) {
|
||||
_pushToTalkCancelTimer.callOnce(delay);
|
||||
} else {
|
||||
pushToTalkCancel();
|
||||
}
|
||||
pushToTalk(
|
||||
pressed,
|
||||
Core::App().settings().groupCallPushToTalkDelay());
|
||||
});
|
||||
}
|
||||
|
||||
void GroupCall::pushToTalk(bool pressed, crl::time delay) {
|
||||
if (muted() == MuteState::ForceMuted
|
||||
|| muted() == MuteState::Active) {
|
||||
return;
|
||||
} else if (pressed) {
|
||||
_pushToTalkCancelTimer.cancel();
|
||||
setMuted(MuteState::PushToTalk);
|
||||
} else if (delay) {
|
||||
_pushToTalkCancelTimer.callOnce(delay);
|
||||
} else {
|
||||
pushToTalkCancel();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::pushToTalkCancel() {
|
||||
_pushToTalkCancelTimer.cancel();
|
||||
if (muted() == MuteState::PushToTalk) {
|
||||
|
||||
@@ -17,6 +17,7 @@ class History;
|
||||
|
||||
namespace tgcalls {
|
||||
class GroupInstanceImpl;
|
||||
struct GroupLevelsUpdate;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace base {
|
||||
@@ -24,6 +25,14 @@ class GlobalShortcutManager;
|
||||
class GlobalShortcutValue;
|
||||
} // namespace base
|
||||
|
||||
namespace Webrtc {
|
||||
class MediaDevices;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Data {
|
||||
struct LastSpokeTimes;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls {
|
||||
|
||||
enum class MuteState {
|
||||
@@ -42,6 +51,7 @@ enum class MuteState {
|
||||
struct LevelUpdate {
|
||||
uint32 ssrc = 0;
|
||||
float value = 0.;
|
||||
bool voice = false;
|
||||
bool self = false;
|
||||
};
|
||||
|
||||
@@ -55,6 +65,13 @@ public:
|
||||
virtual void groupCallFailed(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess) = 0;
|
||||
|
||||
enum class GroupCallSound {
|
||||
Started,
|
||||
Connecting,
|
||||
Ended,
|
||||
};
|
||||
virtual void groupCallPlaySound(GroupCallSound sound) = 0;
|
||||
};
|
||||
|
||||
using GlobalShortcutManager = base::GlobalShortcutManager;
|
||||
@@ -103,6 +120,7 @@ public:
|
||||
[[nodiscard]] rpl::producer<State> stateValue() const {
|
||||
return _state.value();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> connectingValue() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {
|
||||
return _levelUpdates.events();
|
||||
@@ -120,6 +138,8 @@ public:
|
||||
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
|
||||
void applyGlobalShortcutChanges();
|
||||
|
||||
void pushToTalk(bool pressed, crl::time delay);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
@@ -146,11 +166,7 @@ private:
|
||||
void applySelfInCallLocally();
|
||||
void rejoin();
|
||||
|
||||
void myLevelUpdated(float level);
|
||||
void audioLevelsUpdated(
|
||||
const std::vector<std::pair<std::uint32_t, float>> &data);
|
||||
void handleLevelsUpdated(
|
||||
gsl::span<const std::pair<std::uint32_t, float>> data);
|
||||
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
|
||||
void setInstanceConnected(bool connected);
|
||||
void checkLastSpoke();
|
||||
void pushToTalkCancel();
|
||||
@@ -158,6 +174,10 @@ private:
|
||||
void checkGlobalShortcutAvailability();
|
||||
void checkJoined();
|
||||
|
||||
void playConnectingSound();
|
||||
void stopConnectingSound();
|
||||
void playConnectingSoundOnce();
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
@@ -178,7 +198,7 @@ private:
|
||||
|
||||
std::unique_ptr<tgcalls::GroupInstanceImpl> _instance;
|
||||
rpl::event_stream<LevelUpdate> _levelUpdates;
|
||||
base::flat_map<uint32, crl::time> _lastSpoke;
|
||||
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
|
||||
base::Timer _lastSpokeCheckTimer;
|
||||
base::Timer _checkJoinedTimer;
|
||||
|
||||
@@ -187,7 +207,12 @@ private:
|
||||
std::shared_ptr<GlobalShortcutManager> _shortcutManager;
|
||||
std::shared_ptr<GlobalShortcutValue> _pushToTalk;
|
||||
base::Timer _pushToTalkCancelTimer;
|
||||
bool _pushToTalkStarted = false;
|
||||
base::Timer _connectingSoundTimer;
|
||||
bool _hadJoinedState = false;
|
||||
|
||||
std::unique_ptr<Webrtc::MediaDevices> _mediaDevices;
|
||||
QString _audioInputId;
|
||||
QString _audioOutputId;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_peer_values.h" // Data::CanWriteValue.
|
||||
#include "data/data_session.h" // Data::Session::invitedToCallUsers.
|
||||
#include "settings/settings_common.h" // Settings::CreateButton.
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
@@ -85,6 +87,7 @@ public:
|
||||
Active,
|
||||
Inactive,
|
||||
Muted,
|
||||
Invited,
|
||||
};
|
||||
|
||||
void setSkipLevelUpdate(bool value);
|
||||
@@ -97,6 +100,9 @@ public:
|
||||
[[nodiscard]] uint32 ssrc() const {
|
||||
return _ssrc;
|
||||
}
|
||||
[[nodiscard]] bool sounding() const {
|
||||
return _sounding;
|
||||
}
|
||||
[[nodiscard]] bool speaking() const {
|
||||
return _speaking;
|
||||
}
|
||||
@@ -113,7 +119,9 @@ public:
|
||||
st::groupCallActiveButton.height);
|
||||
}
|
||||
bool actionDisabled() const override {
|
||||
return peer()->isSelf() || !_delegate->rowCanMuteMembers();
|
||||
return peer()->isSelf()
|
||||
|| (_state == State::Invited)
|
||||
|| !_delegate->rowCanMuteMembers();
|
||||
}
|
||||
QMargins actionMargins() const override {
|
||||
return QMargins(
|
||||
@@ -132,6 +140,15 @@ public:
|
||||
|
||||
auto generatePaintUserpicCallback() -> PaintRoundImageCallback override;
|
||||
|
||||
void paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override;
|
||||
|
||||
private:
|
||||
struct BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
@@ -147,7 +164,7 @@ private:
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSpeakingUpdateTime = 0;
|
||||
crl::time lastSoundingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
|
||||
QImage userpicCache;
|
||||
@@ -156,6 +173,7 @@ private:
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
void refreshStatus() override;
|
||||
void setSounding(bool sounding);
|
||||
void setSpeaking(bool speaking);
|
||||
void setState(State state);
|
||||
void setSsrc(uint32 ssrc);
|
||||
@@ -172,6 +190,7 @@ private:
|
||||
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
|
||||
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
|
||||
uint32 _ssrc = 0;
|
||||
bool _sounding = false;
|
||||
bool _speaking = false;
|
||||
bool _skipLevelUpdate = false;
|
||||
|
||||
@@ -218,10 +237,15 @@ private:
|
||||
[[nodiscard]] std::unique_ptr<Row> createSelfRow();
|
||||
[[nodiscard]] std::unique_ptr<Row> createRow(
|
||||
const Data::GroupCall::Participant &participant);
|
||||
[[nodiscard]] std::unique_ptr<Row> createInvitedRow(
|
||||
not_null<UserData*> user);
|
||||
|
||||
void prepareRows(not_null<Data::GroupCall*> real);
|
||||
//void repaintByTimer();
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row);
|
||||
void setupListChangeViewers(not_null<GroupCall*> call);
|
||||
void subscribeToChanges(not_null<Data::GroupCall*> real);
|
||||
void updateRow(
|
||||
@@ -236,6 +260,7 @@ private:
|
||||
Row *findRow(not_null<UserData*> user) const;
|
||||
|
||||
[[nodiscard]] Data::GroupCall *resolvedRealCall() const;
|
||||
void appendInvitedUsers();
|
||||
|
||||
const base::weak_ptr<GroupCall> _call;
|
||||
not_null<PeerData*> _peer;
|
||||
@@ -243,6 +268,7 @@ private:
|
||||
// Use only resolvedRealCall() method, not this value directly.
|
||||
Data::GroupCall *_realCallRawValue = nullptr;
|
||||
uint64 _realId = 0;
|
||||
bool _prepared = false;
|
||||
|
||||
rpl::event_stream<MuteRequest> _toggleMuteRequests;
|
||||
rpl::event_stream<not_null<UserData*>> _kickMemberRequests;
|
||||
@@ -252,10 +278,10 @@ private:
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
base::flat_set<not_null<PeerData*>> _menuCheckRowsAfterHidden;
|
||||
|
||||
base::flat_map<uint32, not_null<Row*>> _speakingRowBySsrc;
|
||||
Ui::Animations::Basic _speakingAnimation;
|
||||
base::flat_map<uint32, not_null<Row*>> _soundingRowBySsrc;
|
||||
Ui::Animations::Basic _soundingAnimation;
|
||||
|
||||
crl::time _speakingAnimationHideLastTime = 0;
|
||||
crl::time _soundingAnimationHideLastTime = 0;
|
||||
bool _skipRowLevelUpdate = false;
|
||||
|
||||
Ui::CrossLineAnimation _inactiveCrossLine;
|
||||
@@ -278,22 +304,21 @@ void Row::setSkipLevelUpdate(bool value) {
|
||||
void Row::updateState(const Data::GroupCall::Participant *participant) {
|
||||
setSsrc(participant ? participant->ssrc : 0);
|
||||
if (!participant) {
|
||||
if (peer()->isSelf()) {
|
||||
setCustomStatus(tr::lng_group_call_connecting(tr::now));
|
||||
} else {
|
||||
setCustomStatus(QString());
|
||||
}
|
||||
setState(State::Inactive);
|
||||
setState(State::Invited);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
} else if (!participant->muted
|
||||
|| (participant->speaking && participant->ssrc != 0)) {
|
||||
|| (participant->sounding && participant->ssrc != 0)) {
|
||||
setState(State::Active);
|
||||
setSounding(participant->sounding && participant->ssrc != 0);
|
||||
setSpeaking(participant->speaking && participant->ssrc != 0);
|
||||
} else if (participant->canSelfUnmute) {
|
||||
setState(State::Inactive);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
} else {
|
||||
setState(State::Muted);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
}
|
||||
}
|
||||
@@ -308,7 +333,14 @@ void Row::setSpeaking(bool speaking) {
|
||||
_speaking ? 0. : 1.,
|
||||
_speaking ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
if (!_speaking) {
|
||||
}
|
||||
|
||||
void Row::setSounding(bool sounding) {
|
||||
if (_sounding == sounding) {
|
||||
return;
|
||||
}
|
||||
_sounding = sounding;
|
||||
if (!_sounding) {
|
||||
_blobsAnimation = nullptr;
|
||||
} else if (!_blobsAnimation) {
|
||||
_blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
@@ -358,7 +390,7 @@ void Row::updateLevel(float level) {
|
||||
}
|
||||
|
||||
if (level >= GroupCall::kSpeakLevelThreshold) {
|
||||
_blobsAnimation->lastSpeakingUpdateTime = crl::now();
|
||||
_blobsAnimation->lastSoundingUpdateTime = crl::now();
|
||||
}
|
||||
_blobsAnimation->blobs.setLevel(level);
|
||||
}
|
||||
@@ -366,14 +398,14 @@ void Row::updateLevel(float level) {
|
||||
void Row::updateBlobAnimation(crl::time now) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto speakingFinishesAt = _blobsAnimation->lastSpeakingUpdateTime
|
||||
+ Data::GroupCall::kSpeakStatusKeptFor;
|
||||
const auto speakingStartsFinishing = speakingFinishesAt
|
||||
const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime
|
||||
+ Data::GroupCall::kSoundStatusKeptFor;
|
||||
const auto soundingStartsFinishing = soundingFinishesAt
|
||||
- kBlobsEnterDuration;
|
||||
const auto speakingFinishes = (speakingStartsFinishing < now);
|
||||
if (speakingFinishes) {
|
||||
const auto soundingFinishes = (soundingStartsFinishing < now);
|
||||
if (soundingFinishes) {
|
||||
_blobsAnimation->enter = std::clamp(
|
||||
(speakingFinishesAt - now) / float64(kBlobsEnterDuration),
|
||||
(soundingFinishesAt - now) / float64(kBlobsEnterDuration),
|
||||
0.,
|
||||
1.);
|
||||
} else if (_blobsAnimation->enter < 1.) {
|
||||
@@ -420,7 +452,11 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback {
|
||||
const auto shift = QPointF(x + size / 2., y + size / 2.);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.translate(shift);
|
||||
_blobsAnimation->blobs.paint(p, st::groupCallMemberActiveStatus);
|
||||
const auto brush = anim::brush(
|
||||
st::groupCallMemberInactiveStatus,
|
||||
st::groupCallMemberActiveStatus,
|
||||
_speakingAnimation.value(_speaking ? 1. : 0.));
|
||||
_blobsAnimation->blobs.paint(p, brush);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
|
||||
@@ -456,6 +492,36 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback {
|
||||
};
|
||||
}
|
||||
|
||||
void Row::paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) {
|
||||
if (_state != State::Invited) {
|
||||
PeerListRow::paintStatusText(
|
||||
p,
|
||||
st,
|
||||
x,
|
||||
y,
|
||||
availableWidth,
|
||||
outerWidth,
|
||||
selected);
|
||||
return;
|
||||
}
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st::groupCallMemberNotJoinedStatus);
|
||||
p.drawTextLeft(
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
(peer()->isSelf()
|
||||
? tr::lng_status_connecting(tr::now)
|
||||
: tr::lng_group_call_invited_status(tr::now)));
|
||||
}
|
||||
|
||||
void Row::paintAction(
|
||||
Painter &p,
|
||||
int x,
|
||||
@@ -464,6 +530,20 @@ void Row::paintAction(
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
auto size = actionSize();
|
||||
const auto iconRect = style::rtlrect(
|
||||
x,
|
||||
y,
|
||||
size.width(),
|
||||
size.height(),
|
||||
outerWidth);
|
||||
if (_state == State::Invited) {
|
||||
_actionRipple = nullptr;
|
||||
st::groupCallMemberInvited.paint(
|
||||
p,
|
||||
QPoint(x, y) + st::groupCallMemberInvitedPosition,
|
||||
outerWidth);
|
||||
return;
|
||||
}
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(
|
||||
p,
|
||||
@@ -474,12 +554,6 @@ void Row::paintAction(
|
||||
_actionRipple.reset();
|
||||
}
|
||||
}
|
||||
const auto iconRect = style::rtlrect(
|
||||
x,
|
||||
y,
|
||||
size.width(),
|
||||
size.height(),
|
||||
outerWidth);
|
||||
const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.);
|
||||
const auto active = _activeAnimation.value(
|
||||
(_state == State::Active) ? 1. : 0.);
|
||||
@@ -537,28 +611,28 @@ MembersController::MembersController(
|
||||
) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
|
||||
const auto hide = !(!animDisabled && !deactivated);
|
||||
|
||||
if (!(hide && _speakingAnimationHideLastTime)) {
|
||||
_speakingAnimationHideLastTime = hide ? crl::now() : 0;
|
||||
if (!(hide && _soundingAnimationHideLastTime)) {
|
||||
_soundingAnimationHideLastTime = hide ? crl::now() : 0;
|
||||
}
|
||||
for (const auto [_, row] : _speakingRowBySsrc) {
|
||||
for (const auto [_, row] : _soundingRowBySsrc) {
|
||||
if (hide) {
|
||||
updateRowLevel(row, 0.);
|
||||
}
|
||||
row->setSkipLevelUpdate(hide);
|
||||
}
|
||||
if (!hide && !_speakingAnimation.animating()) {
|
||||
_speakingAnimation.start();
|
||||
if (!hide && !_soundingAnimation.animating()) {
|
||||
_soundingAnimation.start();
|
||||
}
|
||||
_skipRowLevelUpdate = hide;
|
||||
}, _lifetime);
|
||||
|
||||
_speakingAnimation.init([=](crl::time now) {
|
||||
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
|
||||
_soundingAnimation.init([=](crl::time now) {
|
||||
if (const auto &last = _soundingAnimationHideLastTime; (last > 0)
|
||||
&& (now - last >= kBlobsEnterDuration)) {
|
||||
_speakingAnimation.stop();
|
||||
_soundingAnimation.stop();
|
||||
return false;
|
||||
}
|
||||
for (const auto [ssrc, row] : _speakingRowBySsrc) {
|
||||
for (const auto [ssrc, row] : _soundingRowBySsrc) {
|
||||
row->updateBlobAnimation(now);
|
||||
delegate()->peerListUpdateRow(row);
|
||||
}
|
||||
@@ -567,10 +641,7 @@ MembersController::MembersController(
|
||||
}
|
||||
|
||||
MembersController::~MembersController() {
|
||||
if (_menu) {
|
||||
_menu->setDestroyedCallback(nullptr);
|
||||
_menu = nullptr;
|
||||
}
|
||||
base::take(_menu);
|
||||
}
|
||||
|
||||
void MembersController::setupListChangeViewers(not_null<GroupCall*> call) {
|
||||
@@ -600,8 +671,8 @@ void MembersController::setupListChangeViewers(not_null<GroupCall*> call) {
|
||||
|
||||
call->levelUpdates(
|
||||
) | rpl::start_with_next([=](const LevelUpdate &update) {
|
||||
const auto i = _speakingRowBySsrc.find(update.ssrc);
|
||||
if (i != end(_speakingRowBySsrc)) {
|
||||
const auto i = _soundingRowBySsrc.find(update.ssrc);
|
||||
if (i != end(_soundingRowBySsrc)) {
|
||||
updateRowLevel(i->second, update.value);
|
||||
}
|
||||
}, _lifetime);
|
||||
@@ -629,6 +700,7 @@ void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
||||
const auto user = update.was ? update.was->user : update.now->user;
|
||||
if (!update.now) {
|
||||
if (const auto row = findRow(user)) {
|
||||
const auto owner = &user->owner();
|
||||
if (user->isSelf()) {
|
||||
updateRow(row, nullptr);
|
||||
} else {
|
||||
@@ -640,6 +712,30 @@ void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
||||
updateRow(update.was, *update.now);
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
if (_prepared) {
|
||||
appendInvitedUsers();
|
||||
}
|
||||
}
|
||||
|
||||
void MembersController::appendInvitedUsers() {
|
||||
for (const auto user : _peer->owner().invitedToCallUsers(_realId)) {
|
||||
if (auto row = createInvitedRow(user)) {
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
|
||||
using Invite = Data::Session::InviteToCall;
|
||||
_peer->owner().invitesToCalls(
|
||||
) | rpl::filter([=](const Invite &invite) {
|
||||
return (invite.id == _realId);
|
||||
}) | rpl::start_with_next([=](const Invite &invite) {
|
||||
if (auto row = createInvitedRow(invite.user)) {
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MembersController::updateRow(
|
||||
@@ -654,7 +750,21 @@ void MembersController::updateRow(
|
||||
if (row->speaking()) {
|
||||
delegate()->peerListPrependRow(std::move(row));
|
||||
} else {
|
||||
static constexpr auto kInvited = Row::State::Invited;
|
||||
const auto reorder = [&] {
|
||||
const auto count = delegate()->peerListFullRowsCount();
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
const auto row = delegate()->peerListRowAt(count - 1).get();
|
||||
return (static_cast<Row*>(row)->state() == kInvited);
|
||||
}();
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
if (reorder) {
|
||||
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
||||
return static_cast<const Row&>(row).state() != kInvited;
|
||||
});
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
@@ -699,41 +809,41 @@ void MembersController::checkSpeakingRowPosition(not_null<Row*> row) {
|
||||
void MembersController::updateRow(
|
||||
not_null<Row*> row,
|
||||
const Data::GroupCall::Participant *participant) {
|
||||
const auto wasSpeaking = row->speaking();
|
||||
const auto wasSounding = row->sounding();
|
||||
const auto wasSsrc = row->ssrc();
|
||||
row->setSkipLevelUpdate(_skipRowLevelUpdate);
|
||||
row->updateState(participant);
|
||||
const auto nowSpeaking = row->speaking();
|
||||
const auto nowSounding = row->sounding();
|
||||
const auto nowSsrc = row->ssrc();
|
||||
|
||||
const auto wasNoSpeaking = _speakingRowBySsrc.empty();
|
||||
const auto wasNoSounding = _soundingRowBySsrc.empty();
|
||||
if (wasSsrc == nowSsrc) {
|
||||
if (nowSpeaking != wasSpeaking) {
|
||||
if (nowSpeaking) {
|
||||
_speakingRowBySsrc.emplace(nowSsrc, row);
|
||||
if (nowSounding != wasSounding) {
|
||||
if (nowSounding) {
|
||||
_soundingRowBySsrc.emplace(nowSsrc, row);
|
||||
} else {
|
||||
_speakingRowBySsrc.remove(nowSsrc);
|
||||
_soundingRowBySsrc.remove(nowSsrc);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_speakingRowBySsrc.remove(wasSsrc);
|
||||
if (nowSpeaking) {
|
||||
_soundingRowBySsrc.remove(wasSsrc);
|
||||
if (nowSounding) {
|
||||
Assert(nowSsrc != 0);
|
||||
_speakingRowBySsrc.emplace(nowSsrc, row);
|
||||
_soundingRowBySsrc.emplace(nowSsrc, row);
|
||||
}
|
||||
}
|
||||
const auto nowNoSpeaking = _speakingRowBySsrc.empty();
|
||||
if (wasNoSpeaking && !nowNoSpeaking) {
|
||||
_speakingAnimation.start();
|
||||
} else if (nowNoSpeaking && !wasNoSpeaking) {
|
||||
_speakingAnimation.stop();
|
||||
const auto nowNoSounding = _soundingRowBySsrc.empty();
|
||||
if (wasNoSounding && !nowNoSounding) {
|
||||
_soundingAnimation.start();
|
||||
} else if (nowNoSounding && !wasNoSounding) {
|
||||
_soundingAnimation.stop();
|
||||
}
|
||||
|
||||
delegate()->peerListUpdateRow(row);
|
||||
}
|
||||
|
||||
void MembersController::removeRow(not_null<Row*> row) {
|
||||
_speakingRowBySsrc.remove(row->ssrc());
|
||||
_soundingRowBySsrc.remove(row->ssrc());
|
||||
delegate()->peerListRemoveRow(row);
|
||||
}
|
||||
|
||||
@@ -776,7 +886,12 @@ void MembersController::prepare() {
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
loadMoreRows();
|
||||
if (_realId) {
|
||||
appendInvitedUsers();
|
||||
}
|
||||
_prepared = true;
|
||||
}
|
||||
|
||||
void MembersController::prepareRows(not_null<Data::GroupCall*> real) {
|
||||
@@ -901,29 +1016,20 @@ auto MembersController::kickMemberRequests() const
|
||||
}
|
||||
|
||||
void MembersController::rowClicked(not_null<PeerListRow*> row) {
|
||||
if (_menu) {
|
||||
_menu->setDestroyedCallback(nullptr);
|
||||
_menu->deleteLater();
|
||||
_menu = nullptr;
|
||||
}
|
||||
_menu = rowContextMenu(_menuParent, row);
|
||||
if (const auto raw = _menu.get()) {
|
||||
raw->setDestroyedCallback([=] {
|
||||
if (_menu && _menu.get() != raw) {
|
||||
return;
|
||||
}
|
||||
auto saved = base::take(_menu);
|
||||
for (const auto peer : base::take(_menuCheckRowsAfterHidden)) {
|
||||
if (const auto row = findRow(peer->asUser())) {
|
||||
if (row->speaking()) {
|
||||
checkSpeakingRowPosition(row);
|
||||
}
|
||||
delegate()->peerListShowRowMenu(row, [=](not_null<Ui::PopupMenu*> menu) {
|
||||
if (!_menu || _menu.get() != menu) {
|
||||
return;
|
||||
}
|
||||
auto saved = base::take(_menu);
|
||||
for (const auto peer : base::take(_menuCheckRowsAfterHidden)) {
|
||||
if (const auto row = findRow(peer->asUser())) {
|
||||
if (row->speaking()) {
|
||||
checkSpeakingRowPosition(row);
|
||||
}
|
||||
}
|
||||
_menu = std::move(saved);
|
||||
});
|
||||
raw->popup(QCursor::pos());
|
||||
}
|
||||
}
|
||||
_menu = std::move(saved);
|
||||
});
|
||||
}
|
||||
|
||||
void MembersController::rowActionClicked(
|
||||
@@ -934,6 +1040,23 @@ void MembersController::rowActionClicked(
|
||||
base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
auto result = createRowContextMenu(parent, row);
|
||||
|
||||
if (result) {
|
||||
// First clear _menu value, so that we don't check row positions yet.
|
||||
base::take(_menu);
|
||||
|
||||
// Here unique_qptr is used like a shared pointer, where
|
||||
// not the last destroyed pointer destroys the object, but the first.
|
||||
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
Expects(row->peer()->isUser());
|
||||
|
||||
if (row->peer()->isSelf()) {
|
||||
@@ -945,7 +1068,29 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
|
||||
parent,
|
||||
st::groupCallPopupMenu);
|
||||
|
||||
const auto mute = (real->state() != Row::State::Muted);
|
||||
const auto muteState = real->state();
|
||||
const auto admin = [&] {
|
||||
if (const auto chat = _peer->asChat()) {
|
||||
return chat->admins.contains(user)
|
||||
|| (chat->creator == user->bareId());
|
||||
} else if (const auto group = _peer->asMegagroup()) {
|
||||
if (const auto mgInfo = group->mgInfo.get()) {
|
||||
if (mgInfo->creator == user) {
|
||||
return true;
|
||||
}
|
||||
const auto i = mgInfo->lastAdmins.find(user);
|
||||
if (i == mgInfo->lastAdmins.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto &rights = i->second.rights;
|
||||
return rights.c_chatAdminRights().is_manage_call();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
const auto mute = admin
|
||||
? (muteState == Row::State::Active)
|
||||
: (muteState != Row::State::Muted);
|
||||
const auto toggleMute = crl::guard(this, [=] {
|
||||
_toggleMuteRequests.fire(MuteRequest{
|
||||
.user = user,
|
||||
@@ -993,14 +1138,18 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
|
||||
};
|
||||
const auto showHistory = [=] {
|
||||
performOnMainWindow([=](not_null<Window::SessionController*> window) {
|
||||
window->showPeerHistory(user);
|
||||
window->showPeerHistory(
|
||||
user,
|
||||
Window::SectionShow::Way::Forward);
|
||||
});
|
||||
};
|
||||
const auto removeFromGroup = crl::guard(this, [=] {
|
||||
_kickMemberRequests.fire_copy(user);
|
||||
});
|
||||
|
||||
if (_peer->canManageGroupCall()) {
|
||||
if ((muteState != Row::State::Invited)
|
||||
&& _peer->canManageGroupCall()
|
||||
&& (!admin || mute)) {
|
||||
result->addAction(
|
||||
(mute
|
||||
? tr::lng_group_call_context_mute(tr::now)
|
||||
@@ -1014,7 +1163,9 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
|
||||
tr::lng_context_send_message(tr::now),
|
||||
showHistory);
|
||||
const auto canKick = [&] {
|
||||
if (const auto chat = _peer->asChat()) {
|
||||
if (static_cast<Row*>(row.get())->state() == Row::State::Invited) {
|
||||
return false;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
return chat->amCreator()
|
||||
|| (chat->canBanMembers() && !chat->admins.contains(user));
|
||||
} else if (const auto group = _peer->asMegagroup()) {
|
||||
@@ -1044,6 +1195,16 @@ std::unique_ptr<Row> MembersController::createRow(
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Row> MembersController::createInvitedRow(
|
||||
not_null<UserData*> user) {
|
||||
if (findRow(user)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto result = std::make_unique<Row>(this, user);
|
||||
updateRow(result.get(), nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GroupMembers::GroupMembers(
|
||||
@@ -1053,20 +1214,11 @@ GroupMembers::GroupMembers(
|
||||
, _call(call)
|
||||
, _scroll(this, st::defaultSolidScroll)
|
||||
, _listController(std::make_unique<MembersController>(call, parent)) {
|
||||
setupHeader(call);
|
||||
setupAddMember(call);
|
||||
setupList();
|
||||
setContent(_list);
|
||||
setupFakeRoundCorners();
|
||||
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
|
||||
|
||||
paintRequest(
|
||||
) | rpl::start_with_next([=](QRect clip) {
|
||||
const auto headerPart = clip.intersected(
|
||||
QRect(0, 0, width(), _header->height()));
|
||||
if (!headerPart.isEmpty()) {
|
||||
QPainter(this).fillRect(headerPart, st::groupCallMembersBg);
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
auto GroupMembers::toggleMuteRequests() const
|
||||
@@ -1082,7 +1234,7 @@ auto GroupMembers::kickMemberRequests() const
|
||||
}
|
||||
|
||||
int GroupMembers::desiredHeight() const {
|
||||
auto desired = _header ? _header->height() : 0;
|
||||
const auto top = _addMember ? _addMember->height() : 0;
|
||||
auto count = [&] {
|
||||
if (const auto call = _call.get()) {
|
||||
if (const auto real = call->peer()->groupCall()) {
|
||||
@@ -1094,7 +1246,7 @@ int GroupMembers::desiredHeight() const {
|
||||
return 0;
|
||||
}();
|
||||
const auto use = std::max(count, _list->fullRowsCount());
|
||||
return (_header ? _header->height() : 0)
|
||||
return top
|
||||
+ (use * st::groupCallMembersList.item.height)
|
||||
+ (use ? st::lineWidth : 0);
|
||||
}
|
||||
@@ -1104,48 +1256,14 @@ rpl::producer<int> GroupMembers::desiredHeightValue() const {
|
||||
_listController.get());
|
||||
return rpl::combine(
|
||||
heightValue(),
|
||||
_addMemberButton.value(),
|
||||
controller->fullCountValue()
|
||||
) | rpl::map([=] {
|
||||
return desiredHeight();
|
||||
});
|
||||
}
|
||||
|
||||
void GroupMembers::setupHeader(not_null<GroupCall*> call) {
|
||||
_header = object_ptr<Ui::FixedHeightWidget>(
|
||||
this,
|
||||
st::groupCallMembersHeader);
|
||||
auto parent = _header.data();
|
||||
|
||||
_titleWrap = Ui::CreateChild<Ui::RpWidget>(parent);
|
||||
_title = setupTitle(call);
|
||||
_addMember = Ui::CreateChild<Ui::IconButton>(
|
||||
parent,
|
||||
st::groupCallAddMember);
|
||||
setupButtons(call);
|
||||
|
||||
widthValue(
|
||||
) | rpl::start_with_next([this](int width) {
|
||||
_header->resizeToWidth(width);
|
||||
}, _header->lifetime());
|
||||
}
|
||||
|
||||
object_ptr<Ui::FlatLabel> GroupMembers::setupTitle(
|
||||
not_null<GroupCall*> call) {
|
||||
const auto controller = static_cast<MembersController*>(
|
||||
_listController.get());
|
||||
auto result = object_ptr<Ui::FlatLabel>(
|
||||
_titleWrap,
|
||||
tr::lng_chat_status_members(
|
||||
lt_count_decimal,
|
||||
controller->fullCountValue() | tr::to_count(),
|
||||
Ui::Text::Upper
|
||||
),
|
||||
st::groupCallHeaderLabel);
|
||||
result->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
return result;
|
||||
}
|
||||
|
||||
void GroupMembers::setupButtons(not_null<GroupCall*> call) {
|
||||
void GroupMembers::setupAddMember(not_null<GroupCall*> call) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
_canAddMembers = Data::CanWriteValue(call->peer().get());
|
||||
@@ -1156,65 +1274,89 @@ void GroupMembers::setupButtons(not_null<GroupCall*> call) {
|
||||
_canAddMembers = Data::CanWriteValue(channel.get());
|
||||
});
|
||||
|
||||
_addMember->showOn(_canAddMembers.value());
|
||||
_addMember->addClickHandler([=] { // TODO throttle(ripple duration)
|
||||
_addMemberRequests.fire({});
|
||||
});
|
||||
_canAddMembers.value(
|
||||
) | rpl::start_with_next([=](bool can) {
|
||||
if (!can) {
|
||||
_addMemberButton = nullptr;
|
||||
_addMember.destroy();
|
||||
updateControlsGeometry();
|
||||
return;
|
||||
}
|
||||
_addMember = Settings::CreateButton(
|
||||
this,
|
||||
tr::lng_group_call_invite(),
|
||||
st::groupCallAddMember,
|
||||
&st::groupCallAddMemberIcon,
|
||||
st::groupCallAddMemberIconLeft);
|
||||
_addMember->show();
|
||||
|
||||
_addMember->addClickHandler([=] { // TODO throttle(ripple duration)
|
||||
_addMemberRequests.fire({});
|
||||
});
|
||||
_addMemberButton = _addMember.data();
|
||||
|
||||
resizeToList();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void GroupMembers::setupList() {
|
||||
auto topSkip = _header ? _header->height() : 0;
|
||||
rpl::producer<int> GroupMembers::fullCountValue() const {
|
||||
return static_cast<MembersController*>(
|
||||
_listController.get())->fullCountValue();
|
||||
}
|
||||
|
||||
//tr::lng_chat_status_members(
|
||||
// lt_count_decimal,
|
||||
// controller->fullCountValue() | tr::to_count(),
|
||||
// Ui::Text::Upper
|
||||
//),
|
||||
|
||||
void GroupMembers::setupList() {
|
||||
_listController->setStyleOverrides(&st::groupCallMembersList);
|
||||
_list = _scroll->setOwnedWidget(object_ptr<ListWidget>(
|
||||
this,
|
||||
_listController.get()));
|
||||
|
||||
sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
_scroll->setGeometry(0, topSkip, size.width(), size.height() - topSkip);
|
||||
_list->resizeToWidth(size.width());
|
||||
_list->heightValue(
|
||||
) | rpl::start_with_next([=] {
|
||||
resizeToList();
|
||||
}, _list->lifetime());
|
||||
|
||||
_list->heightValue(
|
||||
) | rpl::start_with_next([=](int listHeight) {
|
||||
auto newHeight = (listHeight > 0)
|
||||
? (topSkip + listHeight + st::lineWidth)
|
||||
: 0;
|
||||
resize(width(), newHeight);
|
||||
}, _list->lifetime());
|
||||
_list->moveToLeft(0, topSkip);
|
||||
_list->show();
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void GroupMembers::resizeEvent(QResizeEvent *e) {
|
||||
if (_header) {
|
||||
updateHeaderControlsGeometry(width());
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void GroupMembers::resizeToList() {
|
||||
if (!_list) {
|
||||
return;
|
||||
}
|
||||
const auto listHeight = _list->height();
|
||||
const auto newHeight = (listHeight > 0)
|
||||
? ((_addMember ? _addMember->height() : 0)
|
||||
+ listHeight
|
||||
+ st::lineWidth)
|
||||
: 0;
|
||||
if (height() == newHeight) {
|
||||
updateControlsGeometry();
|
||||
} else {
|
||||
resize(width(), newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupMembers::updateHeaderControlsGeometry(int newWidth) {
|
||||
auto availableWidth = newWidth
|
||||
- st::groupCallAddButtonPosition.x();
|
||||
_addMember->moveToLeft(
|
||||
availableWidth - _addMember->width(),
|
||||
st::groupCallAddButtonPosition.y(),
|
||||
newWidth);
|
||||
if (!_addMember->isHidden()) {
|
||||
availableWidth -= _addMember->width();
|
||||
void GroupMembers::updateControlsGeometry() {
|
||||
if (!_list) {
|
||||
return;
|
||||
}
|
||||
|
||||
_titleWrap->resize(
|
||||
availableWidth - _addMember->width() - st::groupCallHeaderPosition.x(),
|
||||
_title->height());
|
||||
_titleWrap->moveToLeft(
|
||||
st::groupCallHeaderPosition.x(),
|
||||
st::groupCallHeaderPosition.y(),
|
||||
newWidth);
|
||||
_titleWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_title->resizeToWidth(_titleWrap->width());
|
||||
_title->moveToLeft(0, 0);
|
||||
auto topSkip = 0;
|
||||
if (_addMember) {
|
||||
_addMember->resizeToWidth(width());
|
||||
_addMember->move(0, 0);
|
||||
topSkip = _addMember->height();
|
||||
}
|
||||
_scroll->setGeometry(0, topSkip, width(), height() - topSkip);
|
||||
_list->resizeToWidth(width());
|
||||
}
|
||||
|
||||
void GroupMembers::setupFakeRoundCorners() {
|
||||
@@ -1292,6 +1434,9 @@ void GroupMembers::peerListSetTitle(rpl::producer<QString> title) {
|
||||
void GroupMembers::peerListSetAdditionalTitle(rpl::producer<QString> title) {
|
||||
}
|
||||
|
||||
void GroupMembers::peerListSetHideEmpty(bool hide) {
|
||||
}
|
||||
|
||||
bool GroupMembers::peerListIsRowChecked(not_null<PeerListRow*> row) {
|
||||
return false;
|
||||
}
|
||||
@@ -1303,10 +1448,6 @@ int GroupMembers::peerListSelectedRowsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<not_null<PeerData*>> GroupMembers::peerListCollectSelectedRows() {
|
||||
return {};
|
||||
}
|
||||
|
||||
void GroupMembers::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
|
||||
Unexpected("Item selection in Calls::GroupMembers.");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Ui {
|
||||
class ScrollArea;
|
||||
class SettingsButton;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
@@ -36,6 +37,7 @@ public:
|
||||
|
||||
[[nodiscard]] int desiredHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const;
|
||||
[[nodiscard]] rpl::producer<MuteRequest> toggleMuteRequests() const;
|
||||
[[nodiscard]] auto kickMemberRequests() const
|
||||
-> rpl::producer<not_null<UserData*>>;
|
||||
@@ -55,10 +57,10 @@ private:
|
||||
// PeerListContentDelegate interface.
|
||||
void peerListSetTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetHideEmpty(bool hide) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
void peerListScrollToTop() override;
|
||||
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
void peerListAddSelectedRowInBunch(
|
||||
@@ -67,25 +69,21 @@ private:
|
||||
void peerListSetDescription(
|
||||
object_ptr<Ui::FlatLabel> description) override;
|
||||
|
||||
void setupHeader(not_null<GroupCall*> call);
|
||||
object_ptr<Ui::FlatLabel> setupTitle(not_null<GroupCall*> call);
|
||||
void setupAddMember(not_null<GroupCall*> call);
|
||||
void resizeToList();
|
||||
void setupList();
|
||||
void setupFakeRoundCorners();
|
||||
|
||||
void setupButtons(not_null<GroupCall*> call);
|
||||
|
||||
void updateHeaderControlsGeometry(int newWidth);
|
||||
void updateControlsGeometry();
|
||||
|
||||
const base::weak_ptr<GroupCall> _call;
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
std::unique_ptr<PeerListController> _listController;
|
||||
object_ptr<Ui::RpWidget> _header = { nullptr };
|
||||
object_ptr<Ui::SettingsButton> _addMember = { nullptr };
|
||||
rpl::variable<Ui::SettingsButton*> _addMemberButton = nullptr;
|
||||
ListWidget *_list = { nullptr };
|
||||
rpl::event_stream<> _addMemberRequests;
|
||||
|
||||
Ui::RpWidget *_titleWrap = nullptr;
|
||||
Ui::FlatLabel *_title = nullptr;
|
||||
Ui::IconButton *_addMember = nullptr;
|
||||
rpl::variable<bool> _canAddMembers;
|
||||
|
||||
};
|
||||
|
||||
@@ -29,6 +29,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/peers/edit_participants_box.h"
|
||||
#include "boxes/peers/add_participants_box.h"
|
||||
#include "boxes/peer_lists_box.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "app.h"
|
||||
#include "apiwrap.h" // api().kickParticipant.
|
||||
#include "styles/style_calls.h"
|
||||
@@ -45,12 +48,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSpacePushToTalkDelay = crl::time(250);
|
||||
|
||||
class InviteController final : public ParticipantsBoxController {
|
||||
public:
|
||||
InviteController(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn,
|
||||
int fullInCount);
|
||||
base::flat_set<not_null<UserData*>> alreadyIn);
|
||||
|
||||
void prepare() override;
|
||||
|
||||
@@ -61,34 +65,81 @@ public:
|
||||
|
||||
void itemDeselectedHook(not_null<PeerData*> peer) override;
|
||||
|
||||
std::variant<int, not_null<UserData*>> inviteSelectedUsers(
|
||||
not_null<PeerListBox*> box,
|
||||
not_null<GroupCall*> call) const;
|
||||
[[nodiscard]] auto peersWithRows() const
|
||||
-> not_null<const base::flat_set<not_null<UserData*>>*>;
|
||||
[[nodiscard]] rpl::producer<not_null<UserData*>> rowAdded() const;
|
||||
|
||||
[[nodiscard]] bool hasRowFor(not_null<PeerData*> peer) const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] int alreadyInCount() const;
|
||||
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
|
||||
[[nodiscard]] int fullCount() const;
|
||||
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<UserData*> user) const override;
|
||||
|
||||
not_null<PeerData*> _peer;
|
||||
const base::flat_set<not_null<UserData*>> _alreadyIn;
|
||||
const int _fullInCount = 0;
|
||||
mutable base::flat_set<not_null<UserData*>> _skippedUsers;
|
||||
mutable base::flat_set<not_null<UserData*>> _inGroup;
|
||||
rpl::event_stream<not_null<UserData*>> _rowAdded;
|
||||
|
||||
};
|
||||
|
||||
class InviteContactsController final : public AddParticipantsBoxController {
|
||||
public:
|
||||
InviteContactsController(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn,
|
||||
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
|
||||
rpl::producer<not_null<UserData*>> discoveredInGroup);
|
||||
|
||||
private:
|
||||
void prepareViewHook() override;
|
||||
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<UserData*> user) override;
|
||||
|
||||
const not_null<const base::flat_set<not_null<UserData*>>*> _inGroup;
|
||||
rpl::producer<not_null<UserData*>> _discoveredInGroup;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> CreateSectionSubtitle(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> text) {
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(
|
||||
parent,
|
||||
st::searchedBarHeight);
|
||||
|
||||
const auto raw = result.data();
|
||||
raw->paintRequest(
|
||||
) | rpl::start_with_next([=](QRect clip) {
|
||||
auto p = QPainter(raw);
|
||||
p.fillRect(clip, st::groupCallMembersBgOver);
|
||||
}, raw->lifetime());
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
std::move(text),
|
||||
st::groupCallBoxLabel);
|
||||
raw->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
const auto padding = st::groupCallInviteDividerPadding;
|
||||
const auto available = width - padding.left() - padding.right();
|
||||
label->resizeToNaturalWidth(available);
|
||||
label->moveToLeft(padding.left(), padding.top(), width);
|
||||
}, label->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
InviteController::InviteController(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn,
|
||||
int fullInCount)
|
||||
base::flat_set<not_null<UserData*>> alreadyIn)
|
||||
: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members)
|
||||
, _peer(peer)
|
||||
, _alreadyIn(std::move(alreadyIn))
|
||||
, _fullInCount(std::max(fullInCount, int(_alreadyIn.size()))) {
|
||||
_skippedUsers.emplace(peer->session().user());
|
||||
, _alreadyIn(std::move(alreadyIn)) {
|
||||
SubscribeToMigration(
|
||||
_peer,
|
||||
lifetime(),
|
||||
@@ -96,8 +147,14 @@ InviteController::InviteController(
|
||||
}
|
||||
|
||||
void InviteController::prepare() {
|
||||
delegate()->peerListSetHideEmpty(true);
|
||||
ParticipantsBoxController::prepare();
|
||||
delegate()->peerListSetTitle(tr::lng_group_call_invite_title());
|
||||
delegate()->peerListSetAboveWidget(CreateSectionSubtitle(
|
||||
nullptr,
|
||||
tr::lng_group_call_invite_members()));
|
||||
delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(
|
||||
nullptr,
|
||||
tr::lng_group_call_invite_members()));
|
||||
}
|
||||
|
||||
void InviteController::rowClicked(not_null<PeerListRow*> row) {
|
||||
@@ -113,44 +170,71 @@ base::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(
|
||||
void InviteController::itemDeselectedHook(not_null<PeerData*> peer) {
|
||||
}
|
||||
|
||||
int InviteController::alreadyInCount() const {
|
||||
return std::max(_fullInCount, int(_alreadyIn.size()));
|
||||
bool InviteController::hasRowFor(not_null<PeerData*> peer) const {
|
||||
return (delegate()->peerListFindRow(peer->id) != nullptr);
|
||||
}
|
||||
|
||||
bool InviteController::isAlreadyIn(not_null<UserData*> user) const {
|
||||
return _alreadyIn.contains(user);
|
||||
}
|
||||
|
||||
int InviteController::fullCount() const {
|
||||
return alreadyInCount() + delegate()->peerListSelectedRowsCount();
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> InviteController::createRow(
|
||||
not_null<UserData*> user) const {
|
||||
if (user->isSelf() || user->isBot()) {
|
||||
_skippedUsers.emplace(user);
|
||||
return nullptr;
|
||||
}
|
||||
auto result = std::make_unique<PeerListRow>(user);
|
||||
_rowAdded.fire_copy(user);
|
||||
_inGroup.emplace(user);
|
||||
if (isAlreadyIn(user)) {
|
||||
result->setDisabledState(PeerListRow::State::DisabledChecked);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::variant<int, not_null<UserData*>> InviteController::inviteSelectedUsers(
|
||||
not_null<PeerListBox*> box,
|
||||
not_null<GroupCall*> call) const {
|
||||
const auto rows = box->peerListCollectSelectedRows();
|
||||
const auto users = ranges::view::all(
|
||||
rows
|
||||
) | ranges::view::transform([](not_null<PeerData*> peer) {
|
||||
Expects(peer->isUser());
|
||||
Expects(!peer->isSelf());
|
||||
auto InviteController::peersWithRows() const
|
||||
-> not_null<const base::flat_set<not_null<UserData*>>*> {
|
||||
return &_inGroup;
|
||||
}
|
||||
|
||||
return not_null<UserData*>(peer->asUser());
|
||||
}) | ranges::to_vector;
|
||||
return call->inviteUsers(users);
|
||||
rpl::producer<not_null<UserData*>> InviteController::rowAdded() const {
|
||||
return _rowAdded.events();
|
||||
}
|
||||
|
||||
InviteContactsController::InviteContactsController(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn,
|
||||
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
|
||||
rpl::producer<not_null<UserData*>> discoveredInGroup)
|
||||
: AddParticipantsBoxController(peer, std::move(alreadyIn))
|
||||
, _inGroup(inGroup)
|
||||
, _discoveredInGroup(std::move(discoveredInGroup)) {
|
||||
}
|
||||
|
||||
void InviteContactsController::prepareViewHook() {
|
||||
AddParticipantsBoxController::prepareViewHook();
|
||||
|
||||
delegate()->peerListSetAboveWidget(CreateSectionSubtitle(
|
||||
nullptr,
|
||||
tr::lng_contacts_header()));
|
||||
delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(
|
||||
nullptr,
|
||||
tr::lng_group_call_invite_search_results()));
|
||||
|
||||
std::move(
|
||||
_discoveredInGroup
|
||||
) | rpl::start_with_next([=](not_null<UserData*> user) {
|
||||
if (auto row = delegate()->peerListFindRow(user->id)) {
|
||||
delegate()->peerListRemoveRow(row);
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> InviteContactsController::createRow(
|
||||
not_null<UserData*> user) {
|
||||
return _inGroup->contains(user)
|
||||
? nullptr
|
||||
: AddParticipantsBoxController::createRow(user);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -195,6 +279,21 @@ void LeaveGroupCallBox(
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void GroupCallConfirmBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &text,
|
||||
rpl::producer<QString> button,
|
||||
Fn<void()> callback) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
text,
|
||||
st::groupCallBoxLabel),
|
||||
st::boxPadding);
|
||||
box->addButton(std::move(button), callback);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
GroupPanel::GroupPanel(not_null<GroupCall*> call)
|
||||
: _call(call)
|
||||
, _peer(call->peer())
|
||||
@@ -217,6 +316,7 @@ GroupPanel::GroupPanel(not_null<GroupCall*> call)
|
||||
, _hangup(widget(), st::groupCallHangup) {
|
||||
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
|
||||
_settings->setColorOverrides(_mute->colorOverrides());
|
||||
_layerBg->setHideByBackgroundClick(true);
|
||||
|
||||
SubscribeToMigration(
|
||||
_peer,
|
||||
@@ -238,6 +338,14 @@ bool GroupPanel::isActive() const {
|
||||
&& !(_window->windowState() & Qt::WindowMinimized);
|
||||
}
|
||||
|
||||
void GroupPanel::minimize() {
|
||||
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
|
||||
}
|
||||
|
||||
void GroupPanel::close() {
|
||||
_window->close();
|
||||
}
|
||||
|
||||
void GroupPanel::showAndActivate() {
|
||||
if (_window->isHidden()) {
|
||||
_window->show();
|
||||
@@ -272,7 +380,7 @@ void GroupPanel::initWindow() {
|
||||
_window->setAttribute(Qt::WA_NoSystemBackground);
|
||||
_window->setWindowIcon(
|
||||
QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));
|
||||
_window->setTitleStyle(st::callTitle);
|
||||
_window->setTitleStyle(st::groupCallTitle);
|
||||
|
||||
subscribeToPeerChanges();
|
||||
|
||||
@@ -280,6 +388,15 @@ void GroupPanel::initWindow() {
|
||||
if (e->type() == QEvent::Close && handleClose()) {
|
||||
e->ignore();
|
||||
return base::EventFilterResult::Cancel;
|
||||
} else if (e->type() == QEvent::KeyPress
|
||||
|| e->type() == QEvent::KeyRelease) {
|
||||
if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Space) {
|
||||
if (_call) {
|
||||
_call->pushToTalk(
|
||||
e->type() == QEvent::KeyPress,
|
||||
kSpacePushToTalkDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
@@ -315,30 +432,35 @@ void GroupPanel::initWidget() {
|
||||
}, widget()->lifetime());
|
||||
}
|
||||
|
||||
void GroupPanel::hangup(bool discardCallChecked) {
|
||||
void GroupPanel::endCall() {
|
||||
if (!_call) {
|
||||
return;
|
||||
} else if (!_call->peer()->canManageGroupCall()) {
|
||||
_call->hangup();
|
||||
return;
|
||||
}
|
||||
_layerBg->showBox(Box(
|
||||
LeaveGroupCallBox,
|
||||
_call,
|
||||
discardCallChecked,
|
||||
false,
|
||||
BoxContext::GroupCallPanel));
|
||||
}
|
||||
|
||||
void GroupPanel::initControls() {
|
||||
_mute->clicks(
|
||||
) | rpl::filter([=](Qt::MouseButton button) {
|
||||
return (button == Qt::LeftButton)
|
||||
&& _call
|
||||
&& (_call->muted() != MuteState::ForceMuted);
|
||||
return (button == Qt::LeftButton) && (_call != nullptr);
|
||||
}) | rpl::start_with_next([=] {
|
||||
_call->setMuted((_call->muted() == MuteState::Muted)
|
||||
? MuteState::Active
|
||||
: MuteState::Muted);
|
||||
if (_call->muted() == MuteState::ForceMuted) {
|
||||
_mute->shake();
|
||||
} else {
|
||||
_call->setMuted((_call->muted() == MuteState::Muted)
|
||||
? MuteState::Active
|
||||
: MuteState::Muted);
|
||||
}
|
||||
}, _mute->lifetime());
|
||||
|
||||
_hangup->setClickedCallback([=] { hangup(false); });
|
||||
_hangup->setClickedCallback([=] { endCall(); });
|
||||
_settings->setClickedCallback([=] {
|
||||
if (_call) {
|
||||
_layerBg->showBox(Box(GroupCallSettingsBox, _call));
|
||||
@@ -346,7 +468,7 @@ void GroupPanel::initControls() {
|
||||
});
|
||||
|
||||
_settings->setText(tr::lng_menu_settings());
|
||||
_hangup->setText(tr::lng_box_leave());
|
||||
_hangup->setText(tr::lng_group_call_leave());
|
||||
|
||||
_members->desiredHeightValue(
|
||||
) | rpl::start_with_next([=] {
|
||||
@@ -401,14 +523,9 @@ void GroupPanel::initWithCall(GroupCall *call) {
|
||||
}
|
||||
}, _callLifetime);
|
||||
|
||||
using namespace rpl::mappers;
|
||||
rpl::combine(
|
||||
_call->mutedValue() | MapPushToTalkToActive(),
|
||||
_call->stateValue() | rpl::map(
|
||||
_1 == State::Creating
|
||||
|| _1 == State::Joining
|
||||
|| _1 == State::Connecting
|
||||
)
|
||||
_call->connectingValue()
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](MuteState mute, bool connecting) {
|
||||
_mute->setState(Ui::CallMuteButtonState{
|
||||
@@ -419,6 +536,13 @@ void GroupPanel::initWithCall(GroupCall *call) {
|
||||
: mute == MuteState::Muted
|
||||
? tr::lng_group_call_unmute(tr::now)
|
||||
: tr::lng_group_call_you_are_live(tr::now)),
|
||||
.subtext = (connecting
|
||||
? QString()
|
||||
: mute == MuteState::ForceMuted
|
||||
? tr::lng_group_call_force_muted_sub(tr::now)
|
||||
: mute == MuteState::Muted
|
||||
? tr::lng_group_call_unmute_sub(tr::now)
|
||||
: QString()),
|
||||
.type = (connecting
|
||||
? Ui::CallMuteButtonType::Connecting
|
||||
: mute == MuteState::ForceMuted
|
||||
@@ -442,52 +566,131 @@ void GroupPanel::addMembers() {
|
||||
alreadyIn.emplace(_peer->session().user());
|
||||
auto controller = std::make_unique<InviteController>(
|
||||
_peer,
|
||||
std::move(alreadyIn),
|
||||
real->fullCount());
|
||||
alreadyIn);
|
||||
controller->setStyleOverrides(
|
||||
&st::groupCallInviteMembersList,
|
||||
&st::groupCallMultiSelect);
|
||||
|
||||
const auto weak = base::make_weak(_call);
|
||||
auto initBox = [=, controller = controller.get()](
|
||||
not_null<PeerListBox*> box) {
|
||||
box->addButton(tr::lng_group_call_invite_button(), [=] {
|
||||
if (const auto call = weak.get()) {
|
||||
const auto result = controller->inviteSelectedUsers(box, call);
|
||||
auto contactsController = std::make_unique<InviteContactsController>(
|
||||
_peer,
|
||||
std::move(alreadyIn),
|
||||
controller->peersWithRows(),
|
||||
controller->rowAdded());
|
||||
contactsController->setStyleOverrides(
|
||||
&st::groupCallInviteMembersList,
|
||||
&st::groupCallMultiSelect);
|
||||
|
||||
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
|
||||
Ui::Toast::Show(
|
||||
widget(),
|
||||
Ui::Toast::Config{
|
||||
.text = tr::lng_group_call_invite_done_user(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold((*user)->firstName),
|
||||
Ui::Text::WithEntities),
|
||||
.st = &st::defaultToast,
|
||||
});
|
||||
} else if (const auto count = std::get_if<int>(&result)) {
|
||||
if (*count > 0) {
|
||||
Ui::Toast::Show(
|
||||
widget(),
|
||||
Ui::Toast::Config{
|
||||
.text = tr::lng_group_call_invite_done_many(
|
||||
tr::now,
|
||||
lt_count,
|
||||
*count,
|
||||
Ui::Text::RichLangValue),
|
||||
.st = &st::defaultToast,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Unexpected("Result in GroupCall::inviteUsers.");
|
||||
}
|
||||
const auto weak = base::make_weak(_call);
|
||||
const auto invite = [=](const std::vector<not_null<UserData*>> &users) {
|
||||
const auto call = weak.get();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
const auto result = call->inviteUsers(users);
|
||||
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
|
||||
Ui::Toast::Show(
|
||||
widget(),
|
||||
Ui::Toast::Config{
|
||||
.text = tr::lng_group_call_invite_done_user(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold((*user)->firstName),
|
||||
Ui::Text::WithEntities),
|
||||
.st = &st::defaultToast,
|
||||
});
|
||||
} else if (const auto count = std::get_if<int>(&result)) {
|
||||
if (*count > 0) {
|
||||
Ui::Toast::Show(
|
||||
widget(),
|
||||
Ui::Toast::Config{
|
||||
.text = tr::lng_group_call_invite_done_many(
|
||||
tr::now,
|
||||
lt_count,
|
||||
*count,
|
||||
Ui::Text::RichLangValue),
|
||||
.st = &st::defaultToast,
|
||||
});
|
||||
}
|
||||
box->closeBox();
|
||||
} else {
|
||||
Unexpected("Result in GroupCall::inviteUsers.");
|
||||
}
|
||||
};
|
||||
const auto inviteWithAdd = [=](
|
||||
const std::vector<not_null<UserData*>> &users,
|
||||
const std::vector<not_null<UserData*>> &nonMembers,
|
||||
Fn<void()> finish) {
|
||||
_peer->session().api().addChatParticipants(
|
||||
_peer,
|
||||
nonMembers,
|
||||
[=](bool) { invite(users); finish(); });
|
||||
};
|
||||
const auto inviteWithConfirmation = [=](
|
||||
const std::vector<not_null<UserData*>> &users,
|
||||
const std::vector<not_null<UserData*>> &nonMembers,
|
||||
Fn<void()> finish) {
|
||||
if (nonMembers.empty()) {
|
||||
invite(users);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const auto name = _peer->name;
|
||||
const auto text = (nonMembers.size() == 1)
|
||||
? tr::lng_group_call_add_to_group_one(
|
||||
tr::now,
|
||||
lt_user,
|
||||
nonMembers.front()->shortName(),
|
||||
lt_group,
|
||||
name)
|
||||
: (nonMembers.size() < users.size())
|
||||
? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name)
|
||||
: tr::lng_group_call_add_to_group_all(tr::now, lt_group, name);
|
||||
const auto shared = std::make_shared<QPointer<Ui::GenericBox>>();
|
||||
const auto finishWithConfirm = [=] {
|
||||
if (*shared) {
|
||||
(*shared)->closeBox();
|
||||
}
|
||||
finish();
|
||||
};
|
||||
auto box = Box(
|
||||
GroupCallConfirmBox,
|
||||
text,
|
||||
tr::lng_participant_invite(),
|
||||
[=] { inviteWithAdd(users, nonMembers, finishWithConfirm); });
|
||||
*shared = box.data();
|
||||
_layerBg->showBox(std::move(box));
|
||||
};
|
||||
auto initBox = [=, controller = controller.get()](
|
||||
not_null<PeerListsBox*> box) {
|
||||
box->setTitle(tr::lng_group_call_invite_title());
|
||||
box->addButton(tr::lng_group_call_invite_button(), [=] {
|
||||
const auto rows = box->collectSelectedRows();
|
||||
|
||||
const auto users = ranges::view::all(
|
||||
rows
|
||||
) | ranges::view::transform([](not_null<PeerData*> peer) {
|
||||
return not_null<UserData*>(peer->asUser());
|
||||
}) | ranges::to_vector;
|
||||
|
||||
const auto nonMembers = ranges::view::all(
|
||||
users
|
||||
) | ranges::view::filter([&](not_null<UserData*> user) {
|
||||
return !controller->hasRowFor(user);
|
||||
}) | ranges::to_vector;
|
||||
|
||||
const auto finish = [box = Ui::MakeWeak(box)]() {
|
||||
if (box) {
|
||||
box->closeBox();
|
||||
}
|
||||
};
|
||||
inviteWithConfirmation(users, nonMembers, finish);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
};
|
||||
_layerBg->showBox(Box<PeerListBox>(std::move(controller), initBox));
|
||||
|
||||
auto controllers = std::vector<std::unique_ptr<PeerListController>>();
|
||||
controllers.push_back(std::move(controller));
|
||||
controllers.push_back(std::move(contactsController));
|
||||
_layerBg->showBox(Box<PeerListsBox>(std::move(controllers), initBox));
|
||||
}
|
||||
|
||||
void GroupPanel::kickMember(not_null<UserData*> user) {
|
||||
@@ -560,22 +763,22 @@ void GroupPanel::initGeometry() {
|
||||
}
|
||||
|
||||
int GroupPanel::computeMembersListTop() const {
|
||||
#ifdef Q_OS_WIN
|
||||
return st::callTitleButton.height + st::groupCallMembersMargin.top() / 2;
|
||||
#elif defined Q_OS_MAC // Q_OS_WIN
|
||||
return st::groupCallMembersMargin.top() * 2;
|
||||
#else // Q_OS_WIN || Q_OS_MAC
|
||||
return st::groupCallMembersMargin.top();
|
||||
#endif // Q_OS_WIN || Q_OS_MAC
|
||||
if (computeTitleRect().has_value()) {
|
||||
return st::groupCallMembersTop;
|
||||
}
|
||||
return st::groupCallMembersTop
|
||||
- (st::groupCallSubtitleTop - st::groupCallTitleTop);
|
||||
}
|
||||
|
||||
std::optional<QRect> GroupPanel::computeTitleRect() const {
|
||||
#ifdef Q_OS_WIN
|
||||
const auto controls = _controls->geometry();
|
||||
return QRect(0, 0, controls.x(), controls.height());
|
||||
#else // Q_OS_WIN
|
||||
#elif defined Q_OS_MAC // Q_OS_WIN
|
||||
return QRect(70, 0, widget()->width() - 70, 28);
|
||||
#else // Q_OS_WIN || Q_OS_MAC
|
||||
return std::nullopt;
|
||||
#endif // Q_OS_WIN
|
||||
#endif // Q_OS_WIN || Q_OS_MAC
|
||||
}
|
||||
|
||||
void GroupPanel::updateControlsGeometry() {
|
||||
@@ -620,12 +823,13 @@ void GroupPanel::refreshTitle() {
|
||||
_title.create(
|
||||
widget(),
|
||||
Info::Profile::NameValue(_peer),
|
||||
st::groupCallHeaderLabel);
|
||||
st::groupCallTitleLabel);
|
||||
_title->show();
|
||||
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
const auto best = _title->naturalWidth();
|
||||
const auto from = (widget()->width() - best) / 2;
|
||||
const auto top = (computeMembersListTop() - _title->height()) / 2;
|
||||
const auto top = st::groupCallTitleTop;
|
||||
const auto left = titleRect->x();
|
||||
if (from >= left && from + best <= left + titleRect->width()) {
|
||||
_title->resizeToWidth(best);
|
||||
@@ -643,6 +847,25 @@ void GroupPanel::refreshTitle() {
|
||||
} else if (_title) {
|
||||
_title.destroy();
|
||||
}
|
||||
if (!_subtitle) {
|
||||
_subtitle.create(
|
||||
widget(),
|
||||
tr::lng_group_call_members(
|
||||
lt_count_decimal,
|
||||
_members->fullCountValue() | tr::to_count()),
|
||||
st::groupCallSubtitleLabel);
|
||||
_subtitle->show();
|
||||
_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
const auto middle = _title
|
||||
? (_title->x() + _title->width() / 2)
|
||||
: (widget()->width() / 2);
|
||||
const auto top = _title
|
||||
? st::groupCallSubtitleTop
|
||||
: st::groupCallTitleTop;
|
||||
_subtitle->moveToLeft(
|
||||
(widget()->width() - _subtitle->width()) / 2,
|
||||
top);
|
||||
}
|
||||
|
||||
void GroupPanel::paint(QRect clip) {
|
||||
|
||||
@@ -68,6 +68,8 @@ public:
|
||||
~GroupPanel();
|
||||
|
||||
[[nodiscard]] bool isActive() const;
|
||||
void minimize();
|
||||
void close();
|
||||
void showAndActivate();
|
||||
void closeBeforeDestroy();
|
||||
|
||||
@@ -90,7 +92,7 @@ private:
|
||||
void updateControlsGeometry();
|
||||
void showControls();
|
||||
|
||||
void hangup(bool discardCallChecked);
|
||||
void endCall();
|
||||
|
||||
void addMembers();
|
||||
void kickMember(not_null<UserData*> user);
|
||||
@@ -115,6 +117,7 @@ private:
|
||||
rpl::lifetime _callLifetime;
|
||||
|
||||
object_ptr<Ui::FlatLabel> _title = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _subtitle = { nullptr };
|
||||
object_ptr<GroupMembers> _members;
|
||||
|
||||
object_ptr<Ui::CallButton> _settings;
|
||||
|
||||
@@ -98,35 +98,45 @@ void Instance::groupCallFailed(not_null<GroupCall*> call) {
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::playSound(Sound sound) {
|
||||
switch (sound) {
|
||||
case Sound::Busy: {
|
||||
if (!_callBusyTrack) {
|
||||
_callBusyTrack = Media::Audio::Current().createTrack();
|
||||
_callBusyTrack->fillFromFile(
|
||||
Core::App().settings().getSoundPath(qsl("call_busy")));
|
||||
}
|
||||
_callBusyTrack->playOnce();
|
||||
} break;
|
||||
|
||||
case Sound::Ended: {
|
||||
if (!_callEndedTrack) {
|
||||
_callEndedTrack = Media::Audio::Current().createTrack();
|
||||
_callEndedTrack->fillFromFile(
|
||||
Core::App().settings().getSoundPath(qsl("call_end")));
|
||||
}
|
||||
_callEndedTrack->playOnce();
|
||||
} break;
|
||||
|
||||
case Sound::Connecting: {
|
||||
if (!_callConnectingTrack) {
|
||||
_callConnectingTrack = Media::Audio::Current().createTrack();
|
||||
_callConnectingTrack->fillFromFile(
|
||||
Core::App().settings().getSoundPath(qsl("call_connect")));
|
||||
}
|
||||
_callConnectingTrack->playOnce();
|
||||
} break;
|
||||
not_null<Media::Audio::Track*> Instance::ensureSoundLoaded(
|
||||
const QString &key) {
|
||||
const auto i = _tracks.find(key);
|
||||
if (i != end(_tracks)) {
|
||||
return i->second.get();
|
||||
}
|
||||
const auto result = _tracks.emplace(
|
||||
key,
|
||||
Media::Audio::Current().createTrack()).first->second.get();
|
||||
result->fillFromFile(Core::App().settings().getSoundPath(key));
|
||||
return result;
|
||||
}
|
||||
|
||||
void Instance::playSoundOnce(const QString &key) {
|
||||
ensureSoundLoaded(key)->playOnce();
|
||||
}
|
||||
|
||||
void Instance::callPlaySound(CallSound sound) {
|
||||
playSoundOnce([&] {
|
||||
switch (sound) {
|
||||
case CallSound::Busy: return "call_busy";
|
||||
case CallSound::Ended: return "call_end";
|
||||
case CallSound::Connecting: return "call_connect";
|
||||
}
|
||||
Unexpected("CallSound in Instance::callPlaySound.");
|
||||
return "";
|
||||
}());
|
||||
}
|
||||
|
||||
void Instance::groupCallPlaySound(GroupCallSound sound) {
|
||||
playSoundOnce([&] {
|
||||
switch (sound) {
|
||||
case GroupCallSound::Started: return "group_call_start";
|
||||
case GroupCallSound::Ended: return "group_call_end";
|
||||
case GroupCallSound::Connecting: return "group_call_connect";
|
||||
}
|
||||
Unexpected("GroupCallSound in Instance::groupCallPlaySound.");
|
||||
return "";
|
||||
}());
|
||||
}
|
||||
|
||||
void Instance::destroyCall(not_null<Call*> call) {
|
||||
@@ -476,6 +486,25 @@ bool Instance::activateCurrentCall() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Instance::minimizeCurrentActiveCall() {
|
||||
if (inCall() && _currentCallPanel->isActive()) {
|
||||
_currentCallPanel->minimize();
|
||||
return true;
|
||||
} else if (inGroupCall() && _currentGroupCallPanel->isActive()) {
|
||||
_currentGroupCallPanel->minimize();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Instance::closeCurrentActiveCall() {
|
||||
if (inGroupCall() && _currentGroupCallPanel->isActive()) {
|
||||
_currentGroupCallPanel->close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Call *Instance::currentCall() const {
|
||||
return _currentCall.get();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ public:
|
||||
[[nodiscard]] bool hasActivePanel(
|
||||
not_null<Main::Session*> session) const;
|
||||
bool activateCurrentCall();
|
||||
bool minimizeCurrentActiveCall();
|
||||
bool closeCurrentActiveCall();
|
||||
auto getVideoCapture()
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;
|
||||
void requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);
|
||||
@@ -64,6 +66,9 @@ public:
|
||||
[[nodiscard]] bool isQuitPrevent();
|
||||
|
||||
private:
|
||||
using CallSound = Call::Delegate::CallSound;
|
||||
using GroupCallSound = GroupCall::Delegate::GroupCallSound;
|
||||
|
||||
[[nodiscard]] not_null<Call::Delegate*> getCallDelegate() {
|
||||
return static_cast<Call::Delegate*>(this);
|
||||
}
|
||||
@@ -73,6 +78,10 @@ private:
|
||||
[[nodiscard]] DhConfig getDhConfig() const override {
|
||||
return _dhConfig;
|
||||
}
|
||||
|
||||
not_null<Media::Audio::Track*> ensureSoundLoaded(const QString &key);
|
||||
void playSoundOnce(const QString &key);
|
||||
|
||||
void callFinished(not_null<Call*> call) override;
|
||||
void callFailed(not_null<Call*> call) override;
|
||||
void callRedial(not_null<Call*> call) override;
|
||||
@@ -81,15 +90,15 @@ private:
|
||||
bool video) override {
|
||||
requestPermissionsOrFail(std::move(onSuccess), video);
|
||||
}
|
||||
void callPlaySound(CallSound sound) override;
|
||||
|
||||
void groupCallFinished(not_null<GroupCall*> call) override;
|
||||
void groupCallFailed(not_null<GroupCall*> call) override;
|
||||
void groupCallRequestPermissionsOrFail(Fn<void()> onSuccess) override {
|
||||
requestPermissionsOrFail(std::move(onSuccess), false);
|
||||
}
|
||||
void groupCallPlaySound(GroupCallSound sound) override;
|
||||
|
||||
using Sound = Call::Delegate::Sound;
|
||||
void playSound(Sound sound) override;
|
||||
void createCall(not_null<UserData*> user, Call::Type type, bool video);
|
||||
void destroyCall(not_null<Call*> call);
|
||||
|
||||
@@ -98,7 +107,9 @@ private:
|
||||
const MTPInputGroupCall &inputCall);
|
||||
void destroyGroupCall(not_null<GroupCall*> call);
|
||||
|
||||
void requestPermissionOrFail(Platform::PermissionType type, Fn<void()> onSuccess);
|
||||
void requestPermissionOrFail(
|
||||
Platform::PermissionType type,
|
||||
Fn<void()> onSuccess);
|
||||
|
||||
void refreshDhConfig();
|
||||
void refreshServerConfig(not_null<Main::Session*> session);
|
||||
@@ -132,9 +143,7 @@ private:
|
||||
rpl::event_stream<GroupCall*> _currentGroupCallChanges;
|
||||
std::unique_ptr<GroupPanel> _currentGroupCallPanel;
|
||||
|
||||
std::unique_ptr<Media::Audio::Track> _callConnectingTrack;
|
||||
std::unique_ptr<Media::Audio::Track> _callEndedTrack;
|
||||
std::unique_ptr<Media::Audio::Track> _callBusyTrack;
|
||||
base::flat_map<QString, std::unique_ptr<Media::Audio::Track>> _tracks;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -221,6 +221,10 @@ void Panel::showAndActivate() {
|
||||
_window->setFocus();
|
||||
}
|
||||
|
||||
void Panel::minimize() {
|
||||
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
|
||||
}
|
||||
|
||||
void Panel::replaceCall(not_null<Call*> call) {
|
||||
reinitWithCall(call);
|
||||
updateControlsGeometry();
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
|
||||
[[nodiscard]] bool isActive() const;
|
||||
void showAndActivate();
|
||||
void minimize();
|
||||
void replaceCall(not_null<Call*> call);
|
||||
void closeBeforeDestroy();
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/paint/blobs_linear.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/chat/group_call_userpics.h" // Ui::GroupCallUser.
|
||||
#include "ui/chat/group_call_bar.h" // Ui::GroupCallBarContent.
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "core/application.h"
|
||||
#include "calls/calls_call.h"
|
||||
@@ -31,9 +33,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/timer.h"
|
||||
#include "app.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h" // style::GroupCallUserpics
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Calls {
|
||||
|
||||
enum class BarState {
|
||||
Connecting,
|
||||
Active,
|
||||
Muted,
|
||||
ForceMuted,
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxUsersInBar = 3;
|
||||
@@ -46,6 +57,16 @@ constexpr auto kHideBlobsDuration = crl::time(500);
|
||||
constexpr auto kBlobLevelDuration = crl::time(250);
|
||||
constexpr auto kBlobUpdateInterval = crl::time(100);
|
||||
|
||||
auto BarStateFromMuteState(MuteState state, bool connecting) {
|
||||
return (connecting
|
||||
? BarState::Connecting
|
||||
: state == MuteState::ForceMuted
|
||||
? BarState::ForceMuted
|
||||
: state == MuteState::Muted
|
||||
? BarState::Muted
|
||||
: BarState::Active);
|
||||
};
|
||||
|
||||
auto LinearBlobs() {
|
||||
return std::vector<Ui::Paint::LinearBlobs::BlobData>{
|
||||
{
|
||||
@@ -77,18 +98,26 @@ auto LinearBlobs() {
|
||||
|
||||
auto Colors() {
|
||||
using Vector = std::vector<QColor>;
|
||||
return base::flat_map<MuteState, Vector>{
|
||||
using Colors = anim::gradient_colors;
|
||||
return base::flat_map<BarState, Colors>{
|
||||
{
|
||||
MuteState::ForceMuted,
|
||||
Vector{ st::groupCallForceMuted1->c, st::groupCallForceMuted2->c }
|
||||
BarState::ForceMuted,
|
||||
Colors(QGradientStops{
|
||||
{ 0.0, st::groupCallForceMutedBar1->c },
|
||||
{ .35, st::groupCallForceMutedBar2->c },
|
||||
{ 1.0, st::groupCallForceMutedBar3->c } })
|
||||
},
|
||||
{
|
||||
MuteState::Active,
|
||||
Vector{ st::groupCallLive1->c, st::groupCallLive2->c }
|
||||
BarState::Active,
|
||||
Colors(Vector{ st::groupCallLive1->c, st::groupCallLive2->c })
|
||||
},
|
||||
{
|
||||
MuteState::Muted,
|
||||
Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c }
|
||||
BarState::Muted,
|
||||
Colors(Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c })
|
||||
},
|
||||
{
|
||||
BarState::Connecting,
|
||||
Colors(st::callBarBgMuted->c)
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -138,7 +167,7 @@ void DebugInfoBox::updateText() {
|
||||
} // namespace
|
||||
|
||||
struct TopBar::User {
|
||||
Ui::GroupCallBarContent::User data;
|
||||
Ui::GroupCallUser data;
|
||||
};
|
||||
|
||||
class Mute final : public Ui::IconButton {
|
||||
@@ -211,6 +240,12 @@ TopBar::TopBar(
|
||||
: RpWidget(parent)
|
||||
, _call(call)
|
||||
, _groupCall(groupCall)
|
||||
, _userpics(call
|
||||
? nullptr
|
||||
: std::make_unique<Ui::GroupCallUserpics>(
|
||||
st::groupCallTopBarUserpics,
|
||||
rpl::single(true),
|
||||
[=] { updateUserpics(); }))
|
||||
, _durationLabel(_call
|
||||
? object_ptr<Ui::LabelSimple>(this, st::callBarLabel)
|
||||
: object_ptr<Ui::LabelSimple>(nullptr))
|
||||
@@ -239,7 +274,9 @@ void TopBar::initControls() {
|
||||
if (const auto call = _call.get()) {
|
||||
call->setMuted(!call->muted());
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
if (group->muted() != MuteState::ForceMuted) {
|
||||
if (group->muted() == MuteState::ForceMuted) {
|
||||
Ui::Toast::Show(tr::lng_group_call_force_muted_sub(tr::now));
|
||||
} else {
|
||||
group->setMuted((group->muted() == MuteState::Muted)
|
||||
? MuteState::Active
|
||||
: MuteState::Muted);
|
||||
@@ -250,27 +287,36 @@ void TopBar::initControls() {
|
||||
const auto mapToState = [](bool muted) {
|
||||
return muted ? MuteState::Muted : MuteState::Active;
|
||||
};
|
||||
const auto fromState = _mute->lifetime().make_state<MuteState>(
|
||||
_call ? mapToState(_call->muted()) : _groupCall->muted());
|
||||
const auto fromState = _mute->lifetime().make_state<BarState>(
|
||||
BarStateFromMuteState(
|
||||
_call
|
||||
? mapToState(_call->muted())
|
||||
: _groupCall->muted(),
|
||||
false));
|
||||
auto muted = _call
|
||||
? _call->mutedValue() | rpl::map(mapToState)
|
||||
: (_groupCall->mutedValue()
|
||||
| MapPushToTalkToActive()
|
||||
| rpl::distinct_until_changed()
|
||||
| rpl::type_erased());
|
||||
? rpl::combine(
|
||||
_call->mutedValue() | rpl::map(mapToState),
|
||||
rpl::single(false)) | rpl::type_erased()
|
||||
: rpl::combine(
|
||||
(_groupCall->mutedValue()
|
||||
| MapPushToTalkToActive()
|
||||
| rpl::distinct_until_changed()
|
||||
| rpl::type_erased()),
|
||||
_groupCall->connectingValue());
|
||||
std::move(
|
||||
muted
|
||||
) | rpl::start_with_next([=](MuteState state) {
|
||||
setMuted(state != MuteState::Active);
|
||||
) | rpl::map(
|
||||
BarStateFromMuteState
|
||||
) | rpl::start_with_next([=](BarState state) {
|
||||
_isGroupConnecting = (state == BarState::Connecting);
|
||||
setMuted(state != BarState::Active);
|
||||
update();
|
||||
|
||||
const auto isForceMuted = (state == MuteState::ForceMuted);
|
||||
const auto isForceMuted = (state == BarState::ForceMuted);
|
||||
if (isForceMuted) {
|
||||
_mute->clearState();
|
||||
}
|
||||
_mute->setAttribute(
|
||||
Qt::WA_TransparentForMouseEvents,
|
||||
isForceMuted);
|
||||
_mute->setPointerCursor(!isForceMuted);
|
||||
|
||||
const auto to = 1.;
|
||||
const auto from = _switchStateAnimation.animating()
|
||||
@@ -280,8 +326,8 @@ void TopBar::initControls() {
|
||||
const auto toMuted = state;
|
||||
*fromState = state;
|
||||
|
||||
const auto crossFrom = (fromMuted != MuteState::Active) ? 1. : 0.;
|
||||
const auto crossTo = (toMuted != MuteState::Active) ? 1. : 0.;
|
||||
const auto crossFrom = (fromMuted != BarState::Active) ? 1. : 0.;
|
||||
const auto crossTo = (toMuted != BarState::Active) ? 1. : 0.;
|
||||
|
||||
auto animationCallback = [=](float64 value) {
|
||||
if (_groupCall) {
|
||||
@@ -307,6 +353,14 @@ void TopBar::initControls() {
|
||||
|
||||
if (const auto group = _groupCall.get()) {
|
||||
subscribeToMembersChanges(group);
|
||||
|
||||
_isGroupConnecting.value(
|
||||
) | rpl::start_with_next([=](bool isConnecting) {
|
||||
_mute->setAttribute(
|
||||
Qt::WA_TransparentForMouseEvents,
|
||||
isConnecting);
|
||||
updateInfoLabels();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
if (const auto call = _call.get()) {
|
||||
@@ -337,11 +391,15 @@ void TopBar::initControls() {
|
||||
if (const auto call = _call.get()) {
|
||||
call->hangup();
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
Ui::show(Box(
|
||||
LeaveGroupCallBox,
|
||||
group,
|
||||
false,
|
||||
BoxContext::MainWindow));
|
||||
if (!group->peer()->canManageGroupCall()) {
|
||||
group->hangup();
|
||||
} else {
|
||||
Ui::show(Box(
|
||||
LeaveGroupCallBox,
|
||||
group,
|
||||
false,
|
||||
BoxContext::MainWindow));
|
||||
}
|
||||
}
|
||||
});
|
||||
updateDurationText();
|
||||
@@ -406,23 +464,14 @@ void TopBar::initBlobsUnder(
|
||||
auto hideBlobs = rpl::combine(
|
||||
rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
|
||||
Core::App().appDeactivatedValue(),
|
||||
group->stateValue(
|
||||
) | rpl::map([](Calls::GroupCall::State state) {
|
||||
using State = Calls::GroupCall::State;
|
||||
if (state != State::Creating
|
||||
&& state != State::Joining
|
||||
&& state != State::Joined
|
||||
&& state != State::Connecting) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}) | rpl::distinct_until_changed()
|
||||
) | rpl::map([](bool animDisabled, bool hide, bool isBadState) {
|
||||
return isBadState || animDisabled || hide;
|
||||
group->connectingValue()
|
||||
) | rpl::map([](bool animDisabled, bool hide, bool connecting) {
|
||||
return connecting || animDisabled || hide;
|
||||
});
|
||||
|
||||
std::move(
|
||||
hideBlobs
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](bool hide) {
|
||||
if (hide) {
|
||||
state->paint.setLevel(0.);
|
||||
@@ -510,35 +559,31 @@ void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
|
||||
) | rpl::map([=](not_null<Data::GroupCall*> real) {
|
||||
return HistoryView::GroupCallTracker::ContentByCall(
|
||||
real,
|
||||
HistoryView::UserpicsInRowStyle{
|
||||
.size = st::groupCallTopBarUserpicSize,
|
||||
.shift = st::groupCallTopBarUserpicShift,
|
||||
.stroke = st::groupCallTopBarUserpicStroke,
|
||||
});
|
||||
st::groupCallTopBarUserpics.size);
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::filter([=](const Ui::GroupCallBarContent &content) {
|
||||
if (_users.size() != content.users.size()) {
|
||||
return true;
|
||||
}
|
||||
for (auto i = 0, count = int(_users.size()); i != count; ++i) {
|
||||
if (_users[i].data.userpicKey != content.users[i].userpicKey
|
||||
|| _users[i].data.id != content.users[i].id) {
|
||||
if (_users[i].userpicKey != content.users[i].userpicKey
|
||||
|| _users[i].id != content.users[i].id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) | rpl::start_with_next([=](const Ui::GroupCallBarContent &content) {
|
||||
const auto sizeChanged = (_users.size() != content.users.size());
|
||||
_users = ranges::view::all(
|
||||
content.users
|
||||
) | ranges::view::transform([](const auto &user) {
|
||||
return User{ user };
|
||||
}) | ranges::to_vector;
|
||||
generateUserpicsInRow();
|
||||
if (sizeChanged) {
|
||||
updateControlsGeometry();
|
||||
_users = content.users;
|
||||
for (auto &user : _users) {
|
||||
user.speaking = false;
|
||||
}
|
||||
update();
|
||||
_userpics->update(_users, !isHidden());
|
||||
}, lifetime());
|
||||
|
||||
_userpics->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
_userpicsWidth = width;
|
||||
updateControlsGeometry();
|
||||
}, lifetime());
|
||||
|
||||
call->peer()->session().changes().peerUpdates(
|
||||
@@ -550,41 +595,10 @@ void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
|
||||
}) | rpl::start_with_next([=] {
|
||||
updateInfoLabels();
|
||||
}, lifetime());
|
||||
|
||||
}
|
||||
|
||||
void TopBar::generateUserpicsInRow() {
|
||||
const auto count = int(_users.size());
|
||||
if (!count) {
|
||||
_userpics = QImage();
|
||||
return;
|
||||
}
|
||||
const auto limit = std::min(count, kMaxUsersInBar);
|
||||
const auto single = st::groupCallTopBarUserpicSize;
|
||||
const auto shift = st::groupCallTopBarUserpicShift;
|
||||
const auto width = single + (limit - 1) * (single - shift);
|
||||
if (_userpics.width() != width * cIntRetinaFactor()) {
|
||||
_userpics = QImage(
|
||||
QSize(width, single) * cIntRetinaFactor(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
_userpics.fill(Qt::transparent);
|
||||
_userpics.setDevicePixelRatio(cRetinaFactor());
|
||||
|
||||
auto q = Painter(&_userpics);
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(st::groupCallTopBarUserpicStroke);
|
||||
auto x = (count - 1) * (single - shift);
|
||||
for (auto i = count; i != 0;) {
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
q.drawImage(x, 0, _users[--i].data.userpic);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.setPen(pen);
|
||||
q.drawEllipse(x, 0, single, single);
|
||||
x -= single - shift;
|
||||
}
|
||||
void TopBar::updateUserpics() {
|
||||
update(_mute->width(), 0, _userpics->maxWidth(), height());
|
||||
}
|
||||
|
||||
void TopBar::updateInfoLabels() {
|
||||
@@ -602,8 +616,11 @@ void TopBar::setInfoLabels() {
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
const auto peer = group->peer();
|
||||
const auto name = peer->name;
|
||||
_fullInfoLabel->setText(name.toUpper());
|
||||
_shortInfoLabel->setText(name.toUpper());
|
||||
const auto text = _isGroupConnecting.current()
|
||||
? tr::lng_group_call_connecting(tr::now)
|
||||
: name.toUpper();
|
||||
_fullInfoLabel->setText(text);
|
||||
_shortInfoLabel->setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,8 +661,13 @@ void TopBar::updateControlsGeometry() {
|
||||
_durationLabel->moveToLeft(left, st::callBarLabelTop);
|
||||
left += _durationLabel->width() + st::callBarSkip;
|
||||
}
|
||||
if (!_userpics.isNull()) {
|
||||
left += _userpics.width() / _userpics.devicePixelRatio();
|
||||
if (_userpicsWidth) {
|
||||
const auto single = st::groupCallTopBarUserpics.size;
|
||||
const auto skip = anim::interpolate(
|
||||
0,
|
||||
st::callBarSkip,
|
||||
std::min(_userpicsWidth, single) / float64(single));
|
||||
left += _userpicsWidth + skip;
|
||||
}
|
||||
if (_signalBars) {
|
||||
_signalBars->moveToLeft(left, (height() - _signalBars->height()) / 2);
|
||||
@@ -697,11 +719,10 @@ void TopBar::paintEvent(QPaintEvent *e) {
|
||||
: (_muted ? st::callBarBgMuted : st::callBarBg);
|
||||
p.fillRect(e->rect(), std::move(brush));
|
||||
|
||||
if (!_userpics.isNull()) {
|
||||
const auto imageSize = _userpics.size()
|
||||
/ _userpics.devicePixelRatio();
|
||||
const auto top = (height() - imageSize.height()) / 2;
|
||||
p.drawImage(_mute->width(), top, _userpics);
|
||||
if (_userpicsWidth) {
|
||||
const auto size = st::groupCallTopBarUserpics.size;
|
||||
const auto top = (height() - size) / 2;
|
||||
_userpics->paint(p, _mute->width(), top, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ class IconButton;
|
||||
class AbstractButton;
|
||||
class LabelSimple;
|
||||
class FlatLabel;
|
||||
struct GroupCallUser;
|
||||
class GroupCallUserpics;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Main {
|
||||
@@ -33,6 +35,7 @@ class GroupCall;
|
||||
class SignalBars;
|
||||
class Mute;
|
||||
enum class MuteState;
|
||||
enum class BarState;
|
||||
|
||||
class TopBar : public Ui::RpWidget {
|
||||
public:
|
||||
@@ -65,14 +68,15 @@ private:
|
||||
void setMuted(bool mute);
|
||||
|
||||
void subscribeToMembersChanges(not_null<GroupCall*> call);
|
||||
void generateUserpicsInRow();
|
||||
void updateUserpics();
|
||||
|
||||
const base::weak_ptr<Call> _call;
|
||||
const base::weak_ptr<GroupCall> _groupCall;
|
||||
|
||||
bool _muted = false;
|
||||
std::vector<User> _users;
|
||||
QImage _userpics;
|
||||
std::vector<Ui::GroupCallUser> _users;
|
||||
std::unique_ptr<Ui::GroupCallUserpics> _userpics;
|
||||
int _userpicsWidth = 0;
|
||||
object_ptr<Ui::LabelSimple> _durationLabel;
|
||||
object_ptr<SignalBars> _signalBars;
|
||||
object_ptr<Ui::FlatLabel> _fullInfoLabel;
|
||||
@@ -83,8 +87,10 @@ private:
|
||||
object_ptr<Ui::IconButton> _hangup;
|
||||
base::unique_qptr<Ui::RpWidget> _blobs;
|
||||
|
||||
rpl::variable<bool> _isGroupConnecting = false;
|
||||
|
||||
QBrush _groupBrush;
|
||||
anim::linear_gradients<MuteState> _gradients;
|
||||
anim::linear_gradients<BarState> _gradients;
|
||||
Ui::Animations::Simple _switchStateAnimation;
|
||||
|
||||
base::Timer _updateDurationTimer;
|
||||
|
||||
@@ -915,18 +915,25 @@ bool Application::closeActiveWindow() {
|
||||
if (hideMediaView()) {
|
||||
return true;
|
||||
}
|
||||
if (const auto window = activeWindow()) {
|
||||
window->close();
|
||||
return true;
|
||||
if (!calls().closeCurrentActiveCall()) {
|
||||
if (const auto window = activeWindow()) {
|
||||
if (window->widget()->isVisible()
|
||||
&& window->widget()->isActive()) {
|
||||
window->close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Application::minimizeActiveWindow() {
|
||||
hideMediaView();
|
||||
if (const auto window = activeWindow()) {
|
||||
window->minimize();
|
||||
return true;
|
||||
if (!calls().minimizeCurrentActiveCall()) {
|
||||
if (const auto window = activeWindow()) {
|
||||
window->minimize();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -21,60 +21,6 @@ namespace {
|
||||
|
||||
std::map<int, const char*> BetaLogs() {
|
||||
return {
|
||||
{
|
||||
2001008,
|
||||
"- Add support for full group message history export.\n"
|
||||
|
||||
"- Allow export of a single chat message history in JSON format."
|
||||
},
|
||||
{
|
||||
2001014,
|
||||
"- Support for multiple accounts."
|
||||
},
|
||||
{
|
||||
2001017,
|
||||
"- Fix messages editing in a non-active account.\n"
|
||||
|
||||
"- Fix large animated emoji messages editing.\n"
|
||||
|
||||
"- Fix high definition GIF animations opening in media viewer.\n"
|
||||
|
||||
"- Multiple crash fixes."
|
||||
},
|
||||
{
|
||||
2001018,
|
||||
"- Fix a possible crash in Picture-in-Picture video player.\n"
|
||||
|
||||
"- Fix copying links from message texts.\n"
|
||||
|
||||
"- Raise file size limit to 2000 MB.\n"
|
||||
|
||||
"- Allow using system window frame in Windows and Linux."
|
||||
},
|
||||
{
|
||||
2001019,
|
||||
"- File uploading in an inactive account correctly finishes.\n"
|
||||
|
||||
"- Stickers panel works correctly after switching between accounts.\n"
|
||||
|
||||
"- Large .webp files are not shown as stickers.\n"
|
||||
|
||||
"- MacBook TouchBar support was fully rewritten with fixes for multiple accounts.\n"
|
||||
|
||||
"- Custom window title bar works in all Linux versions.\n"
|
||||
|
||||
"- Passcode doesn't auto-lock while you're active in other apps on Linux X11."
|
||||
},
|
||||
{
|
||||
2001021,
|
||||
"- Edit your scheduled messages.\n"
|
||||
|
||||
"- See the unread messages indicator for your additional accounts on the main menu button.\n"
|
||||
|
||||
"- Use Auto-Night Mode to make Telegram night mode match the system Dark Mode settings.\n"
|
||||
|
||||
"- Enjoy dark native window frame for Telegram night mode on Windows.\n"
|
||||
},
|
||||
{
|
||||
2004006,
|
||||
"- Fix image compression option when sending files with drag-n-drop.\n"
|
||||
@@ -119,6 +65,24 @@ std::map<int, const char*> BetaLogs() {
|
||||
|
||||
"- Fix group members display.\n"
|
||||
},
|
||||
{
|
||||
2004015,
|
||||
"- Improve design of voice chats.\n"
|
||||
|
||||
"- Fix sending of voice messages as replies.\n"
|
||||
|
||||
"- Fix 'Open With' menu position in macOS.\n"
|
||||
|
||||
"- Fix freeze on secondary screen disconnect.\n"
|
||||
},
|
||||
{
|
||||
2005002,
|
||||
"- Fix possible crash in video calls.\n"
|
||||
|
||||
"- Fix possible crash in connecting to voice chats.\n"
|
||||
|
||||
"- Use different audio module code on Windows in calls.\n"
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 2004014;
|
||||
constexpr auto AppVersionStr = "2.4.14";
|
||||
constexpr auto AppVersion = 2005002;
|
||||
constexpr auto AppVersionStr = "2.5.2";
|
||||
constexpr auto AppBetaVersion = true;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -33,7 +33,7 @@ GroupCall::GroupCall(
|
||||
uint64 accessHash)
|
||||
: _id(id)
|
||||
, _accessHash(accessHash)
|
||||
, _peer(peer) // #TODO calls migration
|
||||
, _peer(peer)
|
||||
, _speakingByActiveFinishTimer([=] { checkFinishSpeakingByActive(); }) {
|
||||
}
|
||||
|
||||
@@ -312,37 +312,10 @@ void GroupCall::applyParticipantsSlice(
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::applyParticipantsMutes(
|
||||
const MTPDupdateGroupCallParticipants &update) {
|
||||
for (const auto &participant : update.vparticipants().v) {
|
||||
participant.match([&](const MTPDgroupCallParticipant &data) {
|
||||
if (data.is_left()) {
|
||||
return;
|
||||
}
|
||||
const auto userId = data.vuser_id().v;
|
||||
const auto user = _peer->owner().user(userId);
|
||||
const auto i = ranges::find(
|
||||
_participants,
|
||||
user,
|
||||
&Participant::user);
|
||||
if (i != end(_participants)) {
|
||||
const auto was = *i;
|
||||
i->muted = data.is_muted();
|
||||
i->canSelfUnmute = !i->muted || data.is_can_self_unmute();
|
||||
if (!i->canSelfUnmute) {
|
||||
i->speaking = false;
|
||||
_speakingByActiveFinishes.remove(i->user);
|
||||
}
|
||||
_participantUpdates.fire({
|
||||
.was = was,
|
||||
.now = *i,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::applyLastSpoke(uint32 ssrc, crl::time when, crl::time now) {
|
||||
void GroupCall::applyLastSpoke(
|
||||
uint32 ssrc,
|
||||
LastSpokeTimes when,
|
||||
crl::time now) {
|
||||
const auto i = _userBySsrc.find(ssrc);
|
||||
if (i == end(_userBySsrc)) {
|
||||
_unknownSpokenSsrcs[ssrc] = when;
|
||||
@@ -353,10 +326,13 @@ void GroupCall::applyLastSpoke(uint32 ssrc, crl::time when, crl::time now) {
|
||||
Assert(j != end(_participants));
|
||||
|
||||
_speakingByActiveFinishes.remove(j->user);
|
||||
const auto speaking = (when + kSpeakStatusKeptFor >= now)
|
||||
const auto sounding = (when.anything + kSoundStatusKeptFor >= now)
|
||||
&& j->canSelfUnmute;
|
||||
if (j->speaking != speaking) {
|
||||
const auto speaking = sounding
|
||||
&& (when.voice + kSoundStatusKeptFor >= now);
|
||||
if (j->sounding != sounding || j->speaking != speaking) {
|
||||
const auto was = *j;
|
||||
j->sounding = sounding;
|
||||
j->speaking = speaking;
|
||||
_participantUpdates.fire({
|
||||
.was = was,
|
||||
@@ -367,7 +343,7 @@ void GroupCall::applyLastSpoke(uint32 ssrc, crl::time when, crl::time now) {
|
||||
|
||||
void GroupCall::applyActiveUpdate(
|
||||
UserId userId,
|
||||
crl::time when,
|
||||
LastSpokeTimes when,
|
||||
UserData *userLoaded) {
|
||||
if (inCall()) {
|
||||
return;
|
||||
@@ -387,9 +363,9 @@ void GroupCall::applyActiveUpdate(
|
||||
}
|
||||
const auto was = std::make_optional(*i);
|
||||
const auto now = crl::now();
|
||||
const auto elapsed = TimeId((now - when) / crl::time(1000));
|
||||
const auto elapsed = TimeId((now - when.anything) / crl::time(1000));
|
||||
const auto lastActive = base::unixtime::now() - elapsed;
|
||||
const auto finishes = when + kSpeakingAfterActive;
|
||||
const auto finishes = when.anything + kSpeakingAfterActive;
|
||||
if (lastActive <= i->lastActive || finishes <= now) {
|
||||
return;
|
||||
}
|
||||
@@ -450,7 +426,7 @@ void GroupCall::requestUnknownParticipants() {
|
||||
if (_unknownSpokenSsrcs.size() < kRequestPerPage) {
|
||||
return base::take(_unknownSpokenSsrcs);
|
||||
}
|
||||
auto result = base::flat_map<uint32, crl::time>();
|
||||
auto result = base::flat_map<uint32, LastSpokeTimes>();
|
||||
result.reserve(kRequestPerPage);
|
||||
while (result.size() < kRequestPerPage) {
|
||||
const auto [ssrc, when] = _unknownSpokenSsrcs.back();
|
||||
@@ -463,7 +439,7 @@ void GroupCall::requestUnknownParticipants() {
|
||||
if (_unknownSpokenUids.size() + ssrcs.size() < kRequestPerPage) {
|
||||
return base::take(_unknownSpokenUids);
|
||||
}
|
||||
auto result = base::flat_map<UserId, crl::time>();
|
||||
auto result = base::flat_map<UserId, LastSpokeTimes>();
|
||||
const auto available = (kRequestPerPage - int(ssrcs.size()));
|
||||
if (available > 0) {
|
||||
result.reserve(available);
|
||||
@@ -557,17 +533,29 @@ bool GroupCall::inCall() const {
|
||||
|
||||
void GroupCall::applyUpdate(const MTPDupdateGroupCallParticipants &update) {
|
||||
const auto version = update.vversion().v;
|
||||
if (version < _version) {
|
||||
return;
|
||||
} else if (version == _version) {
|
||||
applyParticipantsMutes(update);
|
||||
return;
|
||||
} else if (version != _version + 1) {
|
||||
applyParticipantsMutes(update);
|
||||
reload();
|
||||
const auto applyUpdate = [&] {
|
||||
if (version < _version) {
|
||||
return false;
|
||||
}
|
||||
auto versionShouldIncrement = false;
|
||||
for (const auto &participant : update.vparticipants().v) {
|
||||
const auto versioned = participant.match([&](
|
||||
const MTPDgroupCallParticipant &data) {
|
||||
return data.is_versioned();
|
||||
});
|
||||
if (versioned) {
|
||||
versionShouldIncrement = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return versionShouldIncrement
|
||||
? (version == _version + 1)
|
||||
: (version == _version);
|
||||
}();
|
||||
if (!applyUpdate) {
|
||||
return;
|
||||
}
|
||||
_version = update.vversion().v;
|
||||
_version = version;
|
||||
applyUpdateChecked(update);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ class ApiWrap;
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct LastSpokeTimes {
|
||||
crl::time anything = 0;
|
||||
crl::time voice = 0;
|
||||
};
|
||||
|
||||
class GroupCall final {
|
||||
public:
|
||||
GroupCall(not_null<PeerData*> peer, uint64 id, uint64 accessHash);
|
||||
@@ -32,6 +37,7 @@ public:
|
||||
TimeId date = 0;
|
||||
TimeId lastActive = 0;
|
||||
uint32 ssrc = 0;
|
||||
bool sounding = false;
|
||||
bool speaking = false;
|
||||
bool muted = false;
|
||||
bool canSelfUnmute = false;
|
||||
@@ -41,7 +47,7 @@ public:
|
||||
std::optional<Participant> now;
|
||||
};
|
||||
|
||||
static constexpr auto kSpeakStatusKeptFor = crl::time(350);
|
||||
static constexpr auto kSoundStatusKeptFor = crl::time(350);
|
||||
|
||||
[[nodiscard]] auto participants() const
|
||||
-> const std::vector<Participant> &;
|
||||
@@ -56,10 +62,10 @@ public:
|
||||
void applyUpdate(const MTPDupdateGroupCallParticipants &update);
|
||||
void applyUpdateChecked(
|
||||
const MTPDupdateGroupCallParticipants &update);
|
||||
void applyLastSpoke(uint32 ssrc, crl::time when, crl::time now);
|
||||
void applyLastSpoke(uint32 ssrc, LastSpokeTimes when, crl::time now);
|
||||
void applyActiveUpdate(
|
||||
UserId userId,
|
||||
crl::time when,
|
||||
LastSpokeTimes when,
|
||||
UserData *userLoaded);
|
||||
|
||||
[[nodiscard]] int fullCount() const;
|
||||
@@ -85,8 +91,6 @@ private:
|
||||
void applyParticipantsSlice(
|
||||
const QVector<MTPGroupCallParticipant> &list,
|
||||
ApplySliceSource sliceSource);
|
||||
void applyParticipantsMutes(
|
||||
const MTPDupdateGroupCallParticipants &update);
|
||||
void requestUnknownParticipants();
|
||||
void changePeerEmptyCallFlag();
|
||||
void checkFinishSpeakingByActive();
|
||||
@@ -106,8 +110,8 @@ private:
|
||||
QString _nextOffset;
|
||||
rpl::variable<int> _fullCount = 0;
|
||||
|
||||
base::flat_map<uint32, crl::time> _unknownSpokenSsrcs;
|
||||
base::flat_map<UserId, crl::time> _unknownSpokenUids;
|
||||
base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs;
|
||||
base::flat_map<UserId, LastSpokeTimes> _unknownSpokenUids;
|
||||
mtpRequestId _unknownUsersRequestId = 0;
|
||||
|
||||
rpl::event_stream<ParticipantUpdate> _participantUpdates;
|
||||
|
||||
@@ -814,14 +814,6 @@ void Session::unregisterGroupCall(not_null<GroupCall*> call) {
|
||||
_groupCalls.remove(call->id());
|
||||
}
|
||||
|
||||
rpl::producer<Session::GroupCallDiscard> Session::groupCallDiscards() const {
|
||||
return _groupCallDiscarded.events();
|
||||
}
|
||||
|
||||
void Session::groupCallDiscarded(uint64 id, int duration) {
|
||||
_groupCallDiscarded.fire({ id, duration });
|
||||
}
|
||||
|
||||
GroupCall *Session::groupCall(uint64 callId) const {
|
||||
const auto i = _groupCalls.find(callId);
|
||||
return (i != end(_groupCalls)) ? i->second.get() : nullptr;
|
||||
@@ -849,6 +841,7 @@ void Session::registerInvitedToCallUser(
|
||||
}
|
||||
}
|
||||
_invitedToCallUsers[callId].emplace(user);
|
||||
_invitesToCalls.fire({ callId, user });
|
||||
}
|
||||
|
||||
void Session::unregisterInvitedToCallUser(
|
||||
|
||||
@@ -158,13 +158,6 @@ public:
|
||||
void unregisterGroupCall(not_null<GroupCall*> call);
|
||||
GroupCall *groupCall(uint64 callId) const;
|
||||
|
||||
struct GroupCallDiscard {
|
||||
uint64 id = 0;
|
||||
int duration = 0;
|
||||
};
|
||||
rpl::producer<GroupCallDiscard> groupCallDiscards() const;
|
||||
void groupCallDiscarded(uint64 id, int duration);
|
||||
|
||||
[[nodiscard]] auto invitedToCallUsers(uint64 callId) const
|
||||
-> const base::flat_set<not_null<UserData*>> &;
|
||||
void registerInvitedToCallUser(
|
||||
@@ -173,6 +166,14 @@ public:
|
||||
not_null<UserData*> user);
|
||||
void unregisterInvitedToCallUser(uint64 callId, not_null<UserData*> user);
|
||||
|
||||
struct InviteToCall {
|
||||
uint64 id = 0;
|
||||
not_null<UserData*> user;
|
||||
};
|
||||
[[nodiscard]] rpl::producer<InviteToCall> invitesToCalls() const {
|
||||
return _invitesToCalls.events();
|
||||
}
|
||||
|
||||
void enumerateUsers(Fn<void(not_null<UserData*>)> action) const;
|
||||
void enumerateGroups(Fn<void(not_null<PeerData*>)> action) const;
|
||||
void enumerateChannels(Fn<void(not_null<ChannelData*>)> action) const;
|
||||
@@ -930,8 +931,8 @@ private:
|
||||
base::flat_set<not_null<ViewElement*>> _heavyViewParts;
|
||||
|
||||
base::flat_map<uint64, not_null<GroupCall*>> _groupCalls;
|
||||
rpl::event_stream<InviteToCall> _invitesToCalls;
|
||||
base::flat_map<uint64, base::flat_set<not_null<UserData*>>> _invitedToCallUsers;
|
||||
rpl::event_stream<GroupCallDiscard> _groupCallDiscarded;
|
||||
|
||||
History *_topPromoted = nullptr;
|
||||
|
||||
|
||||
@@ -1647,7 +1647,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
addDocumentActions(lnkDocument->document());
|
||||
}
|
||||
if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
|
||||
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
|
||||
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
|
||||
HistoryView::CopyPostLink(session, itemId, HistoryView::Context::History);
|
||||
});
|
||||
}
|
||||
@@ -1788,7 +1788,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
});
|
||||
} else if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
|
||||
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
|
||||
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
|
||||
HistoryView::CopyPostLink(session, itemId, HistoryView::Context::History);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -776,7 +776,8 @@ bool HistoryItem::canBeEditedFromHistory() const {
|
||||
}
|
||||
if ((IsServerMsgId(id) || isScheduled())
|
||||
&& !serviceMsg()
|
||||
&& (out() || history()->peer->isSelf())) {
|
||||
&& (out() || history()->peer->isSelf())
|
||||
&& !Has<HistoryMessageForwarded>()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -935,15 +935,6 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) {
|
||||
const auto id = CallIdFromInput(data.vcall());
|
||||
call->lifetime.destroy();
|
||||
|
||||
history()->owner().groupCallDiscards(
|
||||
) | rpl::filter([=](Data::Session::GroupCallDiscard discard) {
|
||||
return (discard.id == id);
|
||||
}) | rpl::start_with_next([=](
|
||||
Data::Session::GroupCallDiscard discard) {
|
||||
RemoveComponents(HistoryServiceOngoingCall::Bit());
|
||||
updateText(prepareDiscardedCallText(discard.duration));
|
||||
}, call->lifetime);
|
||||
|
||||
const auto peer = history()->peer;
|
||||
const auto has = PeerHasThisCall(peer, id);
|
||||
if (!has.has_value()) {
|
||||
|
||||
@@ -773,6 +773,12 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
return false;
|
||||
});
|
||||
|
||||
const auto applyLocalDraft = [=] {
|
||||
if (_history && _history->localDraft()) {
|
||||
applyDraft();
|
||||
}
|
||||
};
|
||||
|
||||
_voiceRecordBar->sendActionUpdates(
|
||||
) | rpl::start_with_next([=](const auto &data) {
|
||||
if (!_history) {
|
||||
@@ -799,8 +805,12 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
data.duration,
|
||||
action);
|
||||
_voiceRecordBar->clearListenState();
|
||||
applyLocalDraft();
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->cancelRequests(
|
||||
) | rpl::start_with_next(applyLocalDraft, lifetime());
|
||||
|
||||
_voiceRecordBar->lockShowStarts(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateHistoryDownVisibility();
|
||||
@@ -817,13 +827,6 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
_scroll->viewportEvent(e);
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->shownValue(
|
||||
) | rpl::start_with_next([=](bool shown) {
|
||||
if (!shown) {
|
||||
applyDraft();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->hideFast();
|
||||
}
|
||||
|
||||
@@ -1446,7 +1449,7 @@ void HistoryWidget::activate() {
|
||||
updateHistoryGeometry();
|
||||
}
|
||||
}
|
||||
if (App::wnd()) App::wnd()->setInnerFocus();
|
||||
controller()->widget()->setInnerFocus();
|
||||
}
|
||||
|
||||
void HistoryWidget::setInnerFocus() {
|
||||
@@ -1620,13 +1623,16 @@ void HistoryWidget::fastShowAtEnd(not_null<History*> history) {
|
||||
void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
|
||||
InvokeQueued(this, [=] { updateStickersByEmoji(); });
|
||||
|
||||
if (_voiceRecordBar->isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto draft = !_history
|
||||
? nullptr
|
||||
: _history->localEditDraft()
|
||||
? _history->localEditDraft()
|
||||
: _history->localDraft();
|
||||
auto fieldAvailable = canWriteMessage()
|
||||
&& !_voiceRecordBar->isActive();
|
||||
auto fieldAvailable = canWriteMessage();
|
||||
if (!draft || (!_history->localEditDraft() && !fieldAvailable)) {
|
||||
auto fieldWillBeHiddenAfterEdit = (!fieldAvailable && _editMsgId != 0);
|
||||
clearFieldText(0, fieldHistoryAction);
|
||||
@@ -3341,7 +3347,7 @@ void HistoryWidget::doneShow() {
|
||||
_groupCallBar->finishAnimating();
|
||||
}
|
||||
checkHistoryActivation();
|
||||
App::wnd()->setInnerFocus();
|
||||
controller()->widget()->setInnerFocus();
|
||||
_preserveScrollTop = false;
|
||||
}
|
||||
|
||||
@@ -5401,17 +5407,18 @@ void HistoryWidget::refreshPinnedBarButton(bool many) {
|
||||
void HistoryWidget::setupGroupCallTracker() {
|
||||
Expects(_history != nullptr);
|
||||
|
||||
const auto channel = _history->peer->asChannel();
|
||||
if (!channel) {
|
||||
const auto peer = _history->peer;
|
||||
if (!peer->asMegagroup() && !peer->asChat()) {
|
||||
_groupCallTracker = nullptr;
|
||||
_groupCallBar = nullptr;
|
||||
return;
|
||||
}
|
||||
_groupCallTracker = std::make_unique<HistoryView::GroupCallTracker>(
|
||||
channel);
|
||||
peer);
|
||||
_groupCallBar = std::make_unique<Ui::GroupCallBar>(
|
||||
this,
|
||||
_groupCallTracker->content());
|
||||
_groupCallTracker->content(),
|
||||
Core::App().appDeactivatedValue());
|
||||
|
||||
rpl::single(
|
||||
rpl::empty_value()
|
||||
|
||||
@@ -796,6 +796,9 @@ void ComposeControls::showStarted() {
|
||||
if (_voiceRecordBar) {
|
||||
_voiceRecordBar->hideFast();
|
||||
}
|
||||
if (_autocomplete) {
|
||||
_autocomplete->hideFast();
|
||||
}
|
||||
_wrap->hide();
|
||||
_writeRestricted->hide();
|
||||
}
|
||||
@@ -810,6 +813,9 @@ void ComposeControls::showFinished() {
|
||||
if (_voiceRecordBar) {
|
||||
_voiceRecordBar->hideFast();
|
||||
}
|
||||
if (_autocomplete) {
|
||||
_autocomplete->hideFast();
|
||||
}
|
||||
updateWrappingVisibility();
|
||||
_voiceRecordBar->orderControls();
|
||||
}
|
||||
@@ -1154,6 +1160,8 @@ void ComposeControls::initAutocomplete() {
|
||||
_autocomplete.get(),
|
||||
[=] { checkAutocomplete(); },
|
||||
Qt::QueuedConnection);
|
||||
|
||||
_autocomplete->hideFast();
|
||||
}
|
||||
|
||||
void ComposeControls::updateStickersByEmoji() {
|
||||
@@ -1712,7 +1720,7 @@ void ComposeControls::paintBackground(QRect clip) {
|
||||
}
|
||||
|
||||
void ComposeControls::escape() {
|
||||
if (auto &voice = _voiceRecordBar; !voice->isActive()) {
|
||||
if (const auto voice = _voiceRecordBar.get(); voice->isActive()) {
|
||||
voice->showDiscardBox(nullptr, anim::type::normal);
|
||||
} else {
|
||||
_cancelRequests.fire({});
|
||||
|
||||
@@ -611,11 +611,14 @@ public:
|
||||
RecordLock(not_null<Ui::RpWidget*> parent);
|
||||
|
||||
void requestPaintProgress(float64 progress);
|
||||
void requestPaintLockToStopProgress(float64 progress);
|
||||
|
||||
[[nodiscard]] rpl::producer<> locks() const;
|
||||
[[nodiscard]] bool isLocked() const;
|
||||
[[nodiscard]] bool isStopState() const;
|
||||
|
||||
[[nodiscard]] float64 lockToStopProgress() const;
|
||||
|
||||
protected:
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
@@ -630,9 +633,9 @@ private:
|
||||
const QRect _rippleRect;
|
||||
const QPen _arcPen;
|
||||
|
||||
Ui::Animations::Simple _lockAnimation;
|
||||
Ui::Animations::Simple _lockEnderAnimation;
|
||||
|
||||
float64 _lockToStopProgress = 0.;
|
||||
rpl::variable<float64> _progress = 0.;
|
||||
};
|
||||
|
||||
@@ -662,8 +665,8 @@ void RecordLock::init() {
|
||||
if (!shown) {
|
||||
setCursor(style::cur_default);
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
_lockAnimation.stop();
|
||||
_lockEnderAnimation.stop();
|
||||
_lockToStopProgress = 0.;
|
||||
_progress = 0.;
|
||||
}
|
||||
}, lifetime());
|
||||
@@ -675,32 +678,13 @@ void RecordLock::init() {
|
||||
const auto top = anim::interpolate(
|
||||
0,
|
||||
height() - st::historyRecordLockTopShadow.height() * 2,
|
||||
_lockAnimation.value(1.));
|
||||
_lockToStopProgress);
|
||||
p.translate(0, top);
|
||||
drawProgress(p);
|
||||
return;
|
||||
}
|
||||
drawProgress(p);
|
||||
}, lifetime());
|
||||
|
||||
locks(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto &duration = st::historyRecordVoiceShowDuration;
|
||||
const auto from = 0.;
|
||||
const auto to = 1.;
|
||||
auto callback = [=](float64 value) {
|
||||
update();
|
||||
if (value == to) {
|
||||
setCursor(style::cur_pointer);
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
|
||||
resize(
|
||||
st::historyRecordLockTopShadow.width(),
|
||||
st::historyRecordLockTopShadow.width());
|
||||
}
|
||||
};
|
||||
_lockAnimation.start(std::move(callback), from, to, duration);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void RecordLock::drawProgress(Painter &p) {
|
||||
@@ -773,8 +757,6 @@ void RecordLock::drawProgress(Painter &p) {
|
||||
}
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
const auto lockToStopProgress =
|
||||
_lockAnimation.value(isLocked() ? 1. : 0);
|
||||
const auto &arcOffset = st::historyRecordLockIconLineSkip;
|
||||
const auto &size = st::historyRecordLockIconSize;
|
||||
|
||||
@@ -786,15 +768,15 @@ void RecordLock::drawProgress(Painter &p) {
|
||||
const auto blockRectWidth = anim::interpolateF(
|
||||
size.width(),
|
||||
st::historyRecordStopIconWidth,
|
||||
lockToStopProgress);
|
||||
_lockToStopProgress);
|
||||
const auto blockRectHeight = anim::interpolateF(
|
||||
blockHeight,
|
||||
st::historyRecordStopIconWidth,
|
||||
lockToStopProgress);
|
||||
_lockToStopProgress);
|
||||
const auto blockRectTop = anim::interpolateF(
|
||||
size.height() - blockHeight,
|
||||
std::round((size.height() - blockRectHeight) / 2.),
|
||||
lockToStopProgress);
|
||||
_lockToStopProgress);
|
||||
|
||||
const auto blockRect = QRectF(
|
||||
(size.width() - blockRectWidth) / 2,
|
||||
@@ -809,11 +791,11 @@ void RecordLock::drawProgress(Painter &p) {
|
||||
inner.x() + (inner.width() - size.width()) / 2,
|
||||
inner.y() + (originTop.height() * 2 - size.height()) / 2);
|
||||
{
|
||||
const auto xRadius = anim::interpolate(2, 3, lockToStopProgress);
|
||||
const auto xRadius = anim::interpolate(2, 3, _lockToStopProgress);
|
||||
p.drawRoundedRect(blockRect, xRadius, 3);
|
||||
}
|
||||
|
||||
const auto offsetTranslate = lockToStopProgress *
|
||||
const auto offsetTranslate = _lockToStopProgress *
|
||||
(lineHeight + arcHeight + _arcPen.width() * 2);
|
||||
p.translate(
|
||||
size.width() - arcOffset,
|
||||
@@ -835,7 +817,7 @@ void RecordLock::drawProgress(Painter &p) {
|
||||
0,
|
||||
180 * 16);
|
||||
|
||||
const auto lockProgress = 1. - _lockAnimation.value(1.);
|
||||
const auto lockProgress = 1. - _lockToStopProgress;
|
||||
if (progress == 1. && lockProgress < 1.) {
|
||||
p.drawLine(
|
||||
-arcWidth,
|
||||
@@ -866,6 +848,23 @@ void RecordLock::requestPaintProgress(float64 progress) {
|
||||
setProgress(progress);
|
||||
}
|
||||
|
||||
void RecordLock::requestPaintLockToStopProgress(float64 progress) {
|
||||
_lockToStopProgress = progress;
|
||||
if (isStopState()) {
|
||||
setCursor(style::cur_pointer);
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
|
||||
resize(
|
||||
st::historyRecordLockTopShadow.width(),
|
||||
st::historyRecordLockTopShadow.width());
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
float64 RecordLock::lockToStopProgress() const {
|
||||
return _lockToStopProgress;
|
||||
}
|
||||
|
||||
void RecordLock::setProgress(float64 progress) {
|
||||
_progress = progress;
|
||||
update();
|
||||
@@ -876,7 +875,7 @@ bool RecordLock::isLocked() const {
|
||||
}
|
||||
|
||||
bool RecordLock::isStopState() const {
|
||||
return isLocked() && (_lockAnimation.value(1.) == 1.);
|
||||
return isLocked() && (_lockToStopProgress == 1.);
|
||||
}
|
||||
|
||||
rpl::producer<> RecordLock::locks() const {
|
||||
@@ -892,6 +891,77 @@ QPoint RecordLock::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
|
||||
}
|
||||
|
||||
class CancelButton final : public Ui::RippleButton {
|
||||
public:
|
||||
CancelButton(not_null<Ui::RpWidget*> parent, int height);
|
||||
|
||||
void requestPaintProgress(float64 progress);
|
||||
|
||||
protected:
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
const int _width;
|
||||
const QRect _rippleRect;
|
||||
|
||||
rpl::variable<float64> _showProgress = 0.;
|
||||
|
||||
Ui::Text::String _text;
|
||||
|
||||
};
|
||||
|
||||
CancelButton::CancelButton(not_null<Ui::RpWidget*> parent, int height)
|
||||
: Ui::RippleButton(parent, st::defaultLightButton.ripple)
|
||||
, _width(st::historyRecordCancelButtonWidth)
|
||||
, _rippleRect(QRect(0, (height - _width) / 2, _width, _width))
|
||||
, _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now).toUpper()) {
|
||||
resize(_width, height);
|
||||
init();
|
||||
}
|
||||
|
||||
void CancelButton::init() {
|
||||
_showProgress.value(
|
||||
) | rpl::start_with_next([=](float64 progress) {
|
||||
const auto hasProgress = (progress > 0.);
|
||||
if (isHidden() == !hasProgress) {
|
||||
setVisible(hasProgress);
|
||||
}
|
||||
update();
|
||||
}, lifetime());
|
||||
|
||||
paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
Painter p(this);
|
||||
|
||||
p.setOpacity(_showProgress.current());
|
||||
|
||||
paintRipple(p, _rippleRect.x(), _rippleRect.y());
|
||||
|
||||
p.setPen(st::historyRecordCancelButtonFg);
|
||||
_text.draw(
|
||||
p,
|
||||
0,
|
||||
(height() - _text.minHeight()) / 2,
|
||||
width(),
|
||||
style::al_center);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
QImage CancelButton::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::ellipseMask(_rippleRect.size());
|
||||
}
|
||||
|
||||
QPoint CancelButton::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
|
||||
}
|
||||
|
||||
void CancelButton::requestPaintProgress(float64 progress) {
|
||||
_showProgress = progress;
|
||||
}
|
||||
|
||||
VoiceRecordBar::VoiceRecordBar(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Ui::RpWidget*> sectionWidget,
|
||||
@@ -906,7 +976,12 @@ VoiceRecordBar::VoiceRecordBar(
|
||||
, _level(std::make_unique<VoiceRecordButton>(
|
||||
sectionWidget,
|
||||
_controller->widget()->leaveEvents()))
|
||||
, _cancel(std::make_unique<CancelButton>(this, recorderHeight))
|
||||
, _startTimer([=] { startRecording(); })
|
||||
, _message(
|
||||
st::historyRecordTextStyle,
|
||||
tr::lng_record_cancel(tr::now),
|
||||
TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto })
|
||||
, _cancelFont(st::historyRecordFont) {
|
||||
resize(QSize(parent->width(), recorderHeight));
|
||||
init();
|
||||
@@ -993,6 +1068,7 @@ void VoiceRecordBar::init() {
|
||||
_cancelFont->width(FormatVoiceDuration(kMaxSamples)),
|
||||
ascent);
|
||||
}
|
||||
_cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0);
|
||||
updateMessageGeometry();
|
||||
updateLockGeometry();
|
||||
}, lifetime());
|
||||
@@ -1006,6 +1082,14 @@ void VoiceRecordBar::init() {
|
||||
p.fillRect(clip, st::historyComposeAreaBg);
|
||||
|
||||
p.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio()));
|
||||
const auto opacity = p.opacity();
|
||||
_cancel->requestPaintProgress(_lock->isStopState()
|
||||
? (opacity * _lock->lockToStopProgress())
|
||||
: 0.);
|
||||
|
||||
if (!opacity) {
|
||||
return;
|
||||
}
|
||||
if (clip.intersects(_messageRect)) {
|
||||
// The message should be painted first to avoid flickering.
|
||||
drawMessage(p, activeAnimationRatio());
|
||||
@@ -1056,7 +1140,8 @@ void VoiceRecordBar::init() {
|
||||
const auto &duration = st::historyRecordVoiceShowDuration;
|
||||
auto callback = [=](float64 value) {
|
||||
_listen->requestPaintProgress(value);
|
||||
_level->requestPaintProgress(to - value);
|
||||
const auto reverseValue = to - value;
|
||||
_level->requestPaintProgress(reverseValue);
|
||||
update();
|
||||
if (to == value) {
|
||||
_recordingLifetime.destroy();
|
||||
@@ -1084,23 +1169,15 @@ void VoiceRecordBar::init() {
|
||||
) | rpl::start_with_next([=](bool enter) {
|
||||
_inField = enter;
|
||||
}, _recordingLifetime);
|
||||
}, lifetime());
|
||||
|
||||
rpl::merge(
|
||||
_lock->locks(),
|
||||
shownValue() | rpl::to_empty
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto direction = Qt::LayoutDirectionAuto;
|
||||
_message.setText(
|
||||
st::historyRecordTextStyle,
|
||||
_lock->isLocked()
|
||||
? tr::lng_record_lock_cancel(tr::now)
|
||||
: tr::lng_record_cancel(tr::now),
|
||||
TextParseOptions{ TextParseMultiline, 0, 0, direction });
|
||||
|
||||
updateMessageGeometry();
|
||||
// Update a whole widget to clear a previous text.
|
||||
update();
|
||||
const auto &duration = st::historyRecordVoiceShowDuration;
|
||||
const auto from = 0.;
|
||||
const auto to = 1.;
|
||||
auto callback = [=](float64 value) {
|
||||
_lock->requestPaintLockToStopProgress(value);
|
||||
update();
|
||||
};
|
||||
_lockToStopAnimation.start(std::move(callback), from, to, duration);
|
||||
}, lifetime());
|
||||
|
||||
_send->events(
|
||||
@@ -1135,6 +1212,10 @@ void VoiceRecordBar::init() {
|
||||
|
||||
installListenStateFilter();
|
||||
}, lifetime());
|
||||
|
||||
_cancel->setClickedCallback([=] {
|
||||
hideAnimated();
|
||||
});
|
||||
}
|
||||
|
||||
void VoiceRecordBar::activeAnimate(bool active) {
|
||||
@@ -1290,6 +1371,7 @@ void VoiceRecordBar::finish() {
|
||||
_recordingSamples = 0;
|
||||
|
||||
_showAnimation.stop();
|
||||
_lockToStopAnimation.stop();
|
||||
|
||||
_listen = nullptr;
|
||||
|
||||
@@ -1307,7 +1389,9 @@ void VoiceRecordBar::hideFast() {
|
||||
void VoiceRecordBar::stopRecording(StopType type) {
|
||||
using namespace ::Media::Capture;
|
||||
if (type == StopType::Cancel) {
|
||||
instance()->stop();
|
||||
instance()->stop(crl::guard(this, [=](Result &&data) {
|
||||
_cancelRequests.fire({});
|
||||
}));
|
||||
return;
|
||||
}
|
||||
instance()->stop(crl::guard(this, [=](Result &&data) {
|
||||
@@ -1377,12 +1461,17 @@ void VoiceRecordBar::drawMessage(Painter &p, float64 recordActive) {
|
||||
st::historyRecordCancelActive,
|
||||
1. - recordActive));
|
||||
|
||||
const auto opacity = p.opacity();
|
||||
p.setOpacity(opacity * (1. - _lock->lockToStopProgress()));
|
||||
|
||||
_message.draw(
|
||||
p,
|
||||
_messageRect.x(),
|
||||
_messageRect.y(),
|
||||
_messageRect.width(),
|
||||
style::al_center);
|
||||
|
||||
p.setOpacity(opacity);
|
||||
}
|
||||
|
||||
void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
|
||||
@@ -1404,6 +1493,10 @@ rpl::producer<VoiceToSend> VoiceRecordBar::sendVoiceRequests() const {
|
||||
return _sendVoiceRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> VoiceRecordBar::cancelRequests() const {
|
||||
return _cancelRequests.events();
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::isRecording() const {
|
||||
return _recording.current();
|
||||
}
|
||||
@@ -1416,6 +1509,7 @@ void VoiceRecordBar::hideAnimated() {
|
||||
if (isHidden()) {
|
||||
return;
|
||||
}
|
||||
_lockShowing = false;
|
||||
visibilityAnimate(false, [=] { hideFast(); });
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace HistoryView::Controls {
|
||||
class VoiceRecordButton;
|
||||
class ListenWrap;
|
||||
class RecordLock;
|
||||
class CancelButton;
|
||||
|
||||
class VoiceRecordBar final : public Ui::RpWidget {
|
||||
public:
|
||||
@@ -61,6 +62,7 @@ public:
|
||||
|
||||
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
|
||||
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> recordingStateChanges() const;
|
||||
[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
|
||||
[[nodiscard]] rpl::producer<not_null<QEvent*>> lockViewportEvents() const;
|
||||
@@ -122,12 +124,14 @@ private:
|
||||
const std::shared_ptr<Ui::SendButton> _send;
|
||||
const std::unique_ptr<RecordLock> _lock;
|
||||
const std::unique_ptr<VoiceRecordButton> _level;
|
||||
const std::unique_ptr<CancelButton> _cancel;
|
||||
std::unique_ptr<ListenWrap> _listen;
|
||||
|
||||
base::Timer _startTimer;
|
||||
|
||||
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
|
||||
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
|
||||
rpl::event_stream<> _cancelRequests;
|
||||
rpl::event_stream<> _listenChanges;
|
||||
|
||||
int _centerY = 0;
|
||||
@@ -150,6 +154,7 @@ private:
|
||||
rpl::lifetime _recordingLifetime;
|
||||
|
||||
Ui::Animations::Simple _showLockAnimation;
|
||||
Ui::Animations::Simple _lockToStopAnimation;
|
||||
Ui::Animations::Simple _showListenAnimation;
|
||||
Ui::Animations::Simple _activeAnimation;
|
||||
Ui::Animations::Simple _showAnimation;
|
||||
|
||||
@@ -7,631 +7,56 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/controls/history_view_voice_record_button.h"
|
||||
|
||||
#include "ui/paint/blobs.h"
|
||||
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
#include <QMatrix>
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kSegmentsCount = 12;
|
||||
constexpr auto kMajorDegreeOffset = 360 / kSegmentsCount;
|
||||
constexpr auto kSixtyDegrees = 60;
|
||||
constexpr auto kMaxLevel = 1800.;
|
||||
constexpr auto kBlobAlpha = 76. / 255.;
|
||||
constexpr auto kBlobMaxSpeed = 5.0;
|
||||
constexpr auto kLevelDuration = 100. + 500. * 0.33;
|
||||
constexpr auto kBlobsScaleEnterDuration = crl::time(250);
|
||||
|
||||
constexpr auto kEnterIdleAnimationDuration = crl::time(1200);
|
||||
|
||||
constexpr auto kRotationSpeed = 0.36 * 0.1;
|
||||
|
||||
constexpr auto kRandomAdditionFactor = 0.15;
|
||||
|
||||
constexpr auto kIdleRadiusGlobalFactor = 0.56;
|
||||
constexpr auto kIdleRadiusFactor = 0.15 * 0.5;
|
||||
|
||||
constexpr auto kOpacityMajor = 0.30;
|
||||
constexpr auto kOpacityMinor = 0.15;
|
||||
|
||||
constexpr auto kIdleRotationSpeed = 0.2;
|
||||
constexpr auto kIdleRotateDiff = 0.1 * kIdleRotationSpeed;
|
||||
|
||||
constexpr auto kWaveAngle = 0.03;
|
||||
|
||||
constexpr auto kAnimationSpeedMajor = 1.5 - 0.65;
|
||||
constexpr auto kAnimationSpeedMinor = 1.5 - 0.45;
|
||||
constexpr auto kAnimationSpeedCircle = 1.5 - 0.25;
|
||||
|
||||
constexpr auto kAmplitudeDiffFactorMax = 500. - 100.;
|
||||
constexpr auto kAmplitudeDiffFactorMajor = 300. - 100.;
|
||||
constexpr auto kAmplitudeDiffFactorMinor = 400. - 100.;
|
||||
|
||||
constexpr auto kFlingDistanceFactorMajor = 8 * 16;
|
||||
constexpr auto kFlingDistanceFactorMinor = 20 * 16;
|
||||
|
||||
constexpr auto kFlingInAnimationDurationMajor = 200;
|
||||
constexpr auto kFlingInAnimationDurationMinor = 350;
|
||||
constexpr auto kFlingOutAnimationDurationMajor = 220;
|
||||
constexpr auto kFlingOutAnimationDurationMinor = 380;
|
||||
|
||||
constexpr auto kSineWaveSpeedMajor = 0.02 * 0.2;
|
||||
constexpr auto kSineWaveSpeedMinor = 0.026 * 0.2;
|
||||
|
||||
constexpr auto kSmallWaveRadius = 0.55;
|
||||
|
||||
constexpr auto kFlingDistance = 0.50;
|
||||
|
||||
constexpr auto kMinDivider = 100.;
|
||||
|
||||
constexpr auto kMaxAmplitude = 1800.;
|
||||
|
||||
constexpr auto kZeroPoint = QPointF(0, 0);
|
||||
|
||||
template <typename Number>
|
||||
void Normalize(Number &value, Number right) {
|
||||
if (value >= right) {
|
||||
value -= right;
|
||||
}
|
||||
}
|
||||
|
||||
float64 RandomAdditional() {
|
||||
return (rand_value<int>() % 100 / 100.);
|
||||
}
|
||||
|
||||
void PerformAnimation(
|
||||
rpl::producer<crl::time> &&animationTicked,
|
||||
Fn<void(float64)> &&applyValue,
|
||||
Fn<void()> &&finishCallback,
|
||||
float64 duration,
|
||||
float64 from,
|
||||
float64 to,
|
||||
rpl::lifetime &lifetime) {
|
||||
lifetime.destroy();
|
||||
const auto animValue =
|
||||
lifetime.make_state<anim::value>(from, to);
|
||||
const auto animStarted = crl::now();
|
||||
std::move(
|
||||
animationTicked
|
||||
) | rpl::start_with_next([=,
|
||||
applyValue = std::move(applyValue),
|
||||
finishCallback = std::move(finishCallback),
|
||||
&lifetime](crl::time now) mutable {
|
||||
const auto dt = anim::Disabled()
|
||||
? 1.
|
||||
: ((now - animStarted) / duration);
|
||||
if (dt >= 1.) {
|
||||
animValue->finish();
|
||||
applyValue(animValue->current());
|
||||
lifetime.destroy();
|
||||
if (finishCallback) {
|
||||
finishCallback();
|
||||
}
|
||||
} else {
|
||||
animValue->update(dt, anim::linear);
|
||||
applyValue(animValue->current());
|
||||
}
|
||||
}, lifetime);
|
||||
auto Blobs() {
|
||||
return std::vector<Ui::Paint::Blobs::BlobData>{
|
||||
{
|
||||
.segmentsCount = 9,
|
||||
.minScale = 0.605229,
|
||||
.minRadius = (float)st::historyRecordMinorBlobMinRadius,
|
||||
.maxRadius = (float)st::historyRecordMinorBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = kBlobAlpha,
|
||||
.maxSpeed = kBlobMaxSpeed,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 12,
|
||||
.minScale = 0.553943,
|
||||
.minRadius = (float)st::historyRecordMajorBlobMinRadius,
|
||||
.maxRadius = (float)st::historyRecordMajorBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = kBlobAlpha,
|
||||
.maxSpeed = kBlobMaxSpeed,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ContinuousValue {
|
||||
public:
|
||||
ContinuousValue() = default;
|
||||
ContinuousValue(float64 duration) : _duration(duration) {
|
||||
}
|
||||
void start(float64 to, float64 duration) {
|
||||
_to = to;
|
||||
_delta = (_to - _cur) / duration;
|
||||
}
|
||||
void start(float64 to) {
|
||||
start(to, _duration);
|
||||
}
|
||||
void reset() {
|
||||
_to = _cur = _delta = 0.;
|
||||
}
|
||||
|
||||
float64 current() const {
|
||||
return _cur;
|
||||
}
|
||||
float64 to() const {
|
||||
return _to;
|
||||
}
|
||||
float64 delta() const {
|
||||
return _delta;
|
||||
}
|
||||
void update(crl::time dt, Fn<void(float64 &)> &&callback = nullptr) {
|
||||
if (_to != _cur) {
|
||||
_cur += _delta * dt;
|
||||
if ((_to != _cur) && ((_delta > 0) == (_cur > _to))) {
|
||||
_cur = _to;
|
||||
}
|
||||
if (callback) {
|
||||
callback(_cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
float64 _duration = 0.;
|
||||
float64 _to = 0.;
|
||||
|
||||
float64 _cur = 0.;
|
||||
float64 _delta = 0.;
|
||||
|
||||
};
|
||||
|
||||
class CircleBezier final {
|
||||
public:
|
||||
CircleBezier(int n);
|
||||
|
||||
void computeRandomAdditionals();
|
||||
void paintCircle(
|
||||
Painter &p,
|
||||
const QColor &c,
|
||||
float64 radius,
|
||||
float64 cubicBezierFactor,
|
||||
float64 idleStateDiff,
|
||||
float64 radiusDiff,
|
||||
float64 randomFactor);
|
||||
|
||||
private:
|
||||
struct Points {
|
||||
QPointF point;
|
||||
QPointF control;
|
||||
};
|
||||
|
||||
const int _segmentsCount;
|
||||
const float64 _segmentLength;
|
||||
std::vector<float64> _randomAdditionals;
|
||||
|
||||
};
|
||||
|
||||
class Wave final {
|
||||
public:
|
||||
Wave(
|
||||
rpl::producer<crl::time> animationTicked,
|
||||
int n,
|
||||
float64 rotationOffset,
|
||||
float64 amplitudeRadius,
|
||||
float64 amplitudeWaveDiff,
|
||||
float64 fling,
|
||||
int flingDistanceFactor,
|
||||
int flingInAnimationDuration,
|
||||
int flingOutAnimationDuration,
|
||||
float64 amplitudeDiffSpeed,
|
||||
float64 amplitudeDiffFactor,
|
||||
bool isDirectionClockwise);
|
||||
|
||||
void setValue(float64 to);
|
||||
void tick(float64 circleRadius, crl::time dt);
|
||||
void reset();
|
||||
|
||||
void paint(Painter &p, QColor c);
|
||||
|
||||
private:
|
||||
|
||||
void initEnterIdleAnimation(rpl::producer<crl::time> animationTicked);
|
||||
void initFlingAnimation(rpl::producer<crl::time> animationTicked);
|
||||
|
||||
const std::unique_ptr<CircleBezier> _circleBezier;
|
||||
|
||||
const float _rotationOffset;
|
||||
const float64 _idleGlobalRadius;
|
||||
const float64 _amplitudeRadius;
|
||||
const float64 _amplitudeWaveDiff;
|
||||
const float64 _randomAdditions;
|
||||
const float64 _fling;
|
||||
const int _flingDistanceFactor;
|
||||
const int _flingInAnimationDuration;
|
||||
const int _flingOutAnimationDuration;
|
||||
const float64 _amplitudeInAnimationDuration;
|
||||
const float64 _amplitudeOutAnimationDuration;
|
||||
const int _directionClockwise;
|
||||
|
||||
bool _incRandomAdditionals = false;
|
||||
bool _isIdle = true;
|
||||
bool _wasFling = false;
|
||||
float64 _flingRadius = 0.;
|
||||
float64 _idleRadius = 0.;
|
||||
float64 _idleRotation = 0.;
|
||||
float64 _lastRadius = 0.;
|
||||
float64 _rotation = 0.;
|
||||
float64 _sineAngleMax = 0.;
|
||||
float64 _waveAngle = 0.;
|
||||
float64 _waveDiff = 0.;
|
||||
ContinuousValue _levelValue;
|
||||
|
||||
rpl::event_stream<float64> _flingAnimationRequests;
|
||||
rpl::event_stream<> _enterIdleAnimationRequests;
|
||||
rpl::lifetime _animationEnterIdleLifetime;
|
||||
rpl::lifetime _animationFlingLifetime;
|
||||
rpl::lifetime _lifetime;
|
||||
};
|
||||
|
||||
class RecordCircle final {
|
||||
public:
|
||||
RecordCircle(rpl::producer<crl::time> animationTicked);
|
||||
|
||||
void reset();
|
||||
void setAmplitude(float64 value);
|
||||
void paint(Painter &p, QColor c);
|
||||
|
||||
private:
|
||||
|
||||
const std::unique_ptr<Wave> _majorWave;
|
||||
const std::unique_ptr<Wave> _minorWave;
|
||||
|
||||
crl::time _lastUpdateTime = 0;
|
||||
ContinuousValue _levelValue;
|
||||
|
||||
};
|
||||
|
||||
CircleBezier::CircleBezier(int n)
|
||||
: _segmentsCount(n)
|
||||
, _segmentLength((4.0 / 3.0) * std::tan(M_PI / (2 * n)))
|
||||
, _randomAdditionals(n) {
|
||||
}
|
||||
|
||||
void CircleBezier::computeRandomAdditionals() {
|
||||
ranges::generate(_randomAdditionals, RandomAdditional);
|
||||
}
|
||||
|
||||
void CircleBezier::paintCircle(
|
||||
Painter &p,
|
||||
const QColor &c,
|
||||
float64 radius,
|
||||
float64 cubicBezierFactor,
|
||||
float64 idleStateDiff,
|
||||
float64 radiusDiff,
|
||||
float64 randomFactor) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
const auto r1 = radius - idleStateDiff / 2. - radiusDiff / 2.;
|
||||
const auto r2 = radius + radiusDiff / 2. + idleStateDiff / 2.;
|
||||
const auto l = _segmentLength * std::max(r1, r2) * cubicBezierFactor;
|
||||
|
||||
auto m = QMatrix();
|
||||
|
||||
const auto preparePoints = [&](int i, bool isStart) -> Points {
|
||||
Normalize(i, _segmentsCount);
|
||||
const auto randomAddition = randomFactor * _randomAdditionals[i];
|
||||
const auto r = ((i % 2 == 0) ? r1 : r2) + randomAddition;
|
||||
|
||||
m.reset();
|
||||
m.rotate(360. / _segmentsCount * i);
|
||||
const auto sign = isStart ? 1 : -1;
|
||||
|
||||
return {
|
||||
(isStart && i) ? QPointF() : m.map(QPointF(0, -r)),
|
||||
m.map(QPointF(sign * (l + randomAddition * _segmentLength), -r)),
|
||||
};
|
||||
};
|
||||
|
||||
const auto &[startPoint, _] = preparePoints(0, true);
|
||||
|
||||
auto path = QPainterPath();
|
||||
path.moveTo(startPoint);
|
||||
|
||||
for (auto i = 0; i < _segmentsCount; i++) {
|
||||
const auto &[_, startControl] = preparePoints(i, true);
|
||||
const auto &[end, endControl] = preparePoints(i + 1, false);
|
||||
|
||||
path.cubicTo(startControl, endControl, end);
|
||||
}
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
|
||||
auto pen = QPen(Qt::NoPen);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
|
||||
p.setPen(pen);
|
||||
p.fillPath(path, c);
|
||||
p.drawPath(path);
|
||||
}
|
||||
|
||||
Wave::Wave(
|
||||
rpl::producer<crl::time> animationTicked,
|
||||
int n,
|
||||
float64 rotationOffset,
|
||||
float64 amplitudeRadius,
|
||||
float64 amplitudeWaveDiff,
|
||||
float64 fling,
|
||||
int flingDistanceFactor,
|
||||
int flingInAnimationDuration,
|
||||
int flingOutAnimationDuration,
|
||||
float64 amplitudeDiffSpeed,
|
||||
float64 amplitudeDiffFactor,
|
||||
bool isDirectionClockwise)
|
||||
: _circleBezier(std::make_unique<CircleBezier>(n))
|
||||
, _rotationOffset(rotationOffset)
|
||||
, _idleGlobalRadius(st::historyRecordRadiusDiffMin * kIdleRadiusGlobalFactor)
|
||||
, _amplitudeRadius(amplitudeRadius)
|
||||
, _amplitudeWaveDiff(amplitudeWaveDiff)
|
||||
, _randomAdditions(st::historyRecordRandomAddition * kRandomAdditionFactor)
|
||||
, _fling(fling)
|
||||
, _flingDistanceFactor(flingDistanceFactor)
|
||||
, _flingInAnimationDuration(flingInAnimationDuration)
|
||||
, _flingOutAnimationDuration(flingOutAnimationDuration)
|
||||
, _amplitudeInAnimationDuration(kMinDivider
|
||||
+ amplitudeDiffFactor * amplitudeDiffSpeed)
|
||||
, _amplitudeOutAnimationDuration(kMinDivider
|
||||
+ kAmplitudeDiffFactorMax * amplitudeDiffSpeed)
|
||||
, _directionClockwise(isDirectionClockwise ? 1 : -1)
|
||||
, _rotation(rotationOffset) {
|
||||
initEnterIdleAnimation(rpl::duplicate(animationTicked));
|
||||
initFlingAnimation(std::move(animationTicked));
|
||||
}
|
||||
|
||||
void Wave::reset() {
|
||||
_incRandomAdditionals = false;
|
||||
_isIdle = true;
|
||||
_wasFling = false;
|
||||
_flingRadius = 0.;
|
||||
_idleRadius = 0.;
|
||||
_idleRotation = 0.;
|
||||
_lastRadius = 0.;
|
||||
_rotation = 0.;
|
||||
_sineAngleMax = 0.;
|
||||
_waveAngle = 0.;
|
||||
_waveDiff = 0.;
|
||||
_levelValue.reset();
|
||||
}
|
||||
|
||||
void Wave::setValue(float64 to) {
|
||||
const auto duration = (to <= _levelValue.current())
|
||||
? _amplitudeOutAnimationDuration
|
||||
: _amplitudeInAnimationDuration;
|
||||
_levelValue.start(to, duration);
|
||||
|
||||
const auto idle = to < 0.1;
|
||||
if (_isIdle != idle && idle) {
|
||||
_enterIdleAnimationRequests.fire({});
|
||||
}
|
||||
|
||||
_isIdle = idle;
|
||||
|
||||
if (!_isIdle) {
|
||||
_animationEnterIdleLifetime.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void Wave::initEnterIdleAnimation(rpl::producer<crl::time> animationTicked) {
|
||||
_enterIdleAnimationRequests.events(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto &k = kSixtyDegrees;
|
||||
|
||||
const auto rotation = _rotation;
|
||||
const auto rotationTo = std::round(rotation / k) * k
|
||||
+ _rotationOffset;
|
||||
const auto waveDiff = _waveDiff;
|
||||
|
||||
auto applyValue = [=](float64 v) {
|
||||
_rotation = rotationTo + (rotation - rotationTo) * v;
|
||||
_waveDiff = 1. + (waveDiff - 1.) * v;
|
||||
_waveAngle = std::acos(_waveDiff * _directionClockwise);
|
||||
};
|
||||
|
||||
PerformAnimation(
|
||||
rpl::duplicate(animationTicked),
|
||||
std::move(applyValue),
|
||||
nullptr,
|
||||
kEnterIdleAnimationDuration,
|
||||
1,
|
||||
0,
|
||||
_animationEnterIdleLifetime);
|
||||
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Wave::initFlingAnimation(rpl::producer<crl::time> animationTicked) {
|
||||
_flingAnimationRequests.events(
|
||||
) | rpl::start_with_next([=](float64 delta) {
|
||||
|
||||
const auto fling = _fling * 2;
|
||||
const auto flingDistance = delta
|
||||
* _amplitudeRadius
|
||||
* _flingDistanceFactor
|
||||
* fling;
|
||||
|
||||
const auto applyValue = [=](float64 v) {
|
||||
_flingRadius = v;
|
||||
};
|
||||
auto finishCallback = [=] {
|
||||
PerformAnimation(
|
||||
rpl::duplicate(animationTicked),
|
||||
applyValue,
|
||||
nullptr,
|
||||
_flingOutAnimationDuration * fling,
|
||||
flingDistance,
|
||||
0,
|
||||
_animationFlingLifetime);
|
||||
};
|
||||
|
||||
PerformAnimation(
|
||||
rpl::duplicate(animationTicked),
|
||||
applyValue,
|
||||
std::move(finishCallback),
|
||||
_flingInAnimationDuration * fling,
|
||||
_flingRadius,
|
||||
flingDistance,
|
||||
_animationFlingLifetime);
|
||||
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Wave::tick(float64 circleRadius, crl::time dt) {
|
||||
|
||||
auto amplitudeCallback = [&](float64 &value) {
|
||||
if (std::abs(value - _levelValue.to()) * _amplitudeRadius
|
||||
< (st::historyRecordRandomAddition / 2)) {
|
||||
if (!_wasFling) {
|
||||
_flingAnimationRequests.fire_copy(_levelValue.delta());
|
||||
_wasFling = true;
|
||||
}
|
||||
} else {
|
||||
_wasFling = false;
|
||||
}
|
||||
};
|
||||
_levelValue.update(dt, std::move(amplitudeCallback));
|
||||
|
||||
_idleRadius = circleRadius * kIdleRadiusFactor;
|
||||
|
||||
{
|
||||
const auto to = _levelValue.to();
|
||||
const auto delta = (_sineAngleMax - to);
|
||||
if (std::abs(delta) - 0.25 < 0) {
|
||||
_sineAngleMax = to;
|
||||
} else {
|
||||
_sineAngleMax -= 0.25 * ((delta < 0) ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isIdle) {
|
||||
_rotation += dt
|
||||
* (kRotationSpeed * 4. * std::min(_levelValue.current() / .5, 1.)
|
||||
+ kRotationSpeed * 0.5);
|
||||
Normalize(_rotation, 360.);
|
||||
} else {
|
||||
_idleRotation += kIdleRotateDiff * dt;
|
||||
Normalize(_idleRotation, 360.);
|
||||
}
|
||||
|
||||
_lastRadius = circleRadius;
|
||||
|
||||
if (!_isIdle) {
|
||||
_waveAngle += (_amplitudeWaveDiff * _sineAngleMax) * dt;
|
||||
_waveDiff = std::cos(_waveAngle) * _directionClockwise;
|
||||
|
||||
if ((_waveDiff != 0) && ((_waveDiff > 0) == _incRandomAdditionals)) {
|
||||
_circleBezier->computeRandomAdditionals();
|
||||
_incRandomAdditionals = !_incRandomAdditionals;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Wave::paint(Painter &p, QColor c) {
|
||||
const auto amplitude = _levelValue.current();
|
||||
const auto waveAmplitude = std::min(amplitude / .3, 1.);
|
||||
const auto radiusDiff = st::historyRecordRadiusDiffMin
|
||||
+ st::historyRecordRadiusDiff * kWaveAngle * _levelValue.to();
|
||||
|
||||
const auto diffFactor = 0.35 * waveAmplitude * _waveDiff;
|
||||
|
||||
const auto radius = (_lastRadius + _amplitudeRadius * amplitude)
|
||||
+ _idleGlobalRadius
|
||||
+ (_flingRadius * waveAmplitude);
|
||||
|
||||
const auto cubicBezierFactor = 1.
|
||||
+ std::abs(diffFactor) * waveAmplitude
|
||||
+ (1. - waveAmplitude) * kIdleRadiusFactor;
|
||||
|
||||
const auto circleRadiusDiff = std::max(
|
||||
radiusDiff * diffFactor,
|
||||
st::historyRecordLevelMainRadius - radius);
|
||||
|
||||
p.rotate((_rotation + _idleRotation) * _directionClockwise);
|
||||
|
||||
_circleBezier->paintCircle(
|
||||
p,
|
||||
c,
|
||||
radius,
|
||||
cubicBezierFactor,
|
||||
_idleRadius * (1. - waveAmplitude),
|
||||
circleRadiusDiff,
|
||||
waveAmplitude * _waveDiff * _randomAdditions);
|
||||
|
||||
p.rotate(0);
|
||||
}
|
||||
|
||||
RecordCircle::RecordCircle(rpl::producer<crl::time> animationTicked)
|
||||
: _majorWave(std::make_unique<Wave>(
|
||||
rpl::duplicate(animationTicked),
|
||||
kSegmentsCount,
|
||||
kMajorDegreeOffset,
|
||||
st::historyRecordMajorAmplitudeRadius,
|
||||
kSineWaveSpeedMajor,
|
||||
0.,
|
||||
kFlingDistanceFactorMajor,
|
||||
kFlingInAnimationDurationMajor,
|
||||
kFlingOutAnimationDurationMajor,
|
||||
kAnimationSpeedMajor,
|
||||
kAmplitudeDiffFactorMajor,
|
||||
true))
|
||||
, _minorWave(std::make_unique<Wave>(
|
||||
std::move(animationTicked),
|
||||
kSegmentsCount,
|
||||
0,
|
||||
st::historyRecordMinorAmplitudeRadius
|
||||
+ st::historyRecordMinorAmplitudeRadius * kSmallWaveRadius,
|
||||
kSineWaveSpeedMinor,
|
||||
kFlingDistance,
|
||||
kFlingDistanceFactorMinor,
|
||||
kFlingInAnimationDurationMinor,
|
||||
kFlingOutAnimationDurationMinor,
|
||||
kAnimationSpeedMinor,
|
||||
kAmplitudeDiffFactorMinor,
|
||||
false))
|
||||
, _levelValue(kMinDivider
|
||||
+ kAmplitudeDiffFactorMax * kAnimationSpeedCircle) {
|
||||
}
|
||||
|
||||
void RecordCircle::reset() {
|
||||
_majorWave->reset();
|
||||
_minorWave->reset();
|
||||
_levelValue.reset();
|
||||
}
|
||||
|
||||
void RecordCircle::setAmplitude(float64 value) {
|
||||
const auto to = std::min(kMaxAmplitude, value) / kMaxAmplitude;
|
||||
_levelValue.start(to);
|
||||
_majorWave->setValue(to);
|
||||
_minorWave->setValue(to);
|
||||
}
|
||||
|
||||
void RecordCircle::paint(Painter &p, QColor c) {
|
||||
const auto dt = crl::now() - _lastUpdateTime;
|
||||
_levelValue.update(dt);
|
||||
|
||||
const auto &mainRadius = st::historyRecordLevelMainRadiusAmplitude;
|
||||
const auto radius = (st::historyRecordLevelMainRadius
|
||||
+ (anim::Disabled() ? 0 : mainRadius * _levelValue.current()));
|
||||
|
||||
if (!anim::Disabled()) {
|
||||
_majorWave->tick(radius, dt);
|
||||
_minorWave->tick(radius, dt);
|
||||
_lastUpdateTime = crl::now();
|
||||
|
||||
const auto opacity = p.opacity();
|
||||
p.setOpacity(kOpacityMajor);
|
||||
_majorWave->paint(p, c);
|
||||
p.setOpacity(kOpacityMinor);
|
||||
_minorWave->paint(p, c);
|
||||
p.setOpacity(opacity);
|
||||
}
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(c);
|
||||
p.drawEllipse(kZeroPoint, radius, radius);
|
||||
}
|
||||
|
||||
VoiceRecordButton::VoiceRecordButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<> leaveWindowEventProducer)
|
||||
: AbstractButton(parent)
|
||||
, _recordCircle(std::make_unique<RecordCircle>(
|
||||
_recordAnimationTicked.events()))
|
||||
, _center(st::historyRecordLevelMaxRadius)
|
||||
, _recordingAnimation([=](crl::time now) {
|
||||
if (!anim::Disabled()) {
|
||||
update();
|
||||
}
|
||||
_recordAnimationTicked.fire_copy(now);
|
||||
return true;
|
||||
}) {
|
||||
const auto h = st::historyRecordLevelMaxRadius * 2;
|
||||
resize(h, h);
|
||||
, _blobs(std::make_unique<Ui::Paint::Blobs>(
|
||||
Blobs(),
|
||||
kLevelDuration,
|
||||
kMaxLevel))
|
||||
, _center(_blobs->maxRadius()) {
|
||||
resize(_center * 2, _center * 2);
|
||||
std::move(
|
||||
leaveWindowEventProducer
|
||||
) | rpl::start_with_next([=] {
|
||||
@@ -643,45 +68,71 @@ VoiceRecordButton::VoiceRecordButton(
|
||||
VoiceRecordButton::~VoiceRecordButton() = default;
|
||||
|
||||
void VoiceRecordButton::requestPaintLevel(quint16 level) {
|
||||
_recordCircle->setAmplitude(level);
|
||||
if (_blobsHideLastTime) {
|
||||
return;
|
||||
}
|
||||
_blobs->setLevel(level);
|
||||
update();
|
||||
}
|
||||
|
||||
void VoiceRecordButton::init() {
|
||||
const auto hasProgress = [](auto value) { return value != 0.; };
|
||||
|
||||
const auto stateChangedAnimation =
|
||||
lifetime().make_state<Ui::Animations::Simple>();
|
||||
const auto currentState = lifetime().make_state<Type>(_state.current());
|
||||
|
||||
rpl::single(
|
||||
anim::Disabled()
|
||||
) | rpl::then(
|
||||
anim::Disables()
|
||||
) | rpl::start_with_next([=](bool hide) {
|
||||
if (hide) {
|
||||
_blobs->setLevel(0.);
|
||||
}
|
||||
_blobsHideLastTime = hide ? crl::now() : 0;
|
||||
if (!hide && !_animation.animating()) {
|
||||
_animation.start();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
const auto &mainRadiusMin = st::historyRecordMainBlobMinRadius;
|
||||
const auto mainRadiusDiff = st::historyRecordMainBlobMaxRadius
|
||||
- mainRadiusMin;
|
||||
paintRequest(
|
||||
) | rpl::start_with_next([=](const QRect &clip) {
|
||||
Painter p(this);
|
||||
|
||||
const auto progress = _showProgress.current();
|
||||
const auto complete = (progress == 1.);
|
||||
const auto hideProgress = _blobsHideLastTime
|
||||
? 1. - std::clamp(
|
||||
((crl::now() - _blobsHideLastTime)
|
||||
/ (float64)kBlobsScaleEnterDuration),
|
||||
0.,
|
||||
1.)
|
||||
: 1.;
|
||||
const auto showProgress = _showProgress.current();
|
||||
const auto complete = (showProgress == 1.);
|
||||
|
||||
p.translate(_center, _center);
|
||||
if (!complete) {
|
||||
p.scale(progress, progress);
|
||||
}
|
||||
PainterHighQualityEnabler hq(p);
|
||||
const auto color = anim::color(
|
||||
const auto brush = QBrush(anim::color(
|
||||
st::historyRecordVoiceFgInactive,
|
||||
st::historyRecordVoiceFgActive,
|
||||
_colorProgress.current());
|
||||
_recordCircle->paint(p, color);
|
||||
p.resetTransform();
|
||||
_colorProgress));
|
||||
|
||||
_blobs->paint(p, brush, showProgress * hideProgress);
|
||||
|
||||
const auto radius = (mainRadiusMin
|
||||
+ (mainRadiusDiff * _blobs->currentLevel())) * showProgress;
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(brush);
|
||||
p.drawEllipse(QPointF(), radius, radius);
|
||||
|
||||
if (!complete) {
|
||||
p.setOpacity(progress);
|
||||
p.setOpacity(showProgress);
|
||||
}
|
||||
|
||||
// Paint icon.
|
||||
{
|
||||
const auto stateProgress = stateChangedAnimation->value(0.);
|
||||
const auto stateProgress = _stateChangedAnimation.value(0.);
|
||||
const auto scale = (std::cos(M_PI * 2 * stateProgress) + 1.) * .5;
|
||||
p.translate(_center, _center);
|
||||
if (scale < 1.) {
|
||||
p.scale(scale, scale);
|
||||
}
|
||||
@@ -701,21 +152,33 @@ void VoiceRecordButton::init() {
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_animation.init([=](crl::time now) {
|
||||
if (const auto &last = _blobsHideLastTime; (last > 0)
|
||||
&& (now - last >= kBlobsScaleEnterDuration)) {
|
||||
_animation.stop();
|
||||
return false;
|
||||
}
|
||||
_blobs->updateLevel(now - _lastUpdateTime);
|
||||
_lastUpdateTime = now;
|
||||
update();
|
||||
return true;
|
||||
});
|
||||
|
||||
rpl::merge(
|
||||
shownValue(),
|
||||
_showProgress.value(
|
||||
) | rpl::map(hasProgress) | rpl::distinct_until_changed()
|
||||
) | rpl::map(rpl::mappers::_1 != 0.) | rpl::distinct_until_changed()
|
||||
) | rpl::start_with_next([=](bool show) {
|
||||
setVisible(show);
|
||||
setMouseTracking(show);
|
||||
if (!show) {
|
||||
_recordingAnimation.stop();
|
||||
_animation.stop();
|
||||
_showProgress = 0.;
|
||||
_recordCircle->reset();
|
||||
_blobs->resetLevel();
|
||||
_state = Type::Record;
|
||||
} else {
|
||||
if (!_recordingAnimation.animating()) {
|
||||
_recordingAnimation.start();
|
||||
if (!_animation.animating()) {
|
||||
_animation.start();
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
@@ -736,7 +199,7 @@ void VoiceRecordButton::init() {
|
||||
update();
|
||||
};
|
||||
const auto duration = st::historyRecordVoiceDuration * 2;
|
||||
stateChangedAnimation->start(std::move(callback), 0., to, duration);
|
||||
_stateChangedAnimation.start(std::move(callback), 0., to, duration);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
@@ -757,8 +220,15 @@ rpl::producer<bool> VoiceRecordButton::actives() const {
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<> VoiceRecordButton::clicks() const {
|
||||
return Ui::AbstractButton::clicks(
|
||||
) | rpl::to_empty | rpl::filter([=] {
|
||||
return inCircle(mapFromGlobal(QCursor::pos()));
|
||||
});
|
||||
}
|
||||
|
||||
bool VoiceRecordButton::inCircle(const QPoint &localPos) const {
|
||||
const auto &radii = st::historyRecordLevelMaxRadius;
|
||||
const auto &radii = st::historyRecordMainBlobMaxRadius;
|
||||
const auto dx = std::abs(localPos.x() - _center);
|
||||
if (dx > radii) {
|
||||
return false;
|
||||
@@ -778,6 +248,9 @@ void VoiceRecordButton::requestPaintProgress(float64 progress) {
|
||||
}
|
||||
|
||||
void VoiceRecordButton::requestPaintColor(float64 progress) {
|
||||
if (_colorProgress == progress) {
|
||||
return;
|
||||
}
|
||||
_colorProgress = progress;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -11,9 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
namespace Ui {
|
||||
namespace Paint {
|
||||
class Blobs;
|
||||
} // namespace Paint
|
||||
} // namespace Ui
|
||||
|
||||
class RecordCircle;
|
||||
namespace HistoryView::Controls {
|
||||
|
||||
class VoiceRecordButton final : public Ui::AbstractButton {
|
||||
public:
|
||||
@@ -34,25 +38,28 @@ public:
|
||||
void requestPaintLevel(quint16 level);
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> actives() const;
|
||||
[[nodiscard]] rpl::producer<> clicks() const;
|
||||
|
||||
[[nodiscard]] bool inCircle(const QPoint &localPos) const;
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
rpl::event_stream<crl::time> _recordAnimationTicked;
|
||||
std::unique_ptr<RecordCircle> _recordCircle;
|
||||
std::unique_ptr<Ui::Paint::Blobs> _blobs;
|
||||
|
||||
crl::time _lastUpdateTime = 0;
|
||||
crl::time _blobsHideLastTime = 0;
|
||||
const int _center;
|
||||
|
||||
rpl::variable<float64> _showProgress = 0.;
|
||||
rpl::variable<float64> _colorProgress = 0.;
|
||||
float64 _colorProgress = 0.;
|
||||
rpl::variable<bool> _inCircle = false;
|
||||
rpl::variable<Type> _state = Type::Record;
|
||||
|
||||
// This can animate for a very long time (like in music playing),
|
||||
// so it should be a Basic, not a Simple animation.
|
||||
Ui::Animations::Basic _recordingAnimation;
|
||||
Ui::Animations::Basic _animation;
|
||||
Ui::Animations::Simple _stateChangedAnimation;
|
||||
};
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
@@ -287,7 +287,7 @@ void AddPostLinkAction(
|
||||
: Context::History;
|
||||
menu->addAction(
|
||||
(item->history()->peer->isMegagroup()
|
||||
? tr::lng_context_copy_link
|
||||
? tr::lng_context_copy_message_link
|
||||
: tr::lng_context_copy_post_link)(tr::now),
|
||||
[=] { CopyPostLink(session, itemId, context); });
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_group_call.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/chat/group_call_bar.h"
|
||||
#include "ui/chat/group_call_userpics.h"
|
||||
#include "ui/painter.h"
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_instance.h"
|
||||
@@ -24,7 +25,7 @@ namespace HistoryView {
|
||||
void GenerateUserpicsInRow(
|
||||
QImage &result,
|
||||
const std::vector<UserpicInRow> &list,
|
||||
const UserpicsInRowStyle &st,
|
||||
const style::GroupCallUserpics &st,
|
||||
int maxElements) {
|
||||
const auto count = int(list.size());
|
||||
if (!count) {
|
||||
@@ -67,7 +68,7 @@ GroupCallTracker::GroupCallTracker(not_null<PeerData*> peer)
|
||||
|
||||
rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st) {
|
||||
int userpicSize) {
|
||||
struct State {
|
||||
std::vector<UserpicInRow> userpics;
|
||||
Ui::GroupCallBarContent current;
|
||||
@@ -130,7 +131,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
static const auto RegenerateUserpics = [](
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st,
|
||||
int userpicSize,
|
||||
bool force = false) {
|
||||
const auto result = FillMissingUserpics(state, call) || force;
|
||||
if (!result) {
|
||||
@@ -141,7 +142,9 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
state->someUserpicsNotLoaded = false;
|
||||
for (auto &userpic : state->userpics) {
|
||||
userpic.peer->loadUserpic();
|
||||
const auto pic = userpic.peer->genUserpic(userpic.view, st.size);
|
||||
const auto pic = userpic.peer->genUserpic(
|
||||
userpic.view,
|
||||
userpicSize);
|
||||
userpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view);
|
||||
state->current.users.push_back({
|
||||
.userpic = pic.toImage(),
|
||||
@@ -161,7 +164,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
not_null<UserData*> user,
|
||||
const UserpicsInRowStyle &st) {
|
||||
int userpicSize) {
|
||||
const auto i = ranges::find(
|
||||
state->userpics,
|
||||
user,
|
||||
@@ -170,7 +173,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
return false;
|
||||
}
|
||||
state->userpics.erase(i);
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
RegenerateUserpics(state, call, userpicSize, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -178,7 +181,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
not_null<State*> state,
|
||||
not_null<Data::GroupCall*> call,
|
||||
not_null<UserData*> user,
|
||||
const UserpicsInRowStyle &st) {
|
||||
int userpicSize) {
|
||||
Expects(state->userpics.size() <= kLimit);
|
||||
|
||||
const auto &participants = call->participants();
|
||||
@@ -237,7 +240,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
}
|
||||
Assert(state->userpics.size() <= kLimit);
|
||||
}
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
RegenerateUserpics(state, call, userpicSize, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -262,12 +265,12 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
) | rpl::start_with_next([=](const ParticipantUpdate &update) {
|
||||
const auto user = update.now ? update.now->user : update.was->user;
|
||||
if (!update.now) {
|
||||
if (RemoveUserpic(state, call, user, st)) {
|
||||
if (RemoveUserpic(state, call, user, userpicSize)) {
|
||||
pushNext();
|
||||
}
|
||||
} else if (update.now->speaking
|
||||
&& (!update.was || !update.was->speaking)) {
|
||||
if (CheckPushToFront(state, call, user, st)) {
|
||||
if (CheckPushToFront(state, call, user, userpicSize)) {
|
||||
pushNext();
|
||||
}
|
||||
} else {
|
||||
@@ -287,7 +290,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
updateSpeakingState = false;
|
||||
}
|
||||
}
|
||||
if (RegenerateUserpics(state, call, st)
|
||||
if (RegenerateUserpics(state, call, userpicSize)
|
||||
|| updateSpeakingState) {
|
||||
pushNext();
|
||||
}
|
||||
@@ -296,7 +299,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
|
||||
call->participantsSliceAdded(
|
||||
) | rpl::filter([=] {
|
||||
return RegenerateUserpics(state, call, st);
|
||||
return RegenerateUserpics(state, call, userpicSize);
|
||||
}) | rpl::start_with_next(pushNext, lifetime);
|
||||
|
||||
call->peer()->session().downloaderTaskFinished(
|
||||
@@ -306,14 +309,14 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
|
||||
for (const auto &userpic : state->userpics) {
|
||||
if (userpic.peer->userpicUniqueKey(userpic.view)
|
||||
!= userpic.uniqueKey) {
|
||||
RegenerateUserpics(state, call, st, true);
|
||||
RegenerateUserpics(state, call, userpicSize, true);
|
||||
pushNext();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
RegenerateUserpics(state, call, st);
|
||||
RegenerateUserpics(state, call, userpicSize);
|
||||
|
||||
call->fullCountValue(
|
||||
) | rpl::start_with_next([=](int count) {
|
||||
@@ -345,12 +348,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::content() const {
|
||||
} else if (!call->fullCount() && !call->participantsLoaded()) {
|
||||
call->reload();
|
||||
}
|
||||
const auto st = UserpicsInRowStyle{
|
||||
.size = st::historyGroupCallUserpicSize,
|
||||
.shift = st::historyGroupCallUserpicShift,
|
||||
.stroke = st::historyGroupCallUserpicStroke,
|
||||
};
|
||||
return ContentByCall(call, st);
|
||||
return ContentByCall(call, st::historyGroupCallUserpics.size);
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ class GroupCall;
|
||||
class CloudImageView;
|
||||
} // namespace Data
|
||||
|
||||
namespace style {
|
||||
struct GroupCallUserpics;
|
||||
} // namespace style
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
struct UserpicInRow {
|
||||
@@ -27,16 +31,10 @@ struct UserpicInRow {
|
||||
mutable InMemoryKey uniqueKey;
|
||||
};
|
||||
|
||||
struct UserpicsInRowStyle {
|
||||
int size = 0;
|
||||
int shift = 0;
|
||||
int stroke = 0;
|
||||
};
|
||||
|
||||
void GenerateUserpicsInRow(
|
||||
QImage &result,
|
||||
const std::vector<UserpicInRow> &list,
|
||||
const UserpicsInRowStyle &st,
|
||||
const style::GroupCallUserpics &st,
|
||||
int maxElements = 0);
|
||||
|
||||
class GroupCallTracker final {
|
||||
@@ -48,7 +46,7 @@ public:
|
||||
|
||||
[[nodiscard]] static rpl::producer<Ui::GroupCallBarContent> ContentByCall(
|
||||
not_null<Data::GroupCall*> call,
|
||||
const UserpicsInRowStyle &st);
|
||||
int userpicSize);
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
|
||||
@@ -795,8 +795,8 @@ void Message::paintCommentsButton(
|
||||
auto &list = _comments->userpics;
|
||||
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
|
||||
const auto count = std::min(int(views->recentRepliers.size()), limit);
|
||||
const auto single = st::historyCommentsUserpicSize;
|
||||
const auto shift = st::historyCommentsUserpicOverlap;
|
||||
const auto single = st::historyCommentsUserpics.size;
|
||||
const auto shift = st::historyCommentsUserpics.shift;
|
||||
const auto regenerate = [&] {
|
||||
if (list.size() != count) {
|
||||
return true;
|
||||
@@ -828,12 +828,11 @@ void Message::paintCommentsButton(
|
||||
while (list.size() > count) {
|
||||
list.pop_back();
|
||||
}
|
||||
const auto st = UserpicsInRowStyle{
|
||||
.size = single,
|
||||
.shift = shift,
|
||||
.stroke = st::historyCommentsUserpicStroke,
|
||||
};
|
||||
GenerateUserpicsInRow(_comments->cachedUserpics, list, st, limit);
|
||||
GenerateUserpicsInRow(
|
||||
_comments->cachedUserpics,
|
||||
list,
|
||||
st::historyCommentsUserpics,
|
||||
limit);
|
||||
}
|
||||
p.drawImage(
|
||||
left,
|
||||
@@ -2135,8 +2134,8 @@ int Message::minWidthForMedia() const {
|
||||
const auto views = data()->Get<HistoryMessageViews>();
|
||||
if (data()->repliesAreComments() && !views->replies.text.isEmpty()) {
|
||||
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
|
||||
const auto single = st::historyCommentsUserpicSize;
|
||||
const auto shift = st::historyCommentsUserpicOverlap;
|
||||
const auto single = st::historyCommentsUserpics.size;
|
||||
const auto shift = st::historyCommentsUserpics.shift;
|
||||
const auto added = single
|
||||
+ (limit - 1) * (single - shift)
|
||||
+ st::historyCommentsSkipLeft
|
||||
|
||||
@@ -258,10 +258,6 @@ int InnerWidget::peerListSelectedRowsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<not_null<PeerData*>> InnerWidget::peerListCollectSelectedRows() {
|
||||
return {};
|
||||
}
|
||||
|
||||
void InnerWidget::peerListScrollToTop() {
|
||||
_scrollToRequests.fire({ -1, -1 });
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ private:
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
|
||||
void peerListScrollToTop() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
|
||||
@@ -57,7 +57,6 @@ public:
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
|
||||
void peerListScrollToTop() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
@@ -130,11 +129,6 @@ int ListDelegate::peerListSelectedRowsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto ListDelegate::peerListCollectSelectedRows()
|
||||
-> std::vector<not_null<PeerData*>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
void ListDelegate::peerListScrollToTop() {
|
||||
}
|
||||
|
||||
|
||||
@@ -493,7 +493,7 @@ void ActionsFiller::addInviteToGroupAction(
|
||||
_wrap,
|
||||
tr::lng_profile_invite_to_group(),
|
||||
CanInviteBotToGroupValue(user),
|
||||
[=] { AddBotToGroupBoxController::Start(controller, user); });
|
||||
[=] { AddBotToGroupBoxController::Start(user); });
|
||||
}
|
||||
|
||||
void ActionsFiller::addShareContactAction(not_null<UserData*> user) {
|
||||
|
||||
@@ -423,10 +423,6 @@ int Members::peerListSelectedRowsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<not_null<PeerData*>> Members::peerListCollectSelectedRows() {
|
||||
return {};
|
||||
}
|
||||
|
||||
void Members::peerListScrollToTop() {
|
||||
_scrollToRequests.fire({ -1, -1 });
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ private:
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
|
||||
void peerListScrollToTop() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
|
||||
@@ -193,9 +193,6 @@ private:
|
||||
GroupThumbs::Thumb::Thumb(Key key, Fn<void()> handler)
|
||||
: _key(key) {
|
||||
_link = std::make_shared<LambdaClickHandler>(std::move(handler));
|
||||
_fullWidth = std::min(
|
||||
wantedPixSize().width(),
|
||||
st::mediaviewGroupWidthMax);
|
||||
validateImage();
|
||||
}
|
||||
|
||||
@@ -208,9 +205,6 @@ GroupThumbs::Thumb::Thumb(
|
||||
, _photoMedia(photo->createMediaView())
|
||||
, _origin(origin) {
|
||||
_link = std::make_shared<LambdaClickHandler>(std::move(handler));
|
||||
_fullWidth = std::min(
|
||||
wantedPixSize().width(),
|
||||
st::mediaviewGroupWidthMax);
|
||||
_photoMedia->wanted(Data::PhotoSize::Thumbnail, origin);
|
||||
validateImage();
|
||||
}
|
||||
@@ -224,9 +218,6 @@ GroupThumbs::Thumb::Thumb(
|
||||
, _documentMedia(document->createMediaView())
|
||||
, _origin(origin) {
|
||||
_link = std::make_shared<LambdaClickHandler>(std::move(handler));
|
||||
_fullWidth = std::min(
|
||||
wantedPixSize().width(),
|
||||
st::mediaviewGroupWidthMax);
|
||||
_documentMedia->thumbnailWanted(origin);
|
||||
validateImage();
|
||||
}
|
||||
@@ -257,7 +248,8 @@ void GroupThumbs::Thumb::validateImage() {
|
||||
const auto originalHeight = _image->height();
|
||||
const auto takeWidth = originalWidth * st::mediaviewGroupWidthMax
|
||||
/ pixSize.width();
|
||||
const auto original = _image->original();
|
||||
auto original = _image->original();
|
||||
original.setDevicePixelRatio(cRetinaFactor());
|
||||
_full = App::pixmapFromImageInPlace(original.copy(
|
||||
(originalWidth - takeWidth) / 2,
|
||||
0,
|
||||
@@ -274,6 +266,9 @@ void GroupThumbs::Thumb::validateImage() {
|
||||
pixSize.height() * cIntRetinaFactor(),
|
||||
Images::Option::Smooth);
|
||||
}
|
||||
_fullWidth = std::min(
|
||||
wantedPixSize().width(),
|
||||
st::mediaviewGroupWidthMax);
|
||||
}
|
||||
|
||||
int GroupThumbs::Thumb::leftToUpdate() const {
|
||||
|
||||
@@ -606,28 +606,10 @@ void OverlayWidget::updateDocSize() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_document->loading()) {
|
||||
quint64 ready = _document->loadOffset(), total = _document->size;
|
||||
QString readyStr, totalStr, mb;
|
||||
if (total >= 1024 * 1024) { // more than 1 mb
|
||||
qint64 readyTenthMb = (ready * 10 / (1024 * 1024)), totalTenthMb = (total * 10 / (1024 * 1024));
|
||||
readyStr = QString::number(readyTenthMb / 10) + '.' + QString::number(readyTenthMb % 10);
|
||||
totalStr = QString::number(totalTenthMb / 10) + '.' + QString::number(totalTenthMb % 10);
|
||||
mb = qsl("MB");
|
||||
} else if (total >= 1024) {
|
||||
qint64 readyKb = (ready / 1024), totalKb = (total / 1024);
|
||||
readyStr = QString::number(readyKb);
|
||||
totalStr = QString::number(totalKb);
|
||||
mb = qsl("KB");
|
||||
} else {
|
||||
readyStr = QString::number(ready);
|
||||
totalStr = QString::number(total);
|
||||
mb = qsl("B");
|
||||
}
|
||||
_docSize = tr::lng_media_save_progress(tr::now, lt_ready, readyStr, lt_total, totalStr, lt_mb, mb);
|
||||
} else {
|
||||
_docSize = Ui::FormatSizeText(_document->size);
|
||||
}
|
||||
const auto size = _document->size;
|
||||
_docSize = _document->loading()
|
||||
? Ui::FormatProgressText(_document->loadOffset(), size)
|
||||
: Ui::FormatSizeText(size);
|
||||
_docSizeWidth = st::mediaviewFont->width(_docSize);
|
||||
int32 maxw = st::mediaviewFileSize.width() - st::mediaviewFileIconSize - st::mediaviewFilePadding * 3;
|
||||
if (_docSizeWidth > maxw) {
|
||||
@@ -739,13 +721,7 @@ void OverlayWidget::updateControls() {
|
||||
}
|
||||
return dNow;
|
||||
}();
|
||||
if (d.date() == dNow.date()) {
|
||||
_dateText = tr::lng_mediaview_today(tr::now, lt_time, d.time().toString(cTimeFormat()));
|
||||
} else if (d.date().addDays(1) == dNow.date()) {
|
||||
_dateText = tr::lng_mediaview_yesterday(tr::now, lt_time, d.time().toString(cTimeFormat()));
|
||||
} else {
|
||||
_dateText = tr::lng_mediaview_date_time(tr::now, lt_date, d.date().toString(qsl("dd.MM.yy")), lt_time, d.time().toString(cTimeFormat()));
|
||||
}
|
||||
_dateText = Ui::FormatDateTime(d, cTimeFormat());
|
||||
if (!_fromName.isEmpty()) {
|
||||
_fromNameLabel.setText(st::mediaviewTextStyle, _fromName, Ui::NameTextOptions());
|
||||
_nameNav = myrtlrect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height);
|
||||
@@ -1334,7 +1310,9 @@ void OverlayWidget::handleVisibleChanged(bool visible) {
|
||||
}
|
||||
|
||||
void OverlayWidget::handleScreenChanged(QScreen *screen) {
|
||||
moveToScreen();
|
||||
if (isVisible()) {
|
||||
moveToScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void OverlayWidget::onToMessage() {
|
||||
|
||||
@@ -50,6 +50,10 @@ Launcher::Launcher(int argc, char *argv[])
|
||||
}
|
||||
|
||||
void Launcher::initHook() {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QApplication::setAttribute(Qt::AA_DisableSessionManager, true);
|
||||
#endif // Qt >= 5.14
|
||||
|
||||
QApplication::setDesktopFileName(GetLauncherFilename());
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "platform/linux/linux_gdk_helper.h"
|
||||
|
||||
#include "platform/linux/linux_libs.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/platform/linux/base_xcb_utilities_linux.h"
|
||||
|
||||
extern "C" {
|
||||
@@ -78,17 +79,19 @@ bool GdkHelperLoaded() {
|
||||
|
||||
void XSetTransientForHint(GdkWindow *window, quintptr winId) {
|
||||
if (gdk_helper_loaded == GtkLoaded::Gtk2) {
|
||||
xcb_change_property(
|
||||
base::Platform::XCB::GetConnectionFromQt(),
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
gdk_x11_drawable_get_xid(window),
|
||||
XCB_ATOM_WM_TRANSIENT_FOR,
|
||||
XCB_ATOM_WINDOW,
|
||||
32,
|
||||
1,
|
||||
&winId);
|
||||
if (!IsWayland()) {
|
||||
xcb_change_property(
|
||||
base::Platform::XCB::GetConnectionFromQt(),
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
gdk_x11_drawable_get_xid(window),
|
||||
XCB_ATOM_WM_TRANSIENT_FOR,
|
||||
XCB_ATOM_WINDOW,
|
||||
32,
|
||||
1,
|
||||
&winId);
|
||||
}
|
||||
} else if (gdk_helper_loaded == GtkLoaded::Gtk3) {
|
||||
if (gdk_is_x11_window_check(window)) {
|
||||
if (!IsWayland() && gdk_is_x11_window_check(window)) {
|
||||
xcb_change_property(
|
||||
base::Platform::XCB::GetConnectionFromQt(),
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
|
||||
@@ -209,6 +209,10 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
48,
|
||||
};
|
||||
|
||||
static const auto dprSize = [](const QImage &image) {
|
||||
return image.size() / image.devicePixelRatio();
|
||||
};
|
||||
|
||||
for (const auto iconSize : iconSizes) {
|
||||
auto ¤tImageBack = TrayIconImageBack[iconSize];
|
||||
const auto desiredSize = QSize(iconSize, iconSize);
|
||||
@@ -221,11 +225,17 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
systemIcon = QIcon::fromTheme(iconName);
|
||||
}
|
||||
|
||||
if (systemIcon.actualSize(desiredSize) == desiredSize) {
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(desiredSize)
|
||||
.toImage();
|
||||
} else {
|
||||
// We can't use QIcon::actualSize here
|
||||
// since it works incorrectly with svg icon themes
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(desiredSize)
|
||||
.toImage();
|
||||
|
||||
const auto firstAttemptSize = dprSize(currentImageBack);
|
||||
|
||||
// if current icon theme is not a svg one, Qt can return
|
||||
// a pixmap that less in size even if there are a bigger one
|
||||
if (firstAttemptSize.width() < desiredSize.width()) {
|
||||
const auto availableSizes = systemIcon.availableSizes();
|
||||
|
||||
const auto biggestSize = ranges::max_element(
|
||||
@@ -233,18 +243,17 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
std::less<>(),
|
||||
&QSize::width);
|
||||
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(*biggestSize)
|
||||
.toImage();
|
||||
if ((*biggestSize).width() > firstAttemptSize.width()) {
|
||||
currentImageBack = systemIcon
|
||||
.pixmap(*biggestSize)
|
||||
.toImage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentImageBack = Core::App().logo();
|
||||
}
|
||||
|
||||
const auto currentImageBackSize = currentImageBack.size()
|
||||
/ currentImageBack.devicePixelRatio();
|
||||
|
||||
if (currentImageBackSize != desiredSize) {
|
||||
if (dprSize(currentImageBack) != desiredSize) {
|
||||
currentImageBack = currentImageBack.scaled(
|
||||
desiredSize * currentImageBack.devicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
@@ -331,17 +340,33 @@ std::unique_ptr<QTemporaryFile> TrayIconFile(
|
||||
static const auto templateName = AppRuntimeDirectory()
|
||||
+ kTrayIconFilename.utf16();
|
||||
|
||||
static const auto dprSize = [](const QPixmap &pixmap) {
|
||||
return pixmap.size() / pixmap.devicePixelRatio();
|
||||
};
|
||||
|
||||
static const auto desiredSize = QSize(22, 22);
|
||||
|
||||
static const auto scalePixmap = [=](const QPixmap &pixmap) {
|
||||
if (dprSize(pixmap) != desiredSize) {
|
||||
return pixmap.scaled(
|
||||
desiredSize * pixmap.devicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
} else {
|
||||
return pixmap;
|
||||
}
|
||||
};
|
||||
|
||||
auto ret = std::make_unique<QTemporaryFile>(
|
||||
templateName,
|
||||
parent);
|
||||
|
||||
ret->open();
|
||||
|
||||
if (icon.actualSize(desiredSize) == desiredSize) {
|
||||
icon.pixmap(desiredSize).save(ret.get());
|
||||
} else {
|
||||
const auto firstAttempt = icon.pixmap(desiredSize);
|
||||
const auto firstAttemptSize = dprSize(firstAttempt);
|
||||
|
||||
if (firstAttemptSize.width() < desiredSize.width()) {
|
||||
const auto availableSizes = icon.availableSizes();
|
||||
|
||||
const auto biggestSize = ranges::max_element(
|
||||
@@ -349,14 +374,13 @@ std::unique_ptr<QTemporaryFile> TrayIconFile(
|
||||
std::less<>(),
|
||||
&QSize::width);
|
||||
|
||||
const auto iconPixmap = icon.pixmap(*biggestSize);
|
||||
|
||||
iconPixmap
|
||||
.scaled(
|
||||
desiredSize * iconPixmap.devicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation)
|
||||
.save(ret.get());
|
||||
if ((*biggestSize).width() > firstAttemptSize.width()) {
|
||||
scalePixmap(icon.pixmap(*biggestSize)).save(ret.get());
|
||||
} else {
|
||||
scalePixmap(firstAttempt).save(ret.get());
|
||||
}
|
||||
} else {
|
||||
scalePixmap(firstAttempt).save(ret.get());
|
||||
}
|
||||
|
||||
ret->close();
|
||||
|
||||
@@ -42,7 +42,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/screensaver.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
|
||||
@@ -403,7 +403,7 @@ bool UnsafeShowOpenWithDropdown(const QString &filepath, QPoint menuPosition) {
|
||||
if (!screen) {
|
||||
return false;
|
||||
}
|
||||
const auto r = screen->availableGeometry();
|
||||
const auto r = screen->geometry();
|
||||
auto x = menuPosition.x();
|
||||
auto y = r.y() + r.height() - menuPosition.y();
|
||||
return !![menu popupAtX:x andY:y];
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
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 <QtCore/QtPlugin>
|
||||
|
||||
#ifndef DESKTOP_APP_USE_PACKAGED
|
||||
Q_IMPORT_PLUGIN(QWebpPlugin)
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
|
||||
Q_IMPORT_PLUGIN(QJpegPlugin)
|
||||
Q_IMPORT_PLUGIN(QGifPlugin)
|
||||
#endif // Qt 5.8.0
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin)
|
||||
#elif defined Q_OS_MAC // Q_OS_WIN
|
||||
Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QGenericEnginePlugin)
|
||||
#elif defined Q_OS_UNIX // Q_OS_WIN | Q_OS_MAC
|
||||
Q_IMPORT_PLUGIN(QXcbIntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QGenericEnginePlugin)
|
||||
Q_IMPORT_PLUGIN(QComposePlatformInputContextPlugin)
|
||||
Q_IMPORT_PLUGIN(QSvgPlugin)
|
||||
Q_IMPORT_PLUGIN(QSvgIconPlugin)
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
Q_IMPORT_PLUGIN(QConnmanEnginePlugin)
|
||||
Q_IMPORT_PLUGIN(QNetworkManagerEnginePlugin)
|
||||
Q_IMPORT_PLUGIN(QIbusPlatformInputContextPlugin)
|
||||
Q_IMPORT_PLUGIN(QXdgDesktopPortalThemePlugin)
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
#ifndef DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION
|
||||
Q_IMPORT_PLUGIN(ShmServerBufferPlugin)
|
||||
Q_IMPORT_PLUGIN(DmaBufServerBufferPlugin)
|
||||
Q_IMPORT_PLUGIN(DrmEglServerBufferPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandEglClientBufferPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandIviShellIntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandWlShellIntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandXdgShellV5IntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandXdgShellV6IntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandXdgShellIntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandBradientDecorationPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandIntegrationPlugin)
|
||||
Q_IMPORT_PLUGIN(QWaylandEglPlatformIntegrationPlugin)
|
||||
#endif // !DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION
|
||||
#endif // Q_OS_WIN | Q_OS_MAC | Q_OS_UNIX
|
||||
#endif // !DESKTOP_APP_USE_PACKAGED
|
||||
|
||||
#if defined Q_OS_UNIX && !defined Q_OS_MAC
|
||||
#if !defined DESKTOP_APP_USE_PACKAGED || defined DESKTOP_APP_USE_PACKAGED_LAZY
|
||||
Q_IMPORT_PLUGIN(NimfInputContextPlugin)
|
||||
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
Q_IMPORT_PLUGIN(QFcitxPlatformInputContextPlugin)
|
||||
Q_IMPORT_PLUGIN(QFcitx5PlatformInputContextPlugin)
|
||||
Q_IMPORT_PLUGIN(QHimePlatformInputContextPlugin)
|
||||
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
|
||||
#ifndef DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION
|
||||
Q_IMPORT_PLUGIN(QWaylandMaterialDecorationPlugin)
|
||||
#endif // !DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION
|
||||
#endif // !DESKTOP_APP_USE_PACKAGED || DESKTOP_APP_USE_PACKAGED_LAZY
|
||||
|
||||
#if !defined DESKTOP_APP_USE_PACKAGED || defined DESKTOP_APP_USE_PACKAGED_LAZY_PLATFORMTHEMES
|
||||
Q_IMPORT_PLUGIN(Qt5CTPlatformThemePlugin)
|
||||
Q_IMPORT_PLUGIN(Qt5CTStylePlugin)
|
||||
#endif // !DESKTOP_APP_USE_PACKAGED || DESKTOP_APP_USE_PACKAGED_LAZY_PLATFORMTHEMES
|
||||
#endif // Q_OS_UNIX && !Q_OS_MAC
|
||||
@@ -204,7 +204,7 @@ void Calls::setupContent() {
|
||||
),
|
||||
st::settingsButton
|
||||
)->addClickHandler([=] {
|
||||
Ui::show(ChooseAudioOutputBox(crl::guard(this, [=](
|
||||
Ui::show(ChooseAudioInputBox(crl::guard(this, [=](
|
||||
const QString &id,
|
||||
const QString &name) {
|
||||
_inputNameStream.fire_copy(name);
|
||||
|
||||
@@ -51,8 +51,7 @@ class BlockPeerBoxController
|
||||
: public ChatsListBoxController
|
||||
, private base::Subscriber {
|
||||
public:
|
||||
explicit BlockPeerBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation);
|
||||
explicit BlockPeerBoxController(not_null<Main::Session*> session);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
@@ -72,19 +71,19 @@ protected:
|
||||
private:
|
||||
void updateIsBlocked(not_null<PeerListRow*> row, PeerData *peer) const;
|
||||
|
||||
const not_null<Window::SessionNavigation*> _navigation;
|
||||
const not_null<Main::Session*> _session;
|
||||
Fn<void(not_null<PeerData*> peer)> _blockPeerCallback;
|
||||
|
||||
};
|
||||
|
||||
BlockPeerBoxController::BlockPeerBoxController(
|
||||
not_null<Window::SessionNavigation*> navigation)
|
||||
: ChatsListBoxController(navigation)
|
||||
, _navigation(navigation) {
|
||||
not_null<Main::Session*> session)
|
||||
: ChatsListBoxController(session)
|
||||
, _session(session) {
|
||||
}
|
||||
|
||||
Main::Session &BlockPeerBoxController::session() const {
|
||||
return _navigation->session();
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void BlockPeerBoxController::prepareViewHook() {
|
||||
@@ -294,7 +293,8 @@ void BlockedBoxController::handleBlockedEvent(not_null<PeerData*> user) {
|
||||
|
||||
void BlockedBoxController::BlockNewPeer(
|
||||
not_null<Window::SessionController*> window) {
|
||||
auto controller = std::make_unique<BlockPeerBoxController>(window);
|
||||
auto controller = std::make_unique<BlockPeerBoxController>(
|
||||
&window->session());
|
||||
auto initBox = [=, controller = controller.get()](
|
||||
not_null<PeerListBox*> box) {
|
||||
controller->setBlockPeerCallback([=](not_null<PeerData*> peer) {
|
||||
|
||||
@@ -38,7 +38,9 @@ bool HasExtensionFrom(const QString &file, const QStringList &extensions) {
|
||||
bool ValidPhotoForAlbum(
|
||||
const PreparedFileInformation::Image &image,
|
||||
const QString &mime) {
|
||||
if (image.animated || Core::IsMimeSticker(mime)) {
|
||||
if (image.animated
|
||||
|| Core::IsMimeSticker(mime)
|
||||
|| (mime == u"application/pdf"_q)) {
|
||||
return false;
|
||||
}
|
||||
const auto width = image.data.width();
|
||||
|
||||
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_entity.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
@@ -135,29 +136,6 @@ void EditInfoBox::setInnerFocus() {
|
||||
_field->setFocusFast();
|
||||
}
|
||||
|
||||
QString FormatDateTime(TimeId value) {
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
const auto date = base::unixtime::parse(value);
|
||||
if (date.date() == now.date()) {
|
||||
return tr::lng_mediaview_today(
|
||||
tr::now,
|
||||
lt_time,
|
||||
date.time().toString(cTimeFormat()));
|
||||
} else if (date.date().addDays(1) == now.date()) {
|
||||
return tr::lng_mediaview_yesterday(
|
||||
tr::now,
|
||||
lt_time,
|
||||
date.time().toString(cTimeFormat()));
|
||||
} else {
|
||||
return tr::lng_mediaview_date_time(
|
||||
tr::now,
|
||||
lt_date,
|
||||
date.date().toString(qsl("dd.MM.yy")),
|
||||
lt_time,
|
||||
date.time().toString(cTimeFormat()));
|
||||
}
|
||||
}
|
||||
|
||||
uint32 OccupationTag() {
|
||||
return uint32(Core::Sandbox::Instance().installationTag() & 0xFFFFFFFF);
|
||||
}
|
||||
@@ -484,7 +462,10 @@ rpl::producer<QString> Helper::infoLabelValue(
|
||||
return infoValue(
|
||||
user
|
||||
) | rpl::map([](const Support::UserInfo &info) {
|
||||
return info.author + ", " + FormatDateTime(info.date);
|
||||
const auto time = Ui::FormatDateTime(
|
||||
base::unixtime::parse(info.date),
|
||||
cTimeFormat());
|
||||
return info.author + ", " + time;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ MessageBar {
|
||||
duration: int;
|
||||
}
|
||||
|
||||
GroupCallUserpics {
|
||||
size: pixels;
|
||||
shift: pixels;
|
||||
stroke: pixels;
|
||||
align: align;
|
||||
}
|
||||
|
||||
defaultMessageBar: MessageBar {
|
||||
title: semiboldTextStyle;
|
||||
titleFg: windowActiveTextFg;
|
||||
@@ -345,14 +352,12 @@ historyRecordFont: font(13px);
|
||||
historyRecordDurationSkip: 12px;
|
||||
historyRecordDurationFg: historyComposeAreaFg;
|
||||
|
||||
historyRecordLevelMainRadius: 23px;
|
||||
historyRecordLevelMainRadiusAmplitude: 14px;
|
||||
historyRecordMajorAmplitudeRadius: 14px;
|
||||
historyRecordMinorAmplitudeRadius: 7px;
|
||||
historyRecordRandomAddition: 8px;
|
||||
historyRecordRadiusDiff: 50px;
|
||||
historyRecordRadiusDiffMin: 10px;
|
||||
historyRecordLevelMaxRadius: 70px;
|
||||
historyRecordMainBlobMinRadius: 23px;
|
||||
historyRecordMainBlobMaxRadius: 37px;
|
||||
historyRecordMinorBlobMinRadius: 40px;
|
||||
historyRecordMinorBlobMaxRadius: 47px;
|
||||
historyRecordMajorBlobMinRadius: 43px;
|
||||
historyRecordMajorBlobMaxRadius: 50px;
|
||||
|
||||
historyRecordTextStyle: TextStyle(defaultTextStyle) {
|
||||
font: historyRecordFont;
|
||||
@@ -395,6 +400,9 @@ historyRecordWaveformBar: 3px;
|
||||
|
||||
historyRecordLockPosition: point(1px, 35px);
|
||||
|
||||
historyRecordCancelButtonWidth: 100px;
|
||||
historyRecordCancelButtonFg: lightButtonFg;
|
||||
|
||||
historySilentToggle: IconButton(historyBotKeyboardShow) {
|
||||
icon: icon {{ "send_control_silent_off", historyComposeIconFg }};
|
||||
iconOver: icon {{ "send_control_silent_off", historyComposeIconFgOver }};
|
||||
@@ -738,10 +746,13 @@ historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSel
|
||||
historyCommentsButtonHeight: 40px;
|
||||
historyCommentsSkipLeft: 9px;
|
||||
historyCommentsSkipText: 10px;
|
||||
historyCommentsUserpicSize: 25px;
|
||||
historyCommentsUserpicStroke: 2px;
|
||||
historyCommentsUserpicOverlap: 6px;
|
||||
historyCommentsSkipRight: 8px;
|
||||
historyCommentsUserpics: GroupCallUserpics {
|
||||
size: 25px;
|
||||
shift: 6px;
|
||||
stroke: 2px;
|
||||
align: align(left);
|
||||
}
|
||||
|
||||
boxAttachEmoji: IconButton(historyAttachEmoji) {
|
||||
width: 30px;
|
||||
@@ -797,9 +808,12 @@ historyCommentsOpenOutSelected: icon {{ "history_comments_open", msgFileThumbLin
|
||||
|
||||
historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px);
|
||||
|
||||
historyGroupCallUserpicSize: 32px;
|
||||
historyGroupCallUserpicShift: 12px;
|
||||
historyGroupCallUserpicStroke: 4px;
|
||||
historyGroupCallUserpics: GroupCallUserpics {
|
||||
size: 32px;
|
||||
shift: 12px;
|
||||
stroke: 4px;
|
||||
align: align(top);
|
||||
}
|
||||
historyGroupCallBlobMinRadius: 23px;
|
||||
historyGroupCallBlobMaxRadius: 25px;
|
||||
|
||||
|
||||
@@ -7,12 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/group_call_bar.h"
|
||||
|
||||
#include "ui/chat/message_bar.h"
|
||||
#include "ui/chat/group_call_userpics.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
|
||||
@@ -21,75 +19,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDuration = 160;
|
||||
constexpr auto kMaxUserpics = 4;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
constexpr auto kLevelDuration = 100. + 500. * 0.23;
|
||||
constexpr auto kBlobScale = 0.605;
|
||||
constexpr auto kMinorBlobFactor = 0.9f;
|
||||
constexpr auto kUserpicMinScale = 0.8;
|
||||
constexpr auto kMaxLevel = 1.;
|
||||
constexpr auto kSendRandomLevelInterval = crl::time(100);
|
||||
|
||||
auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::historyGroupCallBlobMinRadius,
|
||||
.maxRadius = (float)st::historyGroupCallBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct GroupCallBar::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
}
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSpeakingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
};
|
||||
|
||||
struct GroupCallBar::Userpic {
|
||||
User data;
|
||||
std::pair<uint64, uint64> cacheKey;
|
||||
crl::time speakingStarted = 0;
|
||||
QImage cache;
|
||||
Animations::Simple leftAnimation;
|
||||
Animations::Simple shownAnimation;
|
||||
std::unique_ptr<BlobsAnimation> blobsAnimation;
|
||||
int left = 0;
|
||||
bool positionInited = false;
|
||||
bool topMost = false;
|
||||
bool hiding = false;
|
||||
bool cacheMasked = false;
|
||||
};
|
||||
|
||||
GroupCallBar::GroupCallBar(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<GroupCallBarContent> content)
|
||||
rpl::producer<GroupCallBarContent> content,
|
||||
rpl::producer<bool> &&hideBlobs)
|
||||
: _wrap(parent, object_ptr<RpWidget>(parent))
|
||||
, _inner(_wrap.entity())
|
||||
, _join(std::make_unique<RoundButton>(
|
||||
@@ -97,16 +31,13 @@ GroupCallBar::GroupCallBar(
|
||||
tr::lng_group_call_join(),
|
||||
st::groupCallTopBarJoin))
|
||||
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
|
||||
, _randomSpeakingTimer([=] { sendRandomLevels(); }) {
|
||||
, _userpics(std::make_unique<GroupCallUserpics>(
|
||||
st::historyGroupCallUserpics,
|
||||
std::move(hideBlobs),
|
||||
[=] { updateUserpics(); })) {
|
||||
_wrap.hide(anim::type::instant);
|
||||
_shadow->hide();
|
||||
|
||||
const auto limit = kMaxUserpics;
|
||||
const auto single = st::historyGroupCallUserpicSize;
|
||||
const auto shift = st::historyGroupCallUserpicShift;
|
||||
// + 1 * single for the blobs.
|
||||
_maxUserpicsWidth = 2 * single + (limit - 1) * (single - shift);
|
||||
|
||||
_wrap.entity()->paintRequest(
|
||||
) | rpl::start_with_next([=](QRect clip) {
|
||||
QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
|
||||
@@ -121,7 +52,7 @@ GroupCallBar::GroupCallBar(
|
||||
copy
|
||||
) | rpl::start_with_next([=](GroupCallBarContent &&content) {
|
||||
_content = content;
|
||||
updateUserpicsFromContent();
|
||||
_userpics->update(_content.users, !_wrap.isHidden());
|
||||
_inner->update();
|
||||
}, lifetime());
|
||||
|
||||
@@ -139,33 +70,10 @@ GroupCallBar::GroupCallBar(
|
||||
_wrap.toggle(false, anim::type::normal);
|
||||
}, lifetime());
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &userpic : _userpics) {
|
||||
userpic.cache = QImage();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_speakingAnimation.init([=](crl::time now) {
|
||||
//if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
|
||||
// && (now - last >= kBlobsEnterDuration)) {
|
||||
// _speakingAnimation.stop();
|
||||
// return false;
|
||||
//}
|
||||
for (auto &userpic : _userpics) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.updateLevel(now - blobs->lastTime);
|
||||
blobs->lastTime = now;
|
||||
}
|
||||
}
|
||||
updateUserpics();
|
||||
});
|
||||
|
||||
setupInner();
|
||||
}
|
||||
|
||||
GroupCallBar::~GroupCallBar() {
|
||||
}
|
||||
GroupCallBar::~GroupCallBar() = default;
|
||||
|
||||
void GroupCallBar::setupInner() {
|
||||
_inner->resize(0, st::historyReplyHeight);
|
||||
@@ -221,7 +129,7 @@ void GroupCallBar::paint(Painter &p) {
|
||||
p.setPen(st::defaultMessageBar.textFg);
|
||||
p.setFont(st::defaultMessageBar.title.font);
|
||||
p.drawTextLeft(left, titleTop, width, tr::lng_group_call_title(tr::now));
|
||||
p.setPen(st::historyComposeAreaFgService);
|
||||
p.setPen(st::historyStatusFg);
|
||||
p.setFont(st::defaultMessageBar.text.font);
|
||||
p.drawTextLeft(
|
||||
left,
|
||||
@@ -231,136 +139,11 @@ void GroupCallBar::paint(Painter &p) {
|
||||
? tr::lng_group_call_members(tr::now, lt_count, _content.count)
|
||||
: tr::lng_group_call_no_members(tr::now)));
|
||||
|
||||
const auto size = st::historyGroupCallUserpics.size;
|
||||
// Skip shadow of the bar above.
|
||||
paintUserpics(p);
|
||||
}
|
||||
|
||||
void GroupCallBar::paintUserpics(Painter &p) {
|
||||
const auto top = (st::historyReplyHeight
|
||||
- st::lineWidth
|
||||
- st::historyGroupCallUserpicSize) / 2 + st::lineWidth;
|
||||
const auto middle = _inner->width() / 2;
|
||||
const auto size = st::historyGroupCallUserpicSize;
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
for (auto &userpic : ranges::view::reverse(_userpics)) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown == 0.) {
|
||||
continue;
|
||||
}
|
||||
validateUserpicCache(userpic);
|
||||
p.setOpacity(shown);
|
||||
const auto left = middle + userpic.leftAnimation.value(userpic.left);
|
||||
const auto blobs = userpic.blobsAnimation.get();
|
||||
const auto shownScale = 0.5 + shown / 2.;
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
const auto scale = shownScale * (blobs
|
||||
? (minScale + (1. - minScale) * blobs->blobs.currentLevel())
|
||||
: 1.);
|
||||
if (blobs) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
const auto shift = QPointF(left + size / 2., top + size / 2.);
|
||||
p.translate(shift);
|
||||
blobs->blobs.paint(p, st::windowActiveTextFg);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (std::abs(scale - 1.) < 0.001) {
|
||||
const auto skip = ((kWideScale - 1) / 2) * size * factor;
|
||||
p.drawImage(
|
||||
QRect(left, top, size, size),
|
||||
userpic.cache,
|
||||
QRect(skip, skip, size * factor, size * factor));
|
||||
} else {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
auto target = QRect(
|
||||
left + (1 - kWideScale) / 2 * size,
|
||||
top + (1 - kWideScale) / 2 * size,
|
||||
kWideScale * size,
|
||||
kWideScale * size);
|
||||
auto shrink = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * size,
|
||||
0,
|
||||
scale);
|
||||
auto margins = QMargins(shrink, shrink, shrink, shrink);
|
||||
p.drawImage(target.marginsAdded(margins), userpic.cache);
|
||||
}
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
|
||||
const auto hidden = [](const Userpic &userpic) {
|
||||
return userpic.hiding && !userpic.shownAnimation.animating();
|
||||
};
|
||||
_userpics.erase(ranges::remove_if(_userpics, hidden), end(_userpics));
|
||||
}
|
||||
|
||||
bool GroupCallBar::needUserpicCacheRefresh(Userpic &userpic) {
|
||||
if (userpic.cache.isNull()) {
|
||||
return true;
|
||||
} else if (userpic.hiding) {
|
||||
return false;
|
||||
} else if (userpic.cacheKey != userpic.data.userpicKey) {
|
||||
return true;
|
||||
}
|
||||
const auto shouldBeMasked = !userpic.topMost;
|
||||
if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
|
||||
return true;
|
||||
}
|
||||
return !userpic.leftAnimation.animating();
|
||||
}
|
||||
|
||||
void GroupCallBar::ensureBlobsAnimation(Userpic &userpic) {
|
||||
if (userpic.blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
Blobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
userpic.blobsAnimation->lastTime = crl::now();
|
||||
}
|
||||
|
||||
void GroupCallBar::sendRandomLevels() {
|
||||
for (auto &userpic : _userpics) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
const auto value = 30 + (openssl::RandomValue<uint32>() % 70);
|
||||
userpic.blobsAnimation->blobs.setLevel(float64(value) / 100.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::validateUserpicCache(Userpic &userpic) {
|
||||
if (!needUserpicCacheRefresh(userpic)) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto size = st::historyGroupCallUserpicSize;
|
||||
const auto shift = st::historyGroupCallUserpicShift;
|
||||
const auto full = QSize(size, size) * kWideScale * factor;
|
||||
if (userpic.cache.isNull()) {
|
||||
userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
userpic.cache.setDevicePixelRatio(factor);
|
||||
}
|
||||
userpic.cacheKey = userpic.data.userpicKey;
|
||||
userpic.cacheMasked = !userpic.topMost;
|
||||
userpic.cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&userpic.cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
p.drawImage(skip, skip, userpic.data.userpic);
|
||||
|
||||
if (userpic.cacheMasked) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(st::historyGroupCallUserpicStroke);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(Qt::transparent);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(skip - size + shift, skip, size, size);
|
||||
}
|
||||
}
|
||||
const auto top = (st::historyReplyHeight - st::lineWidth - size) / 2
|
||||
+ st::lineWidth;
|
||||
_userpics->paint(p, _inner->width() / 2, top, size);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {
|
||||
@@ -386,127 +169,14 @@ void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) {
|
||||
: regular);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateUserpicsFromContent() {
|
||||
const auto idFromUserpic = [](const Userpic &userpic) {
|
||||
return userpic.data.id;
|
||||
};
|
||||
|
||||
// Use "topMost" as "willBeHidden" flag.
|
||||
for (auto &userpic : _userpics) {
|
||||
userpic.topMost = true;
|
||||
}
|
||||
for (const auto &user : _content.users) {
|
||||
const auto i = ranges::find(_userpics, user.id, idFromUserpic);
|
||||
if (i == end(_userpics)) {
|
||||
_userpics.push_back(Userpic{ user });
|
||||
toggleUserpic(_userpics.back(), true);
|
||||
continue;
|
||||
}
|
||||
i->topMost = false;
|
||||
|
||||
if (i->hiding) {
|
||||
toggleUserpic(*i, true);
|
||||
}
|
||||
i->data = user;
|
||||
|
||||
// Put this one after the last we are not hiding.
|
||||
for (auto j = end(_userpics) - 1; j != i; --j) {
|
||||
if (!j->topMost) {
|
||||
ranges::rotate(i, i + 1, j + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the ones that "willBeHidden" (currently having "topMost" flag).
|
||||
// Set correct real values of "topMost" flag.
|
||||
const auto userpicsBegin = begin(_userpics);
|
||||
const auto userpicsEnd = end(_userpics);
|
||||
auto markedTopMost = userpicsEnd;
|
||||
auto hasBlobs = false;
|
||||
for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
|
||||
auto &userpic = *i;
|
||||
if (userpic.data.speaking) {
|
||||
ensureBlobsAnimation(userpic);
|
||||
hasBlobs = true;
|
||||
} else {
|
||||
userpic.blobsAnimation = nullptr;
|
||||
}
|
||||
if (userpic.topMost) {
|
||||
toggleUserpic(userpic, false);
|
||||
userpic.topMost = false;
|
||||
} else if (markedTopMost == userpicsEnd) {
|
||||
userpic.topMost = true;
|
||||
markedTopMost = i;
|
||||
}
|
||||
}
|
||||
if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
|
||||
// Bring the topMost userpic to the very beginning, above all hiding.
|
||||
std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
|
||||
}
|
||||
updateUserpicsPositions();
|
||||
|
||||
if (!hasBlobs) {
|
||||
_randomSpeakingTimer.cancel();
|
||||
_speakingAnimation.stop();
|
||||
} else if (!_randomSpeakingTimer.isActive()) {
|
||||
_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
|
||||
if (_wrap.isHidden()) {
|
||||
for (auto &userpic : _userpics) {
|
||||
userpic.shownAnimation.stop();
|
||||
userpic.leftAnimation.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::toggleUserpic(Userpic &userpic, bool shown) {
|
||||
userpic.hiding = !shown;
|
||||
userpic.shownAnimation.start(
|
||||
[=] { updateUserpics(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
kDuration);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateUserpicsPositions() {
|
||||
const auto shownCount = ranges::count(_userpics, false, &Userpic::hiding);
|
||||
if (!shownCount) {
|
||||
return;
|
||||
}
|
||||
const auto single = st::historyGroupCallUserpicSize;
|
||||
const auto shift = st::historyGroupCallUserpicShift;
|
||||
// + 1 * single for the blobs.
|
||||
const auto fullWidth = single + (shownCount - 1) * (single - shift);
|
||||
auto left = (-fullWidth / 2);
|
||||
for (auto &userpic : _userpics) {
|
||||
if (userpic.hiding) {
|
||||
continue;
|
||||
}
|
||||
if (!userpic.positionInited) {
|
||||
userpic.positionInited = true;
|
||||
userpic.left = left;
|
||||
} else if (userpic.left != left) {
|
||||
userpic.leftAnimation.start(
|
||||
[=] { updateUserpics(); },
|
||||
userpic.left,
|
||||
left,
|
||||
kDuration);
|
||||
userpic.left = left;
|
||||
}
|
||||
left += (single - shift);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::updateUserpics() {
|
||||
const auto widget = _wrap.entity();
|
||||
const auto middle = widget->width() / 2;
|
||||
_wrap.entity()->update(
|
||||
(middle - _maxUserpicsWidth / 2),
|
||||
const auto width = _userpics->maxWidth();
|
||||
widget->update(
|
||||
(middle - width / 2),
|
||||
0,
|
||||
_maxUserpicsWidth,
|
||||
width,
|
||||
widget->height());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
@@ -18,24 +17,21 @@ namespace Ui {
|
||||
|
||||
class PlainShadow;
|
||||
class RoundButton;
|
||||
struct GroupCallUser;
|
||||
class GroupCallUserpics;
|
||||
|
||||
struct GroupCallBarContent {
|
||||
struct User {
|
||||
QImage userpic;
|
||||
std::pair<uint64, uint64> userpicKey = {};
|
||||
int32 id = 0;
|
||||
bool speaking = false;
|
||||
};
|
||||
int count = 0;
|
||||
bool shown = false;
|
||||
std::vector<User> users;
|
||||
std::vector<GroupCallUser> users;
|
||||
};
|
||||
|
||||
class GroupCallBar final {
|
||||
public:
|
||||
GroupCallBar(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<GroupCallBarContent> content);
|
||||
rpl::producer<GroupCallBarContent> content,
|
||||
rpl::producer<bool> &&hideBlobs);
|
||||
~GroupCallBar();
|
||||
|
||||
void show();
|
||||
@@ -57,24 +53,13 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
using User = GroupCallBarContent::User;
|
||||
struct BlobsAnimation;
|
||||
struct Userpic;
|
||||
using User = GroupCallUser;
|
||||
|
||||
void updateShadowGeometry(QRect wrapGeometry);
|
||||
void updateControlsGeometry(QRect wrapGeometry);
|
||||
void updateUserpicsFromContent();
|
||||
void updateUserpics();
|
||||
void setupInner();
|
||||
void paint(Painter &p);
|
||||
void paintUserpics(Painter &p);
|
||||
|
||||
void toggleUserpic(Userpic &userpic, bool shown);
|
||||
void updateUserpics();
|
||||
void updateUserpicsPositions();
|
||||
void validateUserpicCache(Userpic &userpic);
|
||||
[[nodiscard]] bool needUserpicCacheRefresh(Userpic &userpic);
|
||||
void ensureBlobsAnimation(Userpic &userpic);
|
||||
void sendRandomLevels();
|
||||
|
||||
SlideWrap<> _wrap;
|
||||
not_null<RpWidget*> _inner;
|
||||
@@ -82,14 +67,11 @@ private:
|
||||
std::unique_ptr<PlainShadow> _shadow;
|
||||
rpl::event_stream<> _barClicks;
|
||||
Fn<QRect(QRect)> _shadowGeometryPostprocess;
|
||||
std::vector<Userpic> _userpics;
|
||||
base::Timer _randomSpeakingTimer;
|
||||
Ui::Animations::Basic _speakingAnimation;
|
||||
int _maxUserpicsWidth = 0;
|
||||
bool _shouldBeShown = false;
|
||||
bool _forceHidden = false;
|
||||
|
||||
GroupCallBarContent _content;
|
||||
std::unique_ptr<GroupCallUserpics> _userpics;
|
||||
|
||||
};
|
||||
|
||||
|
||||
416
Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
Normal file
416
Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
Normal file
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
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_userpics.h"
|
||||
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDuration = 160;
|
||||
constexpr auto kMaxUserpics = 4;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
constexpr auto kLevelDuration = 100. + 500. * 0.23;
|
||||
constexpr auto kBlobScale = 0.605;
|
||||
constexpr auto kMinorBlobFactor = 0.9f;
|
||||
constexpr auto kUserpicMinScale = 0.8;
|
||||
constexpr auto kMaxLevel = 1.;
|
||||
constexpr auto kSendRandomLevelInterval = crl::time(100);
|
||||
|
||||
auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::historyGroupCallBlobMinRadius,
|
||||
.maxRadius = (float)st::historyGroupCallBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct GroupCallUserpics::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
}
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSpeakingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
};
|
||||
|
||||
struct GroupCallUserpics::Userpic {
|
||||
User data;
|
||||
std::pair<uint64, uint64> cacheKey;
|
||||
crl::time speakingStarted = 0;
|
||||
QImage cache;
|
||||
Animations::Simple leftAnimation;
|
||||
Animations::Simple shownAnimation;
|
||||
std::unique_ptr<BlobsAnimation> blobsAnimation;
|
||||
int left = 0;
|
||||
bool positionInited = false;
|
||||
bool topMost = false;
|
||||
bool hiding = false;
|
||||
bool cacheMasked = false;
|
||||
};
|
||||
|
||||
GroupCallUserpics::GroupCallUserpics(
|
||||
const style::GroupCallUserpics &st,
|
||||
rpl::producer<bool> &&hideBlobs,
|
||||
Fn<void()> repaint)
|
||||
: _st(st)
|
||||
, _randomSpeakingTimer([=] { sendRandomLevels(); })
|
||||
, _repaint(std::move(repaint)) {
|
||||
const auto limit = kMaxUserpics;
|
||||
const auto single = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
// + 1 * single for the blobs.
|
||||
_maxWidth = 2 * single + (limit - 1) * (single - shift);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &userpic : _list) {
|
||||
userpic.cache = QImage();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_speakingAnimation.init([=](crl::time now) {
|
||||
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
|
||||
&& (now - last >= kBlobsEnterDuration)) {
|
||||
_speakingAnimation.stop();
|
||||
}
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.updateLevel(now - blobs->lastTime);
|
||||
blobs->lastTime = now;
|
||||
}
|
||||
}
|
||||
if (const auto onstack = _repaint) {
|
||||
onstack();
|
||||
}
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
|
||||
std::move(hideBlobs)
|
||||
) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
|
||||
const auto hide = animDisabled || deactivated;
|
||||
|
||||
if (!(hide && _speakingAnimationHideLastTime)) {
|
||||
_speakingAnimationHideLastTime = hide ? crl::now() : 0;
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.setLevel(0.);
|
||||
}
|
||||
}
|
||||
if (!hide && !_speakingAnimation.animating()) {
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
GroupCallUserpics::~GroupCallUserpics() = default;
|
||||
|
||||
void GroupCallUserpics::paint(Painter &p, int x, int y, int size) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
for (auto &userpic : ranges::view::reverse(_list)) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown == 0.) {
|
||||
continue;
|
||||
}
|
||||
validateCache(userpic);
|
||||
p.setOpacity(shown);
|
||||
const auto left = x + userpic.leftAnimation.value(userpic.left);
|
||||
const auto blobs = userpic.blobsAnimation.get();
|
||||
const auto shownScale = 0.5 + shown / 2.;
|
||||
const auto scale = shownScale * (!blobs
|
||||
? 1.
|
||||
: (minScale
|
||||
+ (1. - minScale) * (_speakingAnimationHideLastTime
|
||||
? (1. - blobs->blobs.currentLevel())
|
||||
: blobs->blobs.currentLevel())));
|
||||
if (blobs) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
const auto shift = QPointF(left + size / 2., y + size / 2.);
|
||||
p.translate(shift);
|
||||
blobs->blobs.paint(p, st::windowActiveTextFg);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (std::abs(scale - 1.) < 0.001) {
|
||||
const auto skip = ((kWideScale - 1) / 2) * size * factor;
|
||||
p.drawImage(
|
||||
QRect(left, y, size, size),
|
||||
userpic.cache,
|
||||
QRect(skip, skip, size * factor, size * factor));
|
||||
} else {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
auto target = QRect(
|
||||
left + (1 - kWideScale) / 2 * size,
|
||||
y + (1 - kWideScale) / 2 * size,
|
||||
kWideScale * size,
|
||||
kWideScale * size);
|
||||
auto shrink = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * size,
|
||||
0,
|
||||
scale);
|
||||
auto margins = QMargins(shrink, shrink, shrink, shrink);
|
||||
p.drawImage(target.marginsAdded(margins), userpic.cache);
|
||||
}
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
|
||||
const auto hidden = [](const Userpic &userpic) {
|
||||
return userpic.hiding && !userpic.shownAnimation.animating();
|
||||
};
|
||||
_list.erase(ranges::remove_if(_list, hidden), end(_list));
|
||||
}
|
||||
|
||||
int GroupCallUserpics::maxWidth() const {
|
||||
return _maxWidth;
|
||||
}
|
||||
|
||||
rpl::producer<int> GroupCallUserpics::widthValue() const {
|
||||
return _width.value();
|
||||
}
|
||||
|
||||
bool GroupCallUserpics::needCacheRefresh(Userpic &userpic) {
|
||||
if (userpic.cache.isNull()) {
|
||||
return true;
|
||||
} else if (userpic.hiding) {
|
||||
return false;
|
||||
} else if (userpic.cacheKey != userpic.data.userpicKey) {
|
||||
return true;
|
||||
}
|
||||
const auto shouldBeMasked = !userpic.topMost;
|
||||
if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
|
||||
return true;
|
||||
}
|
||||
return !userpic.leftAnimation.animating();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::ensureBlobsAnimation(Userpic &userpic) {
|
||||
if (userpic.blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
Blobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
userpic.blobsAnimation->lastTime = crl::now();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::sendRandomLevels() {
|
||||
if (_skipLevelUpdate) {
|
||||
return;
|
||||
}
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
const auto value = 30 + (openssl::RandomValue<uint32>() % 70);
|
||||
userpic.blobsAnimation->blobs.setLevel(float64(value) / 100.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::validateCache(Userpic &userpic) {
|
||||
if (!needCacheRefresh(userpic)) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto size = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
const auto full = QSize(size, size) * kWideScale * factor;
|
||||
if (userpic.cache.isNull()) {
|
||||
userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
userpic.cache.setDevicePixelRatio(factor);
|
||||
}
|
||||
userpic.cacheKey = userpic.data.userpicKey;
|
||||
userpic.cacheMasked = !userpic.topMost;
|
||||
userpic.cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&userpic.cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
p.drawImage(skip, skip, userpic.data.userpic);
|
||||
|
||||
if (userpic.cacheMasked) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(_st.stroke);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(Qt::transparent);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(skip - size + shift, skip, size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::update(
|
||||
const std::vector<GroupCallUser> &users,
|
||||
bool visible) {
|
||||
const auto idFromUserpic = [](const Userpic &userpic) {
|
||||
return userpic.data.id;
|
||||
};
|
||||
|
||||
// Use "topMost" as "willBeHidden" flag.
|
||||
for (auto &userpic : _list) {
|
||||
userpic.topMost = true;
|
||||
}
|
||||
for (const auto &user : users) {
|
||||
const auto i = ranges::find(_list, user.id, idFromUserpic);
|
||||
if (i == end(_list)) {
|
||||
_list.push_back(Userpic{ user });
|
||||
toggle(_list.back(), true);
|
||||
continue;
|
||||
}
|
||||
i->topMost = false;
|
||||
|
||||
if (i->hiding) {
|
||||
toggle(*i, true);
|
||||
}
|
||||
i->data = user;
|
||||
|
||||
// Put this one after the last we are not hiding.
|
||||
for (auto j = end(_list) - 1; j != i; --j) {
|
||||
if (!j->topMost) {
|
||||
ranges::rotate(i, i + 1, j + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the ones that "willBeHidden" (currently having "topMost" flag).
|
||||
// Set correct real values of "topMost" flag.
|
||||
const auto userpicsBegin = begin(_list);
|
||||
const auto userpicsEnd = end(_list);
|
||||
auto markedTopMost = userpicsEnd;
|
||||
auto hasBlobs = false;
|
||||
for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
|
||||
auto &userpic = *i;
|
||||
if (userpic.data.speaking) {
|
||||
ensureBlobsAnimation(userpic);
|
||||
hasBlobs = true;
|
||||
} else {
|
||||
userpic.blobsAnimation = nullptr;
|
||||
}
|
||||
if (userpic.topMost) {
|
||||
toggle(userpic, false);
|
||||
userpic.topMost = false;
|
||||
} else if (markedTopMost == userpicsEnd) {
|
||||
userpic.topMost = true;
|
||||
markedTopMost = i;
|
||||
}
|
||||
}
|
||||
if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
|
||||
// Bring the topMost userpic to the very beginning, above all hiding.
|
||||
std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
|
||||
}
|
||||
updatePositions();
|
||||
|
||||
if (!hasBlobs) {
|
||||
_randomSpeakingTimer.cancel();
|
||||
_speakingAnimation.stop();
|
||||
} else if (!_randomSpeakingTimer.isActive()) {
|
||||
_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
for (auto &userpic : _list) {
|
||||
userpic.shownAnimation.stop();
|
||||
userpic.leftAnimation.stop();
|
||||
}
|
||||
}
|
||||
recountAndRepaint();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::toggle(Userpic &userpic, bool shown) {
|
||||
userpic.hiding = !shown;
|
||||
userpic.shownAnimation.start(
|
||||
[=] { recountAndRepaint(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
kDuration);
|
||||
}
|
||||
|
||||
void GroupCallUserpics::updatePositions() {
|
||||
const auto shownCount = ranges::count(_list, false, &Userpic::hiding);
|
||||
if (!shownCount) {
|
||||
return;
|
||||
}
|
||||
const auto single = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
// + 1 * single for the blobs.
|
||||
const auto fullWidth = single + (shownCount - 1) * (single - shift);
|
||||
auto left = (_st.align & Qt::AlignLeft)
|
||||
? 0
|
||||
: (_st.align & Qt::AlignHCenter)
|
||||
? (-fullWidth / 2)
|
||||
: -fullWidth;
|
||||
for (auto &userpic : _list) {
|
||||
if (userpic.hiding) {
|
||||
continue;
|
||||
}
|
||||
if (!userpic.positionInited) {
|
||||
userpic.positionInited = true;
|
||||
userpic.left = left;
|
||||
} else if (userpic.left != left) {
|
||||
userpic.leftAnimation.start(
|
||||
_repaint,
|
||||
userpic.left,
|
||||
left,
|
||||
kDuration);
|
||||
userpic.left = left;
|
||||
}
|
||||
left += (single - shift);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::recountAndRepaint() {
|
||||
auto width = 0;
|
||||
auto maxShown = 0.;
|
||||
for (const auto &userpic : _list) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown > maxShown) {
|
||||
maxShown = shown;
|
||||
}
|
||||
width += anim::interpolate(0, _st.size - _st.shift, shown);
|
||||
}
|
||||
_width = width + anim::interpolate(0, _st.shift, maxShown);
|
||||
if (_repaint) {
|
||||
_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
73
Telegram/SourceFiles/ui/chat/group_call_userpics.h
Normal file
73
Telegram/SourceFiles/ui/chat/group_call_userpics.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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/timer.h"
|
||||
|
||||
namespace style {
|
||||
struct GroupCallUserpics;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct GroupCallUser {
|
||||
QImage userpic;
|
||||
std::pair<uint64, uint64> userpicKey = {};
|
||||
int32 id = 0;
|
||||
bool speaking = false;
|
||||
};
|
||||
|
||||
class GroupCallUserpics final {
|
||||
public:
|
||||
GroupCallUserpics(
|
||||
const style::GroupCallUserpics &st,
|
||||
rpl::producer<bool> &&hideBlobs,
|
||||
Fn<void()> repaint);
|
||||
~GroupCallUserpics();
|
||||
|
||||
void update(
|
||||
const std::vector<GroupCallUser> &users,
|
||||
bool visible);
|
||||
void paint(Painter &p, int x, int y, int size);
|
||||
|
||||
[[nodiscard]] int maxWidth() const;
|
||||
[[nodiscard]] rpl::producer<int> widthValue() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
using User = GroupCallUser;
|
||||
struct BlobsAnimation;
|
||||
struct Userpic;
|
||||
|
||||
void toggle(Userpic &userpic, bool shown);
|
||||
void updatePositions();
|
||||
void validateCache(Userpic &userpic);
|
||||
[[nodiscard]] bool needCacheRefresh(Userpic &userpic);
|
||||
void ensureBlobsAnimation(Userpic &userpic);
|
||||
void sendRandomLevels();
|
||||
void recountAndRepaint();
|
||||
|
||||
const style::GroupCallUserpics &_st;
|
||||
std::vector<Userpic> _list;
|
||||
base::Timer _randomSpeakingTimer;
|
||||
Fn<void()> _repaint;
|
||||
Ui::Animations::Basic _speakingAnimation;
|
||||
int _maxWidth = 0;
|
||||
bool _skipLevelUpdate = false;
|
||||
crl::time _speakingAnimationHideLastTime = 0;
|
||||
|
||||
rpl::variable<int> _width;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -45,6 +45,11 @@ MessageBar::MessageBar(not_null<QWidget*> parent, const style::MessageBar &st)
|
||||
: _st(st)
|
||||
, _widget(parent) {
|
||||
setup();
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_topBarGradient = _bottomBarGradient = QPixmap();
|
||||
}, _widget.lifetime());
|
||||
}
|
||||
|
||||
void MessageBar::setup() {
|
||||
|
||||
@@ -13,24 +13,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Ui {
|
||||
|
||||
QString FormatSizeText(qint64 size) {
|
||||
if (size >= 1024 * 1024) { // more than 1 mb
|
||||
qint64 sizeTenthMb = (size * 10 / (1024 * 1024));
|
||||
return QString::number(sizeTenthMb / 10) + '.' + QString::number(sizeTenthMb % 10) + u" MB"_q;
|
||||
}
|
||||
if (size >= 1024) {
|
||||
qint64 sizeTenthKb = (size * 10 / 1024);
|
||||
return QString::number(sizeTenthKb / 10) + '.' + QString::number(sizeTenthKb % 10) + u" KB"_q;
|
||||
}
|
||||
return QString::number(size) + u" B"_q;
|
||||
}
|
||||
namespace {
|
||||
|
||||
QString FormatDownloadText(qint64 ready, qint64 total) {
|
||||
QString FormatTextWithReadyAndTotal(
|
||||
tr::phrase<lngtag_ready, lngtag_total, lngtag_mb> phrase,
|
||||
qint64 ready,
|
||||
qint64 total) {
|
||||
QString readyStr, totalStr, mb;
|
||||
if (total >= 1024 * 1024) { // more than 1 mb
|
||||
qint64 readyTenthMb = (ready * 10 / (1024 * 1024)), totalTenthMb = (total * 10 / (1024 * 1024));
|
||||
readyStr = QString::number(readyTenthMb / 10) + '.' + QString::number(readyTenthMb % 10);
|
||||
totalStr = QString::number(totalTenthMb / 10) + '.' + QString::number(totalTenthMb % 10);
|
||||
const qint64 readyTenthMb = (ready * 10 / (1024 * 1024));
|
||||
const qint64 totalTenthMb = (total * 10 / (1024 * 1024));
|
||||
readyStr = QString::number(readyTenthMb / 10)
|
||||
+ '.'
|
||||
+ QString::number(readyTenthMb % 10);
|
||||
totalStr = QString::number(totalTenthMb / 10)
|
||||
+ '.'
|
||||
+ QString::number(totalTenthMb % 10);
|
||||
mb = u"MB"_q;
|
||||
} else if (total >= 1024) {
|
||||
qint64 readyKb = (ready / 1024), totalKb = (total / 1024);
|
||||
@@ -42,7 +40,61 @@ QString FormatDownloadText(qint64 ready, qint64 total) {
|
||||
totalStr = QString::number(total);
|
||||
mb = u"B"_q;
|
||||
}
|
||||
return tr::lng_save_downloaded(tr::now, lt_ready, readyStr, lt_total, totalStr, lt_mb, mb);
|
||||
return phrase(tr::now, lt_ready, readyStr, lt_total, totalStr, lt_mb, mb);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QString FormatSizeText(qint64 size) {
|
||||
if (size >= 1024 * 1024) { // more than 1 mb
|
||||
const qint64 sizeTenthMb = (size * 10 / (1024 * 1024));
|
||||
return QString::number(sizeTenthMb / 10)
|
||||
+ '.'
|
||||
+ QString::number(sizeTenthMb % 10) + u" MB"_q;
|
||||
}
|
||||
if (size >= 1024) {
|
||||
const qint64 sizeTenthKb = (size * 10 / 1024);
|
||||
return QString::number(sizeTenthKb / 10)
|
||||
+ '.'
|
||||
+ QString::number(sizeTenthKb % 10) + u" KB"_q;
|
||||
}
|
||||
return QString::number(size) + u" B"_q;
|
||||
}
|
||||
|
||||
QString FormatDownloadText(qint64 ready, qint64 total) {
|
||||
return FormatTextWithReadyAndTotal(
|
||||
tr::lng_save_downloaded,
|
||||
ready,
|
||||
total);
|
||||
}
|
||||
|
||||
QString FormatProgressText(qint64 ready, qint64 total) {
|
||||
return FormatTextWithReadyAndTotal(
|
||||
tr::lng_media_save_progress,
|
||||
ready,
|
||||
total);
|
||||
}
|
||||
|
||||
QString FormatDateTime(QDateTime date, QString format) {
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
if (date.date() == now.date()) {
|
||||
return tr::lng_mediaview_today(
|
||||
tr::now,
|
||||
lt_time,
|
||||
date.time().toString(format));
|
||||
} else if (date.date().addDays(1) == now.date()) {
|
||||
return tr::lng_mediaview_yesterday(
|
||||
tr::now,
|
||||
lt_time,
|
||||
date.time().toString(format));
|
||||
} else {
|
||||
return tr::lng_mediaview_date_time(
|
||||
tr::now,
|
||||
lt_date,
|
||||
date.date().toString(u"dd.MM.yy"_q),
|
||||
lt_time,
|
||||
date.time().toString(format));
|
||||
}
|
||||
}
|
||||
|
||||
QString FormatDurationText(qint64 duration) {
|
||||
|
||||
@@ -15,6 +15,8 @@ inline constexpr auto FileStatusSizeFailed = 0x7FFFFFF2;
|
||||
|
||||
[[nodiscard]] QString FormatSizeText(qint64 size);
|
||||
[[nodiscard]] QString FormatDownloadText(qint64 ready, qint64 total);
|
||||
[[nodiscard]] QString FormatProgressText(qint64 ready, qint64 total);
|
||||
[[nodiscard]] QString FormatDateTime(QDateTime date, QString format);
|
||||
[[nodiscard]] QString FormatDurationText(qint64 duration);
|
||||
[[nodiscard]] QString FormatDurationWords(qint64 duration);
|
||||
[[nodiscard]] QString FormatDurationAndSizeText(qint64 duration, qint64 size);
|
||||
|
||||
@@ -483,7 +483,7 @@ void Filler::addUserActions(not_null<UserData*> user) {
|
||||
using AddBotToGroup = AddBotToGroupBoxController;
|
||||
_addAction(
|
||||
tr::lng_profile_invite_to_group(tr::now),
|
||||
[=] { AddBotToGroup::Start(controller, user); });
|
||||
[=] { AddBotToGroup::Start(user); });
|
||||
}
|
||||
addPollAction(user);
|
||||
if (user->canExportChatHistory()) {
|
||||
@@ -517,7 +517,7 @@ void Filler::addChatActions(not_null<ChatData*> chat) {
|
||||
}
|
||||
if (chat->canAddMembers()) {
|
||||
_addAction(
|
||||
tr::lng_profile_add_participant(tr::now),
|
||||
tr::lng_channel_add_members(tr::now),
|
||||
[=] { AddChatMembers(controller, chat); });
|
||||
}
|
||||
addPollAction(chat);
|
||||
@@ -567,7 +567,9 @@ void Filler::addChannelActions(not_null<ChannelData*> channel) {
|
||||
}
|
||||
if (channel->canAddMembers()) {
|
||||
_addAction(
|
||||
tr::lng_channel_add_members(tr::now),
|
||||
(channel->isMegagroup()
|
||||
? tr::lng_channel_add_members(tr::now)
|
||||
: tr::lng_channel_add_users(tr::now)),
|
||||
[=] { PeerMenuAddChannelMembers(navigation, channel); });
|
||||
}
|
||||
addPollAction(channel);
|
||||
@@ -803,7 +805,7 @@ void PeerMenuShareContactBox(
|
||||
};
|
||||
*weak = Ui::show(Box<PeerListBox>(
|
||||
std::make_unique<ChooseRecipientBoxController>(
|
||||
navigation,
|
||||
&navigation->session(),
|
||||
std::move(callback)),
|
||||
[](not_null<PeerListBox*> box) {
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
@@ -1010,7 +1012,7 @@ QPointer<Ui::RpWidget> ShowForwardMessagesBox(
|
||||
};
|
||||
*weak = Ui::show(Box<PeerListBox>(
|
||||
std::make_unique<ChooseRecipientBoxController>(
|
||||
navigation,
|
||||
&navigation->session(),
|
||||
std::move(callback)),
|
||||
std::move(initBox)), Ui::LayerOption::KeepOther);
|
||||
return weak->data();
|
||||
|
||||
@@ -194,7 +194,7 @@ void SessionNavigation::showPeerByLinkResolved(
|
||||
const auto user = peer->asUser();
|
||||
if (user && user->isBot() && !info.startToken.isEmpty()) {
|
||||
user->botInfo->shareGameShortName = info.startToken;
|
||||
AddBotToGroupBoxController::Start(this, user);
|
||||
AddBotToGroupBoxController::Start(user);
|
||||
} else {
|
||||
crl::on_main(this, [=] {
|
||||
showPeerHistory(peer->id, params);
|
||||
@@ -207,7 +207,7 @@ void SessionNavigation::showPeerByLinkResolved(
|
||||
&& !user->botInfo->cantJoinGroups
|
||||
&& !info.startToken.isEmpty()) {
|
||||
user->botInfo->startGroupToken = info.startToken;
|
||||
AddBotToGroupBoxController::Start(this, user);
|
||||
AddBotToGroupBoxController::Start(user);
|
||||
} else if (user && user->isBot()) {
|
||||
// Always open bot chats, even from mention links.
|
||||
crl::on_main(this, [=] {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user