Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78937d716f | ||
|
|
9713abc002 | ||
|
|
b44b45cca0 | ||
|
|
9e2cf0ed73 | ||
|
|
b01d7ea5b9 | ||
|
|
ae89b65a98 | ||
|
|
9b9c3d788d | ||
|
|
ccc6c6daa5 | ||
|
|
9ce6636c6a | ||
|
|
6287d306c2 | ||
|
|
6cfa053328 | ||
|
|
9514b6eecd | ||
|
|
c8d4818d22 | ||
|
|
4142ada729 | ||
|
|
d7ffdbd78d | ||
|
|
e8d87d37bb | ||
|
|
343ffc23eb | ||
|
|
95e0086eed | ||
|
|
c010ecfe38 | ||
|
|
302e9371c8 | ||
|
|
7060c0e6d7 | ||
|
|
20a4c7f9f4 | ||
|
|
e59e4afd3e | ||
|
|
f74dd3ca1e | ||
|
|
511cfc524f | ||
|
|
4cf6173d25 | ||
|
|
17996757fd | ||
|
|
6bc1049858 | ||
|
|
ff44f626ba | ||
|
|
552343fa37 | ||
|
|
4dc7fd8cd1 | ||
|
|
285c96fd2e | ||
|
|
e6af33367e | ||
|
|
7092fe2242 | ||
|
|
a32ff46579 | ||
|
|
7f20cf59d1 | ||
|
|
a8a1b08127 | ||
|
|
1a1e777b87 | ||
|
|
9e76e64064 | ||
|
|
975ae17ef9 | ||
|
|
ed9dcef66f | ||
|
|
b1e537e54e | ||
|
|
e5886862c3 | ||
|
|
d85b668d4f | ||
|
|
b363d8bfb5 | ||
|
|
754d467440 | ||
|
|
598f08d6c7 | ||
|
|
224fdc1864 | ||
|
|
e646b4dc9a | ||
|
|
9077db2e97 | ||
|
|
7e14277ead | ||
|
|
d351a7d697 | ||
|
|
70ed43b811 | ||
|
|
913083ebc6 | ||
|
|
588a95a7ae | ||
|
|
c0a0ad4ec5 | ||
|
|
5eafe96525 |
@@ -1162,6 +1162,8 @@ PRIVATE
|
||||
media/streaming/media_streaming_player.h
|
||||
media/streaming/media_streaming_reader.cpp
|
||||
media/streaming/media_streaming_reader.h
|
||||
media/streaming/media_streaming_round_preview.cpp
|
||||
media/streaming/media_streaming_round_preview.h
|
||||
media/streaming/media_streaming_utility.cpp
|
||||
media/streaming/media_streaming_utility.h
|
||||
media/streaming/media_streaming_video_track.cpp
|
||||
|
||||
BIN
Telegram/Resources/icons/chat/input_video.png
Normal file
BIN
Telegram/Resources/icons/chat/input_video.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 621 B |
BIN
Telegram/Resources/icons/chat/input_video@2x.png
Normal file
BIN
Telegram/Resources/icons/chat/input_video@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/chat/input_video@3x.png
Normal file
BIN
Telegram/Resources/icons/chat/input_video@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
Telegram/Resources/icons/voice_lock/input_round_s.png
Normal file
BIN
Telegram/Resources/icons/voice_lock/input_round_s.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 498 B |
BIN
Telegram/Resources/icons/voice_lock/input_round_s@2x.png
Normal file
BIN
Telegram/Resources/icons/voice_lock/input_round_s@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/voice_lock/input_round_s@3x.png
Normal file
BIN
Telegram/Resources/icons/voice_lock/input_round_s@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -12,6 +12,7 @@ body {
|
||||
margin: 0;
|
||||
background-color: var(--td-window-bg);
|
||||
color: var(--td-window-fg);
|
||||
zoom: var(--td-zoom-percentage);
|
||||
}
|
||||
|
||||
html.custom_scroll ::-webkit-scrollbar {
|
||||
|
||||
@@ -499,8 +499,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_notify_global" = "Global settings";
|
||||
"lng_settings_notify_title" = "Notifications for chats";
|
||||
"lng_settings_desktop_notify" = "Desktop notifications";
|
||||
"lng_settings_native_title" = "Native notifications";
|
||||
"lng_settings_native_title" = "System integration";
|
||||
"lng_settings_use_windows" = "Use Windows notifications";
|
||||
"lng_settings_skip_in_focus" = "Respect system Focus mode";
|
||||
"lng_settings_use_native_notifications" = "Use native notifications";
|
||||
"lng_settings_notifications_position" = "Location on the screen";
|
||||
"lng_settings_notifications_count" = "Notifications count";
|
||||
@@ -2151,6 +2152,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_media_cancel" = "Cancel";
|
||||
"lng_media_video" = "Video";
|
||||
"lng_media_audio" = "Voice message";
|
||||
"lng_media_round" = "Video message";
|
||||
|
||||
"lng_media_auto_settings" = "Automatic media download";
|
||||
"lng_media_auto_in_private" = "In private chats";
|
||||
@@ -3243,9 +3245,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_record_cancel" = "Release outside this field to cancel";
|
||||
"lng_record_cancel_stories" = "Release outside to cancel";
|
||||
"lng_record_lock_cancel_sure" = "Do you want to stop recording and discard your voice message?";
|
||||
"lng_record_lock_cancel_sure_round" = "Do you want to stop recording and discard your video message?";
|
||||
"lng_record_listen_cancel_sure" = "Do you want to discard your recorded voice message?";
|
||||
"lng_record_listen_cancel_sure_round" = "Do you want to discard your recorded video message?";
|
||||
"lng_record_lock_discard" = "Discard";
|
||||
"lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message.";
|
||||
"lng_record_voice_tip" = "Hold to record audio. Click to switch to video.";
|
||||
"lng_record_video_tip" = "Hold to record video. Click to switch to audio.";
|
||||
"lng_record_audio_problem" = "Could not start audio recording. Please check your microphone.";
|
||||
"lng_record_video_problem" = "Could not start video recording. Please check your camera.";
|
||||
"lng_record_once_first_tooltip" = "Click to set this message to **Play Once**.";
|
||||
"lng_record_once_active_tooltip" = "The recipient will be able to listen only once.";
|
||||
"lng_will_be_notified" = "Subscribers will be notified when you post.";
|
||||
@@ -3359,6 +3366,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_bot_settings" = "Settings";
|
||||
"lng_bot_open" = "Open Bot";
|
||||
"lng_bot_terms" = "Terms of Use";
|
||||
"lng_bot_privacy" = "Privacy Policy";
|
||||
"lng_bot_reload_page" = "Reload Page";
|
||||
"lng_bot_add_to_menu" = "{bot} asks your permission to be added as an option to your attachment menu so you can access it from any chat.";
|
||||
"lng_bot_add_to_menu_done" = "Bot added to the menu.";
|
||||
@@ -3576,6 +3584,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
|
||||
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
|
||||
|
||||
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
|
||||
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
|
||||
"lng_context_noforwards_info_bot" = "Copying and forwarding is not allowed from this bot.";
|
||||
|
||||
"lng_context_spoiler_effect" = "Hide with Spoiler";
|
||||
"lng_context_disable_spoiler" = "Remove Spoiler";
|
||||
"lng_context_make_paid" = "Make This Content Paid";
|
||||
@@ -3678,6 +3690,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
"lng_reply_in_another_title" = "Reply in...";
|
||||
"lng_reply_in_another_chat" = "Reply in Another Chat";
|
||||
"lng_reply_in_author" = "Message author";
|
||||
"lng_reply_in_chats_list" = "Your chats";
|
||||
"lng_reply_show_in_chat" = "Show in Chat";
|
||||
"lng_reply_remove" = "Do Not Reply";
|
||||
"lng_reply_about_quote" = "You can select a specific part to quote.";
|
||||
@@ -5558,6 +5572,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_iv_window_title" = "Instant View";
|
||||
"lng_iv_wrong_layout" = "Wrong layout?";
|
||||
"lng_iv_not_supported" = "This link appears to be invalid.";
|
||||
"lng_iv_zoom_tooltip_ctrl" = "Hold Ctrl to zoom by 5%.\nHold Alt to zoom by 1%.";
|
||||
"lng_iv_zoom_tooltip_cmd" = "Hold Cmd to zoom by 5%.\nHold Alt to zoom by 1%.";
|
||||
|
||||
"lng_limit_download_title" = "Download speed limited";
|
||||
"lng_limit_download_subscribe" = "Subscribe to {link} to increase download speed {increase}.";
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="5.6.2.0" />
|
||||
Version="5.6.4.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,6,2,0
|
||||
PRODUCTVERSION 5,6,2,0
|
||||
FILEVERSION 5,6,4,0
|
||||
PRODUCTVERSION 5,6,4,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "5.6.2.0"
|
||||
VALUE "FileVersion", "5.6.4.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.6.2.0"
|
||||
VALUE "ProductVersion", "5.6.4.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,6,2,0
|
||||
PRODUCTVERSION 5,6,2,0
|
||||
FILEVERSION 5,6,4,0
|
||||
PRODUCTVERSION 5,6,4,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", "5.6.2.0"
|
||||
VALUE "FileVersion", "5.6.4.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.6.2.0"
|
||||
VALUE "ProductVersion", "5.6.4.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "settings/settings_credits_graphics.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/effects/premium_stars_colored.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
@@ -129,6 +130,7 @@ void ConfirmSubscriptionBox(
|
||||
struct State final {
|
||||
std::shared_ptr<Data::PhotoMedia> photoMedia;
|
||||
std::unique_ptr<Ui::EmptyUserpic> photoEmpty;
|
||||
QImage frame;
|
||||
|
||||
std::optional<MTP::Sender> api;
|
||||
Ui::RpWidget* saveButton = nullptr;
|
||||
@@ -146,25 +148,45 @@ void ConfirmSubscriptionBox(
|
||||
const auto userpic = userpicWrap->entity();
|
||||
const auto photoSize = st::confirmInvitePhotoSize;
|
||||
userpic->resize(Size(photoSize));
|
||||
const auto creditsIconSize = photoSize / 3;
|
||||
const auto creditsIconCallback =
|
||||
Ui::PaintOutlinedColoredCreditsIconCallback(
|
||||
creditsIconSize,
|
||||
1.5);
|
||||
state->frame = QImage(
|
||||
Size(photoSize * style::DevicePixelRatio()),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
state->frame.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
const auto options = Images::Option::RoundCircle;
|
||||
userpic->paintRequest(
|
||||
) | rpl::start_with_next([=, small = Data::PhotoSize::Small] {
|
||||
auto p = QPainter(userpic);
|
||||
if (state->photoMedia) {
|
||||
if (const auto image = state->photoMedia->image(small)) {
|
||||
p.drawPixmap(
|
||||
state->frame.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&state->frame);
|
||||
if (state->photoMedia) {
|
||||
if (const auto image = state->photoMedia->image(small)) {
|
||||
p.drawPixmap(
|
||||
0,
|
||||
0,
|
||||
image->pix(Size(photoSize), { .options = options }));
|
||||
}
|
||||
} else if (state->photoEmpty) {
|
||||
state->photoEmpty->paintCircle(
|
||||
p,
|
||||
0,
|
||||
0,
|
||||
image->pix(Size(photoSize), { .options = options }));
|
||||
userpic->width(),
|
||||
photoSize);
|
||||
}
|
||||
if (creditsIconCallback) {
|
||||
p.translate(
|
||||
photoSize - creditsIconSize,
|
||||
photoSize - creditsIconSize);
|
||||
creditsIconCallback(p);
|
||||
}
|
||||
} else if (state->photoEmpty) {
|
||||
state->photoEmpty->paintCircle(
|
||||
p,
|
||||
0,
|
||||
0,
|
||||
userpic->width(),
|
||||
photoSize);
|
||||
}
|
||||
auto p = QPainter(userpic);
|
||||
p.drawImage(0, 0, state->frame);
|
||||
}, userpicWrap->lifetime());
|
||||
userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
if (photo) {
|
||||
|
||||
@@ -456,6 +456,7 @@ void SendConfirmedFile(
|
||||
not_null<Main::Session*> session,
|
||||
const std::shared_ptr<FilePrepareResult> &file) {
|
||||
const auto isEditing = (file->type != SendMediaType::Audio)
|
||||
&& (file->type != SendMediaType::Round)
|
||||
&& (file->to.replaceMediaOf != 0);
|
||||
const auto newId = FullMsgId(
|
||||
file->to.peer,
|
||||
@@ -525,7 +526,8 @@ void SendConfirmedFile(
|
||||
// Shortcut messages have no 'edited' badge.
|
||||
flags |= MessageFlag::HideEdited;
|
||||
}
|
||||
if (file->type == SendMediaType::Audio) {
|
||||
if (file->type == SendMediaType::Audio
|
||||
|| file->type == SendMediaType::Round) {
|
||||
if (!peer->isChannel() || peer->isMegagroup()) {
|
||||
flags |= MessageFlag::MediaIsUnread;
|
||||
}
|
||||
@@ -551,29 +553,25 @@ void SendConfirmedFile(
|
||||
MTPint());
|
||||
} else if (file->type == SendMediaType::Audio) {
|
||||
const auto ttlSeconds = file->to.options.ttlSeconds;
|
||||
const auto isVoice = [&] {
|
||||
return file->document.match([](const MTPDdocumentEmpty &d) {
|
||||
return false;
|
||||
}, [](const MTPDdocument &d) {
|
||||
return ranges::any_of(d.vattributes().v, [&](
|
||||
const MTPDocumentAttribute &attribute) {
|
||||
using Att = MTPDdocumentAttributeAudio;
|
||||
return attribute.match([](const Att &data) -> bool {
|
||||
return data.vflags().v & Att::Flag::f_voice;
|
||||
}, [](const auto &) {
|
||||
return false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}();
|
||||
using Flag = MTPDmessageMediaDocument::Flag;
|
||||
return MTP_messageMediaDocument(
|
||||
MTP_flags(Flag::f_document
|
||||
| (isVoice ? Flag::f_voice : Flag())
|
||||
| Flag::f_voice
|
||||
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())),
|
||||
file->document,
|
||||
MTPVector<MTPDocument>(), // alt_documents
|
||||
MTP_int(ttlSeconds));
|
||||
} else if (file->type == SendMediaType::Round) {
|
||||
using Flag = MTPDmessageMediaDocument::Flag;
|
||||
const auto ttlSeconds = file->to.options.ttlSeconds;
|
||||
return MTP_messageMediaDocument(
|
||||
MTP_flags(Flag::f_document
|
||||
| Flag::f_round
|
||||
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())
|
||||
| (file->spoiler ? Flag::f_spoiler : Flag())),
|
||||
file->document,
|
||||
MTPVector<MTPDocument>(), // alt_documents
|
||||
MTP_int(ttlSeconds));
|
||||
} else {
|
||||
Unexpected("Type in sendFilesConfirmed.");
|
||||
}
|
||||
|
||||
@@ -3502,6 +3502,7 @@ void ApiWrap::sendVoiceMessage(
|
||||
QByteArray result,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration,
|
||||
bool video,
|
||||
const SendAction &action) {
|
||||
const auto caption = TextWithTags();
|
||||
const auto to = FileLoadTaskOptions(action);
|
||||
@@ -3510,6 +3511,7 @@ void ApiWrap::sendVoiceMessage(
|
||||
result,
|
||||
duration,
|
||||
waveform,
|
||||
video,
|
||||
to,
|
||||
caption));
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ public:
|
||||
QByteArray result,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration,
|
||||
bool video,
|
||||
const SendAction &action);
|
||||
void sendFiles(
|
||||
Ui::PreparedList &&list,
|
||||
|
||||
@@ -154,9 +154,7 @@ contactsSortButton: IconButton(defaultIconButton) {
|
||||
iconPosition: point(10px, -1px);
|
||||
rippleAreaPosition: point(1px, 6px);
|
||||
rippleAreaSize: 42px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
contactsSortOnlineIcon: icon{{ "contacts_online", boxTitleCloseFg }};
|
||||
contactsSortOnlineIconOver: icon{{ "contacts_online", boxTitleCloseFgOver }};
|
||||
@@ -416,9 +414,7 @@ calendarPrevious: IconButton {
|
||||
|
||||
rippleAreaPosition: point(2px, 2px);
|
||||
rippleAreaSize: 44px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
calendarPreviousDisabled: icon {{ "calendar_down-flip_vertical", menuIconFg }};
|
||||
calendarNext: IconButton(calendarPrevious) {
|
||||
@@ -616,9 +612,7 @@ proxyTryIPv6Padding: margins(22px, 8px, 22px, 5px);
|
||||
proxyRowPadding: margins(22px, 8px, 8px, 8px);
|
||||
proxyRowIconSkip: 32px;
|
||||
proxyRowSkip: 2px;
|
||||
proxyRowRipple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
proxyRowRipple: defaultRippleAnimationBgOver;
|
||||
proxyRowTitleFg: windowFg;
|
||||
proxyRowTitlePalette: TextPalette(defaultTextPalette) {
|
||||
linkFg: windowSubTextFg;
|
||||
@@ -683,9 +677,7 @@ themesMenuToggle: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(4px, 4px);
|
||||
rippleAreaSize: 36px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
themesMenuPosition: point(-2px, 25px);
|
||||
|
||||
@@ -738,9 +730,7 @@ createPollOptionRemove: CrossButton {
|
||||
|
||||
duration: 150;
|
||||
loadingPeriod: 1000;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
createPollOptionRemovePosition: point(11px, 9px);
|
||||
createPollOptionEmojiPositionSkip: 4px;
|
||||
@@ -888,6 +878,13 @@ peerListWithInviteViaLink: PeerList(peerListBox) {
|
||||
peerListSingleRow: PeerList(peerListBox) {
|
||||
padding: margins(0px, 0px, 0px, 0px);
|
||||
}
|
||||
peerListSmallSkips: PeerList(peerListBox) {
|
||||
padding: margins(
|
||||
0px,
|
||||
defaultVerticalListSkip,
|
||||
0px,
|
||||
defaultVerticalListSkip);
|
||||
}
|
||||
|
||||
scheduleHeight: 95px;
|
||||
scheduleDateTop: 38px;
|
||||
@@ -951,9 +948,7 @@ sponsoredUrlButton: RoundButton(defaultActiveButton) {
|
||||
textTop: 7px;
|
||||
style: defaultTextStyle;
|
||||
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
requestPeerRestriction: FlatLabel(defaultFlatLabel) {
|
||||
|
||||
@@ -1944,6 +1944,13 @@ PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
|
||||
}
|
||||
}
|
||||
|
||||
if (_controller->overrideKeyboardNavigation(
|
||||
direction,
|
||||
_selected.index.value,
|
||||
newSelectedIndex)) {
|
||||
return { _selected.index.value, _selected.index.value };
|
||||
}
|
||||
|
||||
_selected.index.value = newSelectedIndex;
|
||||
_selected.element = 0;
|
||||
if (newSelectedIndex >= 0) {
|
||||
|
||||
@@ -357,6 +357,8 @@ public:
|
||||
virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;
|
||||
virtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0;
|
||||
|
||||
virtual void peerListSelectSkip(int direction) = 0;
|
||||
|
||||
virtual void peerListPressLeftToContextMenu(bool shown) = 0;
|
||||
virtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0;
|
||||
|
||||
@@ -573,6 +575,13 @@ public:
|
||||
Unexpected("PeerListController::customRowRippleMaskGenerator.");
|
||||
}
|
||||
|
||||
virtual bool overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
@@ -1016,6 +1025,10 @@ public:
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;
|
||||
|
||||
void peerListSelectSkip(int direction) override {
|
||||
_content->selectSkip(direction);
|
||||
}
|
||||
|
||||
void peerListPressLeftToContextMenu(bool shown) override {
|
||||
_content->pressLeftToContextMenu(shown);
|
||||
}
|
||||
|
||||
@@ -956,12 +956,11 @@ void Controller::rowClicked(not_null<PeerListRow*> row) {
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
|
||||
const auto &stUser = st::boostReplaceUserpic;
|
||||
const auto photoSize = st::boostReplaceUserpic.photoSize;
|
||||
const auto session = &row->peer()->session();
|
||||
content->add(object_ptr<Ui::CenterWrap<>>(
|
||||
content,
|
||||
object_ptr<Ui::UserpicButton>(content, channel, stUser))
|
||||
)->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
Settings::SubscriptionUserpic(content, channel, photoSize)));
|
||||
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
|
||||
@@ -409,9 +409,7 @@ callRatingStar: IconButton {
|
||||
icon: icon {{ "calls/call_rating", windowSubTextFg }};
|
||||
iconPosition: point(-1px, -1px);
|
||||
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 36px;
|
||||
}
|
||||
@@ -1410,9 +1408,7 @@ groupCallRtmpShowButton: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 32px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
groupCallSettingsRtmpShowButton: IconButton(groupCallRtmpShowButton) {
|
||||
ripple: groupCallRipple;
|
||||
|
||||
@@ -73,8 +73,8 @@ private:
|
||||
|
||||
SourceButton _widget;
|
||||
FlatLabel _label;
|
||||
RoundRect _selectedRect;
|
||||
RoundRect _activeRect;
|
||||
Ui::RoundRect _selectedRect;
|
||||
Ui::RoundRect _activeRect;
|
||||
tgcalls::DesktopCaptureSource _source;
|
||||
std::unique_ptr<Preview> _preview;
|
||||
rpl::event_stream<> _activations;
|
||||
|
||||
@@ -150,6 +150,8 @@ SendButton {
|
||||
inner: IconButton;
|
||||
record: icon;
|
||||
recordOver: icon;
|
||||
round: icon;
|
||||
roundOver: icon;
|
||||
sendDisabledFg: color;
|
||||
}
|
||||
|
||||
@@ -331,9 +333,7 @@ stickersRemove: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaSize: 40px;
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
stickersUndoRemove: RoundButton(defaultLightButton) {
|
||||
width: -16px;
|
||||
@@ -494,9 +494,7 @@ hashtagClose: IconButton {
|
||||
|
||||
rippleAreaPosition: point(5px, 5px);
|
||||
rippleAreaSize: 20px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
stickerPanWidthMin: 64px;
|
||||
@@ -898,9 +896,7 @@ historyBusinessBotSettings: IconButton(defaultIconButton) {
|
||||
iconPosition: point(-1px, -1px);
|
||||
rippleAreaSize: 40px;
|
||||
rippleAreaPosition: point(4px, 9px);
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
height: 58px;
|
||||
width: 48px;
|
||||
}
|
||||
@@ -927,9 +923,7 @@ historyReplyCancel: IconButton {
|
||||
|
||||
rippleAreaPosition: point(4px, 4px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
historyPinnedShowAll: IconButton(historyReplyCancel) {
|
||||
icon: icon {{ "pinned_show_all", historyReplyCancelFg }};
|
||||
@@ -1058,9 +1052,7 @@ historyAttach: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(2px, 3px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
historyMessagesTTL: IconButtonWithText {
|
||||
@@ -1169,6 +1161,10 @@ historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive
|
||||
historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }};
|
||||
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
|
||||
historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }};
|
||||
historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }};
|
||||
historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }};
|
||||
historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }};
|
||||
historyRecordRoundIconPosition: point(0px, 0px);
|
||||
historyRecordSendIconPosition: point(2px, 0px);
|
||||
historyRecordVoiceRippleBgActive: lightButtonBgOver;
|
||||
historyRecordSignalRadius: 5px;
|
||||
@@ -1214,6 +1210,7 @@ historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }}
|
||||
historyRecordLockMargin: margins(4px, 4px, 4px, 4px);
|
||||
historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }};
|
||||
historyRecordLockInput: icon {{ "voice_lock/input_mic_s", historyToDownFg }};
|
||||
historyRecordLockRound: icon {{ "voice_lock/input_round_s", historyToDownFg }};
|
||||
historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px);
|
||||
|
||||
historyRecordDelete: IconButton(historyAttach) {
|
||||
@@ -1274,6 +1271,8 @@ historySend: SendButton {
|
||||
}
|
||||
record: historyRecordVoice;
|
||||
recordOver: historyRecordVoiceOver;
|
||||
round: historyRecordRound;
|
||||
roundOver: historyRecordRoundOver;
|
||||
sendDisabledFg: historyComposeIconFg;
|
||||
}
|
||||
|
||||
@@ -1287,9 +1286,7 @@ defaultComposeFilesMenu: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(1px, 6px);
|
||||
rippleAreaSize: 42px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
defaultComposeFilesField: InputField(defaultInputField) {
|
||||
textMargins: margins(1px, 26px, 31px, 4px);
|
||||
@@ -1349,9 +1346,7 @@ moreChatsBarClose: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(0px, 4px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
reportReasonTopSkip: 8px;
|
||||
|
||||
@@ -222,7 +222,7 @@ QByteArray Settings::serialize() const {
|
||||
+ Serialize::stringSize(_customFontFamily)
|
||||
+ sizeof(qint32) * 3
|
||||
+ Serialize::bytearraySize(_tonsiteStorageToken)
|
||||
+ sizeof(qint32);
|
||||
+ sizeof(qint32) * 4;
|
||||
|
||||
auto result = QByteArray();
|
||||
result.reserve(size);
|
||||
@@ -377,7 +377,10 @@ QByteArray Settings::serialize() const {
|
||||
<< qint32(_systemUnlockEnabled ? 1 : 0)
|
||||
<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
|
||||
<< _tonsiteStorageToken
|
||||
<< qint32(_includeMutedCounterFolders ? 1 : 0);
|
||||
<< qint32(_includeMutedCounterFolders ? 1 : 0)
|
||||
<< qint32(_ivZoom.current())
|
||||
<< qint32(_skipToastsInFocus ? 1 : 0)
|
||||
<< qint32(_recordVideoMessages ? 1 : 0);
|
||||
}
|
||||
|
||||
Ensures(result.size() == size);
|
||||
@@ -501,6 +504,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
|
||||
qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
|
||||
QByteArray tonsiteStorageToken = _tonsiteStorageToken;
|
||||
qint32 ivZoom = _ivZoom.current();
|
||||
qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0;
|
||||
qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;
|
||||
|
||||
stream >> themesAccentColors;
|
||||
if (!stream.atEnd()) {
|
||||
@@ -810,6 +816,15 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
if (!stream.atEnd()) {
|
||||
stream >> includeMutedCounterFolders;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> ivZoom;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> skipToastsInFocus;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> recordVideoMessages;
|
||||
}
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
LOG(("App Error: "
|
||||
"Bad data for Core::Settings::constructFromSerialized()"));
|
||||
@@ -1021,6 +1036,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
? std::optional<bool>()
|
||||
: (weatherInCelsius == 1);
|
||||
_tonsiteStorageToken = tonsiteStorageToken;
|
||||
_ivZoom = ivZoom;
|
||||
_skipToastsInFocus = (skipToastsInFocus == 1);
|
||||
_recordVideoMessages = (recordVideoMessages == 1);
|
||||
}
|
||||
|
||||
QString Settings::getSoundPath(const QString &key) const {
|
||||
@@ -1347,6 +1365,7 @@ void Settings::resetOnLastLogout() {
|
||||
_flashBounceNotify = true;
|
||||
_notifyView = NotifyView::ShowPreview;
|
||||
//_nativeNotifications = std::nullopt;
|
||||
//_skipToastsInFocus = false;
|
||||
//_notificationsCount = 3;
|
||||
//_notificationsCorner = ScreenCorner::BottomRight;
|
||||
_includeMutedCounter = true;
|
||||
@@ -1408,6 +1427,8 @@ void Settings::resetOnLastLogout() {
|
||||
_hiddenGroupCallTooltips = 0;
|
||||
_storiesClickTooltipHidden = false;
|
||||
_ttlVoiceClickTooltipHidden = false;
|
||||
_ivZoom = 100;
|
||||
_recordVideoMessages = false;
|
||||
|
||||
_recentEmojiPreload.clear();
|
||||
_recentEmoji.clear();
|
||||
@@ -1465,6 +1486,14 @@ void Settings::setNativeNotifications(bool value) {
|
||||
: std::make_optional(value);
|
||||
}
|
||||
|
||||
bool Settings::skipToastsInFocus() const {
|
||||
return _skipToastsInFocus;
|
||||
}
|
||||
|
||||
void Settings::setSkipToastsInFocus(bool value) {
|
||||
_skipToastsInFocus = value;
|
||||
}
|
||||
|
||||
void Settings::setTranslateButtonEnabled(bool value) {
|
||||
_translateButtonEnabled = value;
|
||||
}
|
||||
@@ -1547,4 +1576,16 @@ bool Settings::rememberedDeleteMessageOnlyForYou() const {
|
||||
return _rememberedDeleteMessageOnlyForYou;
|
||||
}
|
||||
|
||||
int Settings::ivZoom() const {
|
||||
return _ivZoom.current();
|
||||
}
|
||||
rpl::producer<int> Settings::ivZoomValue() const {
|
||||
return _ivZoom.value();
|
||||
}
|
||||
void Settings::setIvZoom(int value) {
|
||||
constexpr auto kMin = 30;
|
||||
constexpr auto kMax = 200;
|
||||
_ivZoom = std::clamp(value, kMin, kMax);
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -220,6 +220,9 @@ public:
|
||||
[[nodiscard]] bool nativeNotifications() const;
|
||||
void setNativeNotifications(bool value);
|
||||
|
||||
[[nodiscard]] bool skipToastsInFocus() const;
|
||||
void setSkipToastsInFocus(bool value);
|
||||
|
||||
[[nodiscard]] int notificationsCount() const {
|
||||
return _notificationsCount;
|
||||
}
|
||||
@@ -625,6 +628,13 @@ public:
|
||||
return _floatPlayerCorner;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool recordVideoMessages() const {
|
||||
return _recordVideoMessages;
|
||||
}
|
||||
void setRecordVideoMessages(bool value) {
|
||||
_recordVideoMessages = value;
|
||||
}
|
||||
|
||||
void updateDialogsWidthRatio(float64 ratio, bool nochat);
|
||||
[[nodiscard]] float64 dialogsWidthRatio(bool nochat) const;
|
||||
|
||||
@@ -915,6 +925,10 @@ public:
|
||||
_tonsiteStorageToken = value;
|
||||
}
|
||||
|
||||
[[nodiscard]] int ivZoom() const;
|
||||
[[nodiscard]] rpl::producer<int> ivZoomValue() const;
|
||||
void setIvZoom(int value);
|
||||
|
||||
[[nodiscard]] static bool ThirdColumnByDefault();
|
||||
[[nodiscard]] static float64 DefaultDialogsWidthRatio();
|
||||
|
||||
@@ -954,6 +968,7 @@ private:
|
||||
bool _flashBounceNotify = true;
|
||||
NotifyView _notifyView = NotifyView::ShowPreview;
|
||||
std::optional<bool> _nativeNotifications;
|
||||
bool _skipToastsInFocus = false;
|
||||
int _notificationsCount = 3;
|
||||
ScreenCorner _notificationsCorner = ScreenCorner::BottomRight;
|
||||
bool _includeMutedCounter = true;
|
||||
@@ -1050,6 +1065,7 @@ private:
|
||||
bool _systemUnlockEnabled = false;
|
||||
std::optional<bool> _weatherInCelsius;
|
||||
QByteArray _tonsiteStorageToken;
|
||||
rpl::variable<int> _ivZoom = 100;
|
||||
|
||||
bool _tabbedReplacedWithInfo = false; // per-window
|
||||
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window
|
||||
@@ -1060,6 +1076,8 @@ private:
|
||||
bool _rememberedFlashBounceNotifyFromTray = false;
|
||||
bool _dialogsWidthSetToZeroWithoutChat = false;
|
||||
|
||||
bool _recordVideoMessages = false;
|
||||
|
||||
QByteArray _photoEditorBrush;
|
||||
|
||||
};
|
||||
|
||||
@@ -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 = 5006002;
|
||||
constexpr auto AppVersionStr = "5.6.2";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppVersion = 5006004;
|
||||
constexpr auto AppVersionStr = "5.6.4";
|
||||
constexpr auto AppBetaVersion = true;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -222,9 +222,7 @@ dialogsMenuToggle: IconButton {
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
dialogsMenuToggleUnread: icon {
|
||||
{ "dialogs/dialogs_menu_unread", dialogsMenuIconFg },
|
||||
|
||||
@@ -57,7 +57,6 @@ namespace {
|
||||
|
||||
constexpr auto kCollapsedChannelsCount = 5;
|
||||
constexpr auto kProbablyMaxChannels = 1000;
|
||||
constexpr auto kProbablyMaxRecommendations = 100;
|
||||
constexpr auto kCollapsedAppsCount = 5;
|
||||
constexpr auto kProbablyMaxApps = 100;
|
||||
|
||||
|
||||
@@ -284,10 +284,12 @@ FormatPointer MakeFormatPointer(
|
||||
return {};
|
||||
}
|
||||
result->pb = io.get();
|
||||
result->flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
|
||||
auto options = (AVDictionary*)nullptr;
|
||||
const auto guard = gsl::finally([&] { av_dict_free(&options); });
|
||||
av_dict_set(&options, "usetoc", "1", 0);
|
||||
|
||||
const auto error = AvErrorWrap(avformat_open_input(
|
||||
&result,
|
||||
nullptr,
|
||||
@@ -307,6 +309,54 @@ FormatPointer MakeFormatPointer(
|
||||
return FormatPointer(result);
|
||||
}
|
||||
|
||||
FormatPointer MakeWriteFormatPointer(
|
||||
void *opaque,
|
||||
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#if DA_FFMPEG_CONST_WRITE_CALLBACK
|
||||
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
|
||||
#else
|
||||
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#endif
|
||||
int64_t(*seek)(void *opaque, int64_t offset, int whence),
|
||||
const QByteArray &format) {
|
||||
const AVOutputFormat *found = nullptr;
|
||||
void *i = nullptr;
|
||||
while ((found = av_muxer_iterate(&i))) {
|
||||
if (found->name == format) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LogError(
|
||||
"av_muxer_iterate",
|
||||
u"Format %1 not found"_q.arg(QString::fromUtf8(format)));
|
||||
return {};
|
||||
}
|
||||
|
||||
auto io = MakeIOPointer(opaque, read, write, seek);
|
||||
if (!io) {
|
||||
return {};
|
||||
}
|
||||
io->seekable = (seek != nullptr);
|
||||
|
||||
auto result = (AVFormatContext*)nullptr;
|
||||
auto error = AvErrorWrap(avformat_alloc_output_context2(
|
||||
&result,
|
||||
(AVOutputFormat*)found,
|
||||
nullptr,
|
||||
nullptr));
|
||||
if (!result || error) {
|
||||
LogError("avformat_alloc_output_context2", error);
|
||||
return {};
|
||||
}
|
||||
result->pb = io.get();
|
||||
result->flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
|
||||
// Now FormatPointer will own and free the IO context.
|
||||
io.release();
|
||||
return FormatPointer(result);
|
||||
}
|
||||
|
||||
void FormatDeleter::operator()(AVFormatContext *value) {
|
||||
if (value) {
|
||||
const auto deleter = IOPointer(value->pb);
|
||||
@@ -448,21 +498,134 @@ SwscalePointer MakeSwscalePointer(
|
||||
existing);
|
||||
}
|
||||
|
||||
void SwresampleDeleter::operator()(SwrContext *value) {
|
||||
if (value) {
|
||||
swr_free(&value);
|
||||
}
|
||||
}
|
||||
|
||||
SwresamplePointer MakeSwresamplePointer(
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *srcLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t srcLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat srcFormat,
|
||||
int srcRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *dstLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t dstLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat dstFormat,
|
||||
int dstRate,
|
||||
SwresamplePointer *existing) {
|
||||
// We have to use custom caching for SwsContext, because
|
||||
// sws_getCachedContext checks passed flags with existing context flags,
|
||||
// and re-creates context if they're different, but in the process of
|
||||
// context creation the passed flags are modified before being written
|
||||
// to the resulting context, so the caching doesn't work.
|
||||
if (existing && (*existing) != nullptr) {
|
||||
const auto &deleter = existing->get_deleter();
|
||||
if (true
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
&& srcLayout->nb_channels == deleter.srcChannels
|
||||
&& dstLayout->nb_channels == deleter.dstChannels
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
&& (av_get_channel_layout_nb_channels(srcLayout)
|
||||
== deleter.srcChannels)
|
||||
&& (av_get_channel_layout_nb_channels(dstLayout)
|
||||
== deleter.dstChannels)
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
&& srcFormat == deleter.srcFormat
|
||||
&& dstFormat == deleter.dstFormat
|
||||
&& srcRate == deleter.srcRate
|
||||
&& dstRate == deleter.dstRate) {
|
||||
return std::move(*existing);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize audio resampler
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
auto result = (SwrContext*)nullptr;
|
||||
auto error = AvErrorWrap(swr_alloc_set_opts2(
|
||||
&result,
|
||||
dstLayout,
|
||||
dstFormat,
|
||||
dstRate,
|
||||
srcLayout,
|
||||
srcFormat,
|
||||
srcRate,
|
||||
0,
|
||||
nullptr));
|
||||
if (error || !result) {
|
||||
LogError(u"swr_alloc_set_opts2"_q, error);
|
||||
return SwresamplePointer();
|
||||
}
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
auto result = swr_alloc_set_opts(
|
||||
existing ? existing.get() : nullptr,
|
||||
dstLayout,
|
||||
dstFormat,
|
||||
dstRate,
|
||||
srcLayout,
|
||||
srcFormat,
|
||||
srcRate,
|
||||
0,
|
||||
nullptr);
|
||||
if (!result) {
|
||||
LogError(u"swr_alloc_set_opts"_q);
|
||||
}
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
|
||||
error = AvErrorWrap(swr_init(result));
|
||||
if (error) {
|
||||
LogError(u"swr_init"_q, error);
|
||||
swr_free(&result);
|
||||
return SwresamplePointer();
|
||||
}
|
||||
|
||||
return SwresamplePointer(
|
||||
result,
|
||||
{
|
||||
srcFormat,
|
||||
srcRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
srcLayout->nb_channels,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
av_get_channel_layout_nb_channels(srcLayout),
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
dstFormat,
|
||||
dstRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
dstLayout->nb_channels,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
av_get_channel_layout_nb_channels(dstLayout),
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
});
|
||||
}
|
||||
|
||||
void SwscaleDeleter::operator()(SwsContext *value) {
|
||||
if (value) {
|
||||
sws_freeContext(value);
|
||||
}
|
||||
}
|
||||
|
||||
void LogError(const QString &method) {
|
||||
LOG(("Streaming Error: Error in %1.").arg(method));
|
||||
void LogError(const QString &method, const QString &details) {
|
||||
LOG(("Streaming Error: Error in %1%2."
|
||||
).arg(method
|
||||
).arg(details.isEmpty() ? QString() : " - " + details));
|
||||
}
|
||||
|
||||
void LogError(const QString &method, AvErrorWrap error) {
|
||||
LOG(("Streaming Error: Error in %1 (code: %2, text: %3)."
|
||||
void LogError(
|
||||
const QString &method,
|
||||
AvErrorWrap error,
|
||||
const QString &details) {
|
||||
LOG(("Streaming Error: Error in %1 (code: %2, text: %3)%4."
|
||||
).arg(method
|
||||
).arg(error.code()
|
||||
).arg(error.text()));
|
||||
).arg(error.text()
|
||||
).arg(details.isEmpty() ? QString() : " - " + details));
|
||||
}
|
||||
|
||||
crl::time PtsToTime(int64_t pts, AVRational timeBase) {
|
||||
|
||||
@@ -19,6 +19,8 @@ extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/version.h>
|
||||
} // extern "C"
|
||||
|
||||
@@ -138,6 +140,16 @@ using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;
|
||||
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#endif
|
||||
int64_t(*seek)(void *opaque, int64_t offset, int whence));
|
||||
[[nodiscard]] FormatPointer MakeWriteFormatPointer(
|
||||
void *opaque,
|
||||
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#if DA_FFMPEG_CONST_WRITE_CALLBACK
|
||||
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
|
||||
#else
|
||||
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#endif
|
||||
int64_t(*seek)(void *opaque, int64_t offset, int whence),
|
||||
const QByteArray &format);
|
||||
|
||||
struct CodecDeleter {
|
||||
void operator()(AVCodecContext *value);
|
||||
@@ -179,8 +191,39 @@ using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;
|
||||
QSize resize,
|
||||
SwscalePointer *existing = nullptr);
|
||||
|
||||
void LogError(const QString &method);
|
||||
void LogError(const QString &method, FFmpeg::AvErrorWrap error);
|
||||
struct SwresampleDeleter {
|
||||
AVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE;
|
||||
int srcRate = 0;
|
||||
int srcChannels = 0;
|
||||
AVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE;
|
||||
int dstRate = 0;
|
||||
int dstChannels = 0;
|
||||
|
||||
void operator()(SwrContext *value);
|
||||
};
|
||||
using SwresamplePointer = std::unique_ptr<SwrContext, SwresampleDeleter>;
|
||||
[[nodiscard]] SwresamplePointer MakeSwresamplePointer(
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *srcLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t srcLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat srcFormat,
|
||||
int srcRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *dstLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t dstLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat dstFormat,
|
||||
int dstRate,
|
||||
SwresamplePointer *existing = nullptr);
|
||||
|
||||
void LogError(const QString &method, const QString &details = {});
|
||||
void LogError(
|
||||
const QString &method,
|
||||
FFmpeg::AvErrorWrap error,
|
||||
const QString &details = {});
|
||||
|
||||
[[nodiscard]] const AVCodec *FindDecoder(not_null<AVCodecContext*> context);
|
||||
[[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase);
|
||||
|
||||
@@ -2848,7 +2848,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!actionText.isEmpty() && !hasCopyRestriction(item)) {
|
||||
if (!actionText.isEmpty()) {
|
||||
_menu->addAction(
|
||||
actionText,
|
||||
[text = link->copyToClipboardText()] {
|
||||
@@ -2935,11 +2935,20 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
if (_dragStateItem) {
|
||||
const auto view = viewByItem(_dragStateItem);
|
||||
const auto textItem = view ? view->textItem() : _dragStateItem;
|
||||
const auto wasAmount = _menu->actions().size();
|
||||
HistoryView::AddEmojiPacksAction(
|
||||
_menu,
|
||||
textItem ? textItem : _dragStateItem,
|
||||
HistoryView::EmojiPacksSource::Message,
|
||||
_controller);
|
||||
const auto added = (_menu->actions().size() > wasAmount);
|
||||
if (!added) {
|
||||
_menu->addSeparator();
|
||||
}
|
||||
HistoryView::AddSelectRestrictionAction(
|
||||
_menu,
|
||||
textItem ? textItem : _dragStateItem,
|
||||
!added);
|
||||
}
|
||||
if (hasWhoReactedItem) {
|
||||
HistoryView::AddWhoReactedAction(
|
||||
|
||||
@@ -150,6 +150,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "main/session/send_as_peers.h"
|
||||
#include "webrtc/webrtc_environment.h"
|
||||
#include "window/notifications_manager.h"
|
||||
#include "window/window_adaptive.h"
|
||||
#include "window/window_controller.h"
|
||||
@@ -1042,6 +1043,7 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
action);
|
||||
_voiceRecordBar->clearListenState();
|
||||
}, lifetime());
|
||||
@@ -1055,6 +1057,24 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
_cornerButtons.updateUnreadThingsVisibility();
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->errors(
|
||||
) | rpl::start_with_next([=](::Media::Capture::Error error) {
|
||||
using Error = ::Media::Capture::Error;
|
||||
switch (error) {
|
||||
case Error::AudioInit:
|
||||
case Error::AudioTimeout:
|
||||
controller()->showToast(tr::lng_record_audio_problem(tr::now));
|
||||
break;
|
||||
case Error::VideoInit:
|
||||
case Error::VideoTimeout:
|
||||
controller()->showToast(tr::lng_record_video_problem(tr::now));
|
||||
break;
|
||||
default:
|
||||
controller()->showToast(u"Unknown error."_q);
|
||||
break;
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->updateSendButtonTypeRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateSendButtonType();
|
||||
@@ -1067,7 +1087,17 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
|
||||
_voiceRecordBar->recordingTipRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
controller()->showToast(tr::lng_record_hold_tip(tr::now));
|
||||
Core::App().settings().setRecordVideoMessages(
|
||||
!Core::App().settings().recordVideoMessages());
|
||||
updateSendButtonType();
|
||||
switch (_send->type()) {
|
||||
case Ui::SendButton::Type::Record:
|
||||
controller()->showToast(tr::lng_record_voice_tip(tr::now));
|
||||
break;
|
||||
case Ui::SendButton::Type::Round:
|
||||
controller()->showToast(tr::lng_record_video_tip(tr::now));
|
||||
break;
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->recordingStateChanges(
|
||||
@@ -2104,6 +2134,7 @@ void HistoryWidget::showHistory(
|
||||
MsgId showAtMsgId,
|
||||
const TextWithEntities &highlightPart,
|
||||
int highlightPartOffsetHint) {
|
||||
|
||||
_pinnedClickedId = FullMsgId();
|
||||
_minPinnedId = std::nullopt;
|
||||
_showAtMsgHighlightPart = {};
|
||||
@@ -2298,6 +2329,8 @@ void HistoryWidget::showHistory(
|
||||
_contactStatus = nullptr;
|
||||
_businessBotStatus = nullptr;
|
||||
|
||||
updateRecordMediaState();
|
||||
|
||||
if (peerId) {
|
||||
using namespace HistoryView;
|
||||
_peer = session().data().peer(peerId);
|
||||
@@ -4253,7 +4286,10 @@ auto HistoryWidget::computeSendButtonType() const {
|
||||
} else if (_isInlineBot) {
|
||||
return Type::Cancel;
|
||||
} else if (showRecordButton()) {
|
||||
return Type::Record;
|
||||
return (Core::App().settings().recordVideoMessages()
|
||||
&& _canRecordVideoMessage)
|
||||
? Type::Round
|
||||
: Type::Record;
|
||||
}
|
||||
return Type::Send;
|
||||
}
|
||||
@@ -4587,7 +4623,8 @@ void HistoryWidget::sendButtonClicked() {
|
||||
const auto type = _send->type();
|
||||
if (type == Ui::SendButton::Type::Cancel) {
|
||||
cancelInlineBot();
|
||||
} else if (type != Ui::SendButton::Type::Record) {
|
||||
} else if (type != Ui::SendButton::Type::Record
|
||||
&& type != Ui::SendButton::Type::Round) {
|
||||
send({});
|
||||
}
|
||||
}
|
||||
@@ -4877,7 +4914,7 @@ bool HistoryWidget::isSearching() const {
|
||||
}
|
||||
|
||||
bool HistoryWidget::showRecordButton() const {
|
||||
return Media::Capture::instance()->available()
|
||||
return _canRecordAudioMessage
|
||||
&& !_voiceRecordBar->isListenState()
|
||||
&& !_voiceRecordBar->isRecordingByAnotherBar()
|
||||
&& !HasSendText(_field)
|
||||
@@ -4908,7 +4945,9 @@ void HistoryWidget::updateSendButtonType() {
|
||||
}();
|
||||
_send->setSlowmodeDelay(delay);
|
||||
_send->setDisabled(disabledBySlowmode
|
||||
&& (type == Type::Send || type == Type::Record));
|
||||
&& (type == Type::Send
|
||||
|| type == Type::Record
|
||||
|| type == Type::Round));
|
||||
|
||||
if (delay != 0) {
|
||||
base::call_delayed(
|
||||
@@ -5478,6 +5517,15 @@ void HistoryWidget::inlineBotChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryWidget::updateRecordMediaState() {
|
||||
Media::Capture::instance()->check();
|
||||
_canRecordAudioMessage = Media::Capture::instance()->available();
|
||||
|
||||
const auto environment = &Core::App().mediaDevices();
|
||||
const auto type = Webrtc::DeviceType::Camera;
|
||||
_canRecordVideoMessage = !environment->devices(type).empty();
|
||||
}
|
||||
|
||||
void HistoryWidget::fieldResized() {
|
||||
moveFieldControls();
|
||||
updateHistoryGeometry();
|
||||
|
||||
@@ -497,6 +497,7 @@ private:
|
||||
bool replyToPreviousMessage();
|
||||
bool replyToNextMessage();
|
||||
[[nodiscard]] bool showSlowmodeError();
|
||||
void updateRecordMediaState();
|
||||
|
||||
void hideChildWidgets();
|
||||
void hideSelectorControlsAnimated();
|
||||
@@ -746,6 +747,9 @@ private:
|
||||
mtpRequestId _inlineBotResolveRequestId = 0;
|
||||
bool _isInlineBot = false;
|
||||
|
||||
bool _canRecordVideoMessage = false;
|
||||
bool _canRecordAudioMessage = false;
|
||||
|
||||
std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
|
||||
std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ struct VoiceToSend {
|
||||
VoiceWaveform waveform;
|
||||
crl::time duration = 0;
|
||||
Api::SendOptions options;
|
||||
bool video = false;
|
||||
};
|
||||
struct SendActionUpdate {
|
||||
Api::SendProgressType type = Api::SendProgressType();
|
||||
|
||||
@@ -81,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/controls/silent_toggle.h"
|
||||
#include "ui/chat/choose_send_as.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "webrtc/webrtc_environment.h"
|
||||
#include "window/window_adaptive.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "mainwindow.h"
|
||||
@@ -963,6 +964,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) {
|
||||
initWebpageProcess();
|
||||
initWriteRestriction();
|
||||
initForwardProcess();
|
||||
updateRecordMediaState();
|
||||
updateBotCommandShown();
|
||||
updateLikeShown();
|
||||
updateMessagesTTLShown();
|
||||
@@ -1517,7 +1519,7 @@ void ComposeControls::orderControls() {
|
||||
}
|
||||
|
||||
bool ComposeControls::showRecordButton() const {
|
||||
return ::Media::Capture::instance()->available()
|
||||
return _canRecordAudioMessage
|
||||
&& !_voiceRecordBar->isListenState()
|
||||
&& !_voiceRecordBar->isRecordingByAnotherBar()
|
||||
&& !HasSendText(_field)
|
||||
@@ -2413,12 +2415,54 @@ void ComposeControls::initVoiceRecordBar() {
|
||||
return false;
|
||||
});
|
||||
|
||||
_voiceRecordBar->recordingTipRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
Core::App().settings().setRecordVideoMessages(
|
||||
!Core::App().settings().recordVideoMessages());
|
||||
updateSendButtonType();
|
||||
switch (_send->type()) {
|
||||
case Ui::SendButton::Type::Record:
|
||||
_show->showToast(tr::lng_record_voice_tip(tr::now));
|
||||
break;
|
||||
case Ui::SendButton::Type::Round:
|
||||
_show->showToast(tr::lng_record_video_tip(tr::now));
|
||||
break;
|
||||
}
|
||||
}, _wrap->lifetime());
|
||||
|
||||
_voiceRecordBar->errors(
|
||||
) | rpl::start_with_next([=](::Media::Capture::Error error) {
|
||||
using Error = ::Media::Capture::Error;
|
||||
switch (error) {
|
||||
case Error::AudioInit:
|
||||
case Error::AudioTimeout:
|
||||
_show->showToast(tr::lng_record_audio_problem(tr::now));
|
||||
break;
|
||||
case Error::VideoInit:
|
||||
case Error::VideoTimeout:
|
||||
_show->showToast(tr::lng_record_video_problem(tr::now));
|
||||
break;
|
||||
default:
|
||||
_show->showToast(u"Unknown error."_q);
|
||||
break;
|
||||
}
|
||||
}, _wrap->lifetime());
|
||||
|
||||
_voiceRecordBar->updateSendButtonTypeRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateSendButtonType();
|
||||
}, _wrap->lifetime());
|
||||
}
|
||||
|
||||
void ComposeControls::updateRecordMediaState() {
|
||||
::Media::Capture::instance()->check();
|
||||
_canRecordAudioMessage = ::Media::Capture::instance()->available();
|
||||
|
||||
const auto environment = &Core::App().mediaDevices();
|
||||
const auto type = Webrtc::DeviceType::Camera;
|
||||
_canRecordVideoMessage = !environment->devices(type).empty();
|
||||
}
|
||||
|
||||
void ComposeControls::updateWrappingVisibility() {
|
||||
const auto hidden = _hidden.current();
|
||||
const auto &restriction = _writeRestriction.current();
|
||||
@@ -2454,7 +2498,10 @@ auto ComposeControls::computeSendButtonType() const {
|
||||
} else if (_isInlineBot) {
|
||||
return Type::Cancel;
|
||||
} else if (showRecordButton()) {
|
||||
return Type::Record;
|
||||
return (Core::App().settings().recordVideoMessages()
|
||||
&& _canRecordVideoMessage)
|
||||
? Type::Round
|
||||
: Type::Record;
|
||||
}
|
||||
return (_mode == Mode::Normal) ? Type::Send : Type::Schedule;
|
||||
}
|
||||
@@ -2487,7 +2534,9 @@ void ComposeControls::updateSendButtonType() {
|
||||
}();
|
||||
_send->setSlowmodeDelay(delay);
|
||||
_send->setDisabled(_sendDisabledBySlowmode.current()
|
||||
&& (type == Type::Send || type == Type::Record));
|
||||
&& (type == Type::Send
|
||||
|| type == Type::Record
|
||||
|| type == Type::Round));
|
||||
}
|
||||
|
||||
void ComposeControls::finishAnimating() {
|
||||
@@ -3149,8 +3198,9 @@ bool ComposeControls::isRecording() const {
|
||||
bool ComposeControls::isRecordingPressed() const {
|
||||
return !_voiceRecordBar->isRecordingLocked()
|
||||
&& (!_voiceRecordBar->isHidden()
|
||||
|| (_send->type() == Ui::SendButton::Type::Record
|
||||
&& _send->isDown()));
|
||||
|| (_send->isDown()
|
||||
&& (_send->type() == Ui::SendButton::Type::Record
|
||||
|| _send->type() == Ui::SendButton::Type::Round)));
|
||||
}
|
||||
|
||||
rpl::producer<bool> ComposeControls::recordingActiveValue() const {
|
||||
|
||||
@@ -278,6 +278,7 @@ private:
|
||||
bool updateSendAsButton();
|
||||
void updateAttachBotsMenu();
|
||||
void updateHeight();
|
||||
void updateRecordMediaState();
|
||||
void updateWrappingVisibility();
|
||||
void updateControlsVisibility();
|
||||
void updateControlsGeometry(QSize size);
|
||||
@@ -437,6 +438,9 @@ private:
|
||||
bool _botCommandShown = false;
|
||||
bool _likeShown = false;
|
||||
|
||||
bool _canRecordVideoMessage = false;
|
||||
bool _canRecordAudioMessage = false;
|
||||
|
||||
FullMsgId _editingId;
|
||||
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
|
||||
bool _canReplaceMedia = false;
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/filters/edit_filter_chats_list.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
@@ -41,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
@@ -911,6 +913,117 @@ void DraftOptionsBox(
|
||||
}, box->lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthorSelector {
|
||||
object_ptr<Ui::RpWidget> content = { nullptr };
|
||||
Fn<bool(int, int, int)> overrideKey;
|
||||
};
|
||||
[[nodiscard]] AuthorSelector AuthorRowSelector(
|
||||
not_null<Main::Session*> session,
|
||||
FullReplyTo reply,
|
||||
Fn<void(not_null<Data::Thread*>)> chosen) {
|
||||
const auto item = session->data().message(reply.messageId);
|
||||
if (!item) {
|
||||
return {};
|
||||
}
|
||||
const auto displayFrom = item->displayFrom();
|
||||
const auto from = displayFrom ? displayFrom : item->from().get();
|
||||
if (!from->isUser() || from == item->history()->peer || from->isSelf()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
class AuthorController final : public PeerListController {
|
||||
public:
|
||||
AuthorController(not_null<PeerData*> peer, Fn<void()> click)
|
||||
: _peer(peer)
|
||||
, _click(std::move(click)) {
|
||||
}
|
||||
|
||||
void prepare() override {
|
||||
delegate()->peerListAppendRow(
|
||||
std::make_unique<ChatsListBoxController::Row>(
|
||||
_peer->owner().history(_peer),
|
||||
&computeListSt().item));
|
||||
delegate()->peerListRefreshRows();
|
||||
TrackPremiumRequiredChanges(this, _lifetime);
|
||||
}
|
||||
void loadMoreRows() override {
|
||||
}
|
||||
void rowClicked(not_null<PeerListRow*> row) override {
|
||||
if (RecipientRow::ShowLockedError(this, row, WritePremiumRequiredError)) {
|
||||
return;
|
||||
} else if (const auto onstack = _click) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
Main::Session &session() const override {
|
||||
return _peer->session();
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
Fn<void()> _click;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
|
||||
const auto container = result.data();
|
||||
|
||||
container->add(CreatePeerListSectionSubtitle(
|
||||
container,
|
||||
tr::lng_reply_in_author()));
|
||||
Ui::AddSkip(container);
|
||||
|
||||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime().make_state<
|
||||
AuthorController
|
||||
>(from, [=] { chosen(from->owner().history(from)); });
|
||||
controller->setStyleOverrides(&st::peerListSingleRow);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
controller));
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
|
||||
Ui::AddSkip(container);
|
||||
container->add(CreatePeerListSectionSubtitle(
|
||||
container,
|
||||
tr::lng_reply_in_chats_list()));
|
||||
|
||||
const auto overrideKey = [=](int direction, int from, int to) {
|
||||
if (!content->isVisible()) {
|
||||
return false;
|
||||
} else if (direction > 0 && from < 0 && to >= 0) {
|
||||
if (content->hasSelection()) {
|
||||
const auto was = content->selectedIndex();
|
||||
const auto now = content->selectSkip(1).reallyMovedTo;
|
||||
if (was != now) {
|
||||
return true;
|
||||
}
|
||||
content->clearSelection();
|
||||
} else {
|
||||
content->selectSkip(1);
|
||||
return true;
|
||||
}
|
||||
} else if (direction < 0 && to < 0) {
|
||||
if (!content->hasSelection()) {
|
||||
content->selectLast();
|
||||
} else if (from >= 0 || content->hasSelection()) {
|
||||
content->selectSkip(-1);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
.content = std::move(result),
|
||||
.overrideKey = overrideKey,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShowReplyToChatBox(
|
||||
@@ -921,7 +1034,7 @@ void ShowReplyToChatBox(
|
||||
public:
|
||||
using Chosen = not_null<Data::Thread*>;
|
||||
|
||||
Controller(not_null<Main::Session*> session)
|
||||
Controller(not_null<Main::Session*> session, FullReplyTo reply)
|
||||
: ChooseRecipientBoxController({
|
||||
.session = session,
|
||||
.callback = [=](Chosen thread) {
|
||||
@@ -929,6 +1042,13 @@ void ShowReplyToChatBox(
|
||||
},
|
||||
.premiumRequiredError = WritePremiumRequiredError,
|
||||
}) {
|
||||
_authorRow = AuthorRowSelector(
|
||||
session,
|
||||
reply,
|
||||
[=](Chosen thread) { _singleChosen.fire_copy(thread); });
|
||||
if (_authorRow.content) {
|
||||
setStyleOverrides(&st::peerListSmallSkips);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<Chosen> singleChosen() const {
|
||||
@@ -939,13 +1059,26 @@ void ShowReplyToChatBox(
|
||||
return tr::lng_saved_quote_here(tr::now);
|
||||
}
|
||||
|
||||
bool overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) override {
|
||||
return _authorRow.overrideKey
|
||||
&& _authorRow.overrideKey(direction, fromIndex, toIndex);
|
||||
}
|
||||
|
||||
private:
|
||||
void prepareViewHook() override {
|
||||
if (_authorRow.content) {
|
||||
delegate()->peerListSetAboveWidget(
|
||||
std::move(_authorRow.content));
|
||||
}
|
||||
ChooseRecipientBoxController::prepareViewHook();
|
||||
delegate()->peerListSetTitle(tr::lng_reply_in_another_title());
|
||||
}
|
||||
|
||||
rpl::event_stream<Chosen> _singleChosen;
|
||||
AuthorSelector _authorRow;
|
||||
|
||||
};
|
||||
|
||||
@@ -956,7 +1089,7 @@ void ShowReplyToChatBox(
|
||||
};
|
||||
const auto session = &show->session();
|
||||
const auto state = [&] {
|
||||
auto controller = std::make_unique<Controller>(session);
|
||||
auto controller = std::make_unique<Controller>(session, reply);
|
||||
const auto controllerRaw = controller.get();
|
||||
auto box = Box<PeerListBox>(std::move(controller), [=](
|
||||
not_null<PeerListBox*> box) {
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "core/application.h"
|
||||
#include "data/data_document.h"
|
||||
@@ -27,27 +28,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "media/audio/media_audio_capture.h"
|
||||
#include "media/player/media_player_button.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "media/streaming/media_streaming_instance.h"
|
||||
#include "media/streaming/media_streaming_round_preview.h"
|
||||
#include "storage/storage_account.h"
|
||||
#include "ui/controls/round_video_recorder.h"
|
||||
#include "ui/controls/send_button.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/animation_value_f.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_media_player.h"
|
||||
|
||||
#include <tgcalls/VideoCaptureInterface.h>
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
namespace {
|
||||
|
||||
using SendActionUpdate = VoiceRecordBar::SendActionUpdate;
|
||||
using VoiceToSend = VoiceRecordBar::VoiceToSend;
|
||||
|
||||
constexpr auto kAudioVoiceUpdateView = crl::time(200);
|
||||
constexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes
|
||||
constexpr auto kMaxSamples
|
||||
@@ -69,6 +75,61 @@ enum class FilterType {
|
||||
Cancel,
|
||||
};
|
||||
|
||||
class SoundedPreview final : public Ui::DynamicImage {
|
||||
public:
|
||||
SoundedPreview(
|
||||
not_null<DocumentData*> document,
|
||||
rpl::producer<> repaints);
|
||||
std::shared_ptr<DynamicImage> clone() override;
|
||||
QImage image(int size) override;
|
||||
void subscribeToUpdates(Fn<void()> callback) override;
|
||||
|
||||
private:
|
||||
const not_null<DocumentData*> _document;
|
||||
QImage _roundingMask;
|
||||
Fn<void()> _repaint;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
SoundedPreview::SoundedPreview(
|
||||
not_null<DocumentData*> document,
|
||||
rpl::producer<> repaints)
|
||||
: _document(document) {
|
||||
std::move(repaints) | rpl::start_with_next([=] {
|
||||
if (const auto onstack = _repaint) {
|
||||
onstack();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> SoundedPreview::clone() {
|
||||
Unexpected("ListenWrap::videoPreview::clone.");
|
||||
}
|
||||
|
||||
QImage SoundedPreview::image(int size) {
|
||||
const auto player = ::Media::Player::instance();
|
||||
const auto streamed = player->roundVideoPreview(_document);
|
||||
if (!streamed) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto full = QSize(size, size) * style::DevicePixelRatio();
|
||||
if (_roundingMask.size() != full) {
|
||||
_roundingMask = Images::EllipseMask(full);
|
||||
}
|
||||
const auto frame = streamed->frameWithInfo({
|
||||
.resize = full,
|
||||
.outer = full,
|
||||
.mask = _roundingMask,
|
||||
});
|
||||
return frame.image;
|
||||
}
|
||||
|
||||
void SoundedPreview::subscribeToUpdates(Fn<void()> callback) {
|
||||
_repaint = std::move(callback);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto InactiveColor(const QColor &c) {
|
||||
return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
|
||||
}
|
||||
@@ -77,10 +138,6 @@ enum class FilterType {
|
||||
return std::clamp(float64(low) / high, 0., 1.);
|
||||
}
|
||||
|
||||
[[nodiscard]] crl::time Duration(int samples) {
|
||||
return samples * crl::time(1000) / ::Media::Player::kDefaultFrequency;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto FormatVoiceDuration(int samples) {
|
||||
const int duration = kPrecision
|
||||
* (float64(samples) / ::Media::Player::kDefaultFrequency);
|
||||
@@ -201,6 +258,44 @@ void PaintWaveform(
|
||||
}
|
||||
}
|
||||
|
||||
void FillWithMinithumbs(
|
||||
QPainter &p,
|
||||
not_null<const Ui::RoundVideoResult*> data,
|
||||
QRect rect,
|
||||
float64 progress) {
|
||||
if (!data->minithumbsCount || !data->minithumbSize || rect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto size = rect.height();
|
||||
const auto single = data->minithumbSize;
|
||||
const auto perrow = data->minithumbs.width() / single;
|
||||
const auto thumbs = (rect.width() + size - 1) / size;
|
||||
if (!thumbs || !perrow) {
|
||||
return;
|
||||
}
|
||||
for (auto i = 0; i != thumbs - 1; ++i) {
|
||||
const auto index = (i * data->minithumbsCount) / thumbs;
|
||||
p.drawImage(
|
||||
QRect(rect.x() + i * size, rect.y(), size, size),
|
||||
data->minithumbs,
|
||||
QRect(
|
||||
(index % perrow) * single,
|
||||
(index / perrow) * single,
|
||||
single,
|
||||
single));
|
||||
}
|
||||
const auto last = rect.width() - (thumbs - 1) * size;
|
||||
const auto index = ((thumbs - 1) * data->minithumbsCount) / thumbs;
|
||||
p.drawImage(
|
||||
QRect(rect.x() + (thumbs - 1) * size, rect.y(), last, size),
|
||||
data->minithumbs,
|
||||
QRect(
|
||||
(index % perrow) * single,
|
||||
(index / perrow) * single,
|
||||
(last * single) / size,
|
||||
single));
|
||||
}
|
||||
|
||||
[[nodiscard]] QRect DrawLockCircle(
|
||||
QPainter &p,
|
||||
const QRect &widgetRect,
|
||||
@@ -426,35 +521,37 @@ public:
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::RecordBar &st,
|
||||
not_null<Main::Session*> session,
|
||||
::Media::Capture::Result *data,
|
||||
not_null<Ui::RoundVideoResult*> data,
|
||||
const style::font &font);
|
||||
|
||||
void requestPaintProgress(float64 progress);
|
||||
rpl::producer<> stopRequests() const;
|
||||
[[nodiscard]] rpl::producer<> stopRequests() const;
|
||||
|
||||
void playPause();
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> videoPreview();
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
void init();
|
||||
void initPlayButton();
|
||||
void initPlayProgress();
|
||||
|
||||
bool isInPlayer(const ::Media::Player::TrackState &state) const;
|
||||
bool isInPlayer() const;
|
||||
[[nodiscard]] bool isInPlayer(
|
||||
const ::Media::Player::TrackState &state) const;
|
||||
[[nodiscard]] bool isInPlayer() const;
|
||||
|
||||
int computeTopMargin(int height) const;
|
||||
QRect computeWaveformRect(const QRect ¢erRect) const;
|
||||
[[nodiscard]] int computeTopMargin(int height) const;
|
||||
[[nodiscard]] QRect computeWaveformRect(const QRect ¢erRect) const;
|
||||
|
||||
not_null<Ui::RpWidget*> _parent;
|
||||
const not_null<Ui::RpWidget*> _parent;
|
||||
|
||||
const style::RecordBar &_st;
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<DocumentData*> _document;
|
||||
const std::unique_ptr<VoiceData> _voiceData;
|
||||
const std::shared_ptr<Data::DocumentMedia> _mediaView;
|
||||
const not_null<::Media::Capture::Result*> _data;
|
||||
const not_null<Ui::RoundVideoResult*> _data;
|
||||
const base::unique_qptr<Ui::IconButton> _delete;
|
||||
const style::font &_durationFont;
|
||||
const QString _duration;
|
||||
@@ -475,6 +572,7 @@ private:
|
||||
anim::value _playProgress;
|
||||
|
||||
rpl::variable<float64> _showProgress = 0.;
|
||||
rpl::event_stream<> _videoRepaints;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
@@ -484,7 +582,7 @@ ListenWrap::ListenWrap(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::RecordBar &st,
|
||||
not_null<Main::Session*> session,
|
||||
::Media::Capture::Result *data,
|
||||
not_null<Ui::RoundVideoResult*> data,
|
||||
const style::font &font)
|
||||
: _parent(parent)
|
||||
, _st(st)
|
||||
@@ -495,8 +593,7 @@ ListenWrap::ListenWrap(
|
||||
, _data(data)
|
||||
, _delete(base::make_unique_q<Ui::IconButton>(parent, _st.remove))
|
||||
, _durationFont(font)
|
||||
, _duration(Ui::FormatDurationText(
|
||||
float64(_data->samples) / ::Media::Player::kDefaultFrequency))
|
||||
, _duration(Ui::FormatDurationText(_data->duration / 1000))
|
||||
, _durationWidth(_durationFont->width(_duration))
|
||||
, _playPauseSt(st::mediaPlayerButton)
|
||||
, _playPauseButton(base::make_unique_q<Ui::AbstractButton>(parent))
|
||||
@@ -603,20 +700,27 @@ void ListenWrap::init() {
|
||||
}
|
||||
|
||||
// Waveform paint.
|
||||
{
|
||||
const auto rect = (progress == 1.)
|
||||
? _waveformFgRect
|
||||
: computeWaveformRect(bgCenterRect);
|
||||
if (rect.width() > 0) {
|
||||
p.translate(rect.topLeft());
|
||||
const auto waveformRect = (progress == 1.)
|
||||
? _waveformFgRect
|
||||
: computeWaveformRect(bgCenterRect);
|
||||
if (!waveformRect.isEmpty()) {
|
||||
const auto playProgress = _playProgress.current();
|
||||
if (_data->minithumbs.isNull()) {
|
||||
p.translate(waveformRect.topLeft());
|
||||
PaintWaveform(
|
||||
p,
|
||||
_voiceData.get(),
|
||||
rect.width(),
|
||||
waveformRect.width(),
|
||||
_activeWaveformBar,
|
||||
_inactiveWaveformBar,
|
||||
_playProgress.current());
|
||||
playProgress);
|
||||
p.resetTransform();
|
||||
} else {
|
||||
FillWithMinithumbs(
|
||||
p,
|
||||
_data,
|
||||
waveformRect,
|
||||
playProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -630,9 +734,11 @@ void ListenWrap::initPlayButton() {
|
||||
using namespace ::Media::Player;
|
||||
using State = TrackState;
|
||||
|
||||
_mediaView->setBytes(_data->bytes);
|
||||
_document->size = _data->bytes.size();
|
||||
_document->type = VoiceDocument;
|
||||
_mediaView->setBytes(_data->content);
|
||||
_document->size = _data->content.size();
|
||||
_document->type = _data->minithumbs.isNull()
|
||||
? VoiceDocument
|
||||
: RoundVideoDocument;
|
||||
|
||||
const auto &play = _playPauseSt.playOuter;
|
||||
const auto &width = _waveformBgFinalCenterRect.height();
|
||||
@@ -668,6 +774,9 @@ void ListenWrap::initPlayButton() {
|
||||
) | rpl::start_with_next([=](const State &state) {
|
||||
if (isInPlayer(state)) {
|
||||
*showPause = ShowPauseIcon(state.state);
|
||||
if (!_data->minithumbs.isNull()) {
|
||||
_videoRepaints.fire({});
|
||||
}
|
||||
} else if (showPause->current()) {
|
||||
*showPause = false;
|
||||
}
|
||||
@@ -678,6 +787,13 @@ void ListenWrap::initPlayButton() {
|
||||
) | rpl::start_with_next([=] {
|
||||
*showPause = false;
|
||||
}, _lifetime);
|
||||
|
||||
_lifetime.add([=] {
|
||||
const auto current = instance()->current(AudioMsgId::Type::Voice);
|
||||
if (current.audio() == _document) {
|
||||
instance()->stop(AudioMsgId::Type::Voice, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ListenWrap::initPlayProgress() {
|
||||
@@ -817,6 +933,12 @@ rpl::producer<> ListenWrap::stopRequests() const {
|
||||
return _delete->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> ListenWrap::videoPreview() {
|
||||
return std::make_shared<SoundedPreview>(
|
||||
_document,
|
||||
_videoRepaints.events());
|
||||
}
|
||||
|
||||
rpl::lifetime &ListenWrap::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
@@ -831,6 +953,7 @@ public:
|
||||
void requestPaintLockToStopProgress(float64 progress);
|
||||
void requestPaintPauseToInputProgress(float64 progress);
|
||||
void setVisibleTopPart(int part);
|
||||
void setRecordingVideo(bool value);
|
||||
|
||||
[[nodiscard]] rpl::producer<> locks() const;
|
||||
[[nodiscard]] bool isLocked() const;
|
||||
@@ -859,6 +982,7 @@ private:
|
||||
float64 _pauseToInputProgress = 0.;
|
||||
rpl::variable<float64> _progress = 0.;
|
||||
int _visibleTopPart = -1;
|
||||
bool _recordingVideo = false;
|
||||
|
||||
};
|
||||
|
||||
@@ -882,6 +1006,10 @@ void RecordLock::setVisibleTopPart(int part) {
|
||||
_visibleTopPart = part;
|
||||
}
|
||||
|
||||
void RecordLock::setRecordingVideo(bool value) {
|
||||
_recordingVideo = value;
|
||||
}
|
||||
|
||||
void RecordLock::init() {
|
||||
shownValue(
|
||||
) | rpl::start_with_next([=](bool shown) {
|
||||
@@ -972,9 +1100,10 @@ void RecordLock::drawProgress(QPainter &p) {
|
||||
p.setBrush(_st.fg);
|
||||
if (_pauseToInputProgress > 0.) {
|
||||
p.setOpacity(_pauseToInputProgress);
|
||||
st::historyRecordLockInput.paintInCenter(
|
||||
p,
|
||||
blockRect.toRect());
|
||||
const auto &icon = _recordingVideo
|
||||
? st::historyRecordLockRound
|
||||
: st::historyRecordLockInput;
|
||||
icon.paintInCenter(p, blockRect.toRect());
|
||||
p.setOpacity(1. - _pauseToInputProgress);
|
||||
}
|
||||
p.drawRoundedRect(
|
||||
@@ -1244,7 +1373,7 @@ VoiceRecordBar::VoiceRecordBar(
|
||||
}
|
||||
|
||||
VoiceRecordBar::~VoiceRecordBar() {
|
||||
if (isRecording()) {
|
||||
if (isActive()) {
|
||||
stopRecording(StopType::Cancel);
|
||||
}
|
||||
}
|
||||
@@ -1473,7 +1602,6 @@ void VoiceRecordBar::init() {
|
||||
if (!paused) {
|
||||
return;
|
||||
}
|
||||
// _lockShowing = false;
|
||||
|
||||
const auto to = 1.;
|
||||
auto callback = [=](float64 value) {
|
||||
@@ -1529,12 +1657,12 @@ void VoiceRecordBar::init() {
|
||||
if (_startRecordingFilter && _startRecordingFilter()) {
|
||||
return;
|
||||
}
|
||||
_recordingTipRequired = true;
|
||||
_recordingTipRequire = crl::now();
|
||||
_recordingVideo = (_send->type() == Ui::SendButton::Type::Round);
|
||||
_lock->setRecordingVideo(_recordingVideo);
|
||||
_startTimer.callOnce(st::universalDuration);
|
||||
} else if (e->type() == QEvent::MouseButtonRelease) {
|
||||
if (base::take(_recordingTipRequired)) {
|
||||
_recordingTipRequests.fire({});
|
||||
}
|
||||
checkTipRequired();
|
||||
_startTimer.cancel();
|
||||
}
|
||||
}, lifetime());
|
||||
@@ -1579,6 +1707,11 @@ void VoiceRecordBar::activeAnimate(bool active) {
|
||||
}
|
||||
|
||||
void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
|
||||
if (_send->type() == Ui::SendButton::Type::Round) {
|
||||
_level->setType(VoiceRecordButton::Type::Round);
|
||||
} else {
|
||||
_level->setType(VoiceRecordButton::Type::Record);
|
||||
}
|
||||
const auto to = show ? 1. : 0.;
|
||||
const auto from = show ? 0. : 1.;
|
||||
auto animationCallback = [=, callback = std::move(callback)](auto value) {
|
||||
@@ -1652,6 +1785,10 @@ void VoiceRecordBar::startRecording() {
|
||||
}
|
||||
|
||||
using namespace ::Media::Capture;
|
||||
if (_recordingVideo && !createVideoRecorder()) {
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
if (!instance()->available()) {
|
||||
stop(false);
|
||||
return;
|
||||
@@ -1664,16 +1801,35 @@ void VoiceRecordBar::startRecording() {
|
||||
if (_paused.current()) {
|
||||
_paused = false;
|
||||
instance()->pause(false, nullptr);
|
||||
if (_videoRecorder) {
|
||||
_videoRecorder->resume({
|
||||
.video = std::move(_data),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
instance()->start();
|
||||
instance()->start(_videoRecorder
|
||||
? _videoRecorder->audioChunkProcessor()
|
||||
: nullptr);
|
||||
}
|
||||
instance()->updated(
|
||||
) | rpl::start_with_next_error([=](const Update &update) {
|
||||
_recordingTipRequired = (update.samples < kMinSamples);
|
||||
recordUpdated(update.level, update.samples);
|
||||
}, [=] {
|
||||
stop(false);
|
||||
}, _recordingLifetime);
|
||||
if (_videoRecorder) {
|
||||
_videoRecorder->updated(
|
||||
) | rpl::start_with_next_error([=](const Update &update) {
|
||||
recordUpdated(update.level, update.samples);
|
||||
if (update.finished) {
|
||||
stopRecording(StopType::Listen);
|
||||
_lockShowing = false;
|
||||
}
|
||||
}, [=](Error error) {
|
||||
stop(false);
|
||||
_errors.fire_copy(error);
|
||||
}, _recordingLifetime);
|
||||
}
|
||||
_recordingLifetime.add([=] {
|
||||
_recording = false;
|
||||
});
|
||||
@@ -1705,14 +1861,22 @@ void VoiceRecordBar::startRecording() {
|
||||
}
|
||||
computeAndSetLockProgress(mouse->globalPos());
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
if (base::take(_recordingTipRequired)) {
|
||||
_recordingTipRequests.fire({});
|
||||
}
|
||||
checkTipRequired();
|
||||
stop(_inField.current());
|
||||
}
|
||||
}, _recordingLifetime);
|
||||
}
|
||||
|
||||
void VoiceRecordBar::checkTipRequired() {
|
||||
const auto require = base::take(_recordingTipRequire);
|
||||
const auto duration = st::universalDuration
|
||||
+ (kMinSamples * crl::time(1000)
|
||||
/ ::Media::Player::kDefaultFrequency);
|
||||
if (require && (require + duration > crl::now())) {
|
||||
_recordingTipRequests.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceRecordBar::recordUpdated(quint16 level, int samples) {
|
||||
_level->requestPaintLevel(level);
|
||||
_recordingSamples = samples;
|
||||
@@ -1735,7 +1899,6 @@ void VoiceRecordBar::stop(bool send) {
|
||||
const auto type = send ? StopType::Send : StopType::Cancel;
|
||||
stopRecording(type, ttlBeforeHide);
|
||||
};
|
||||
// _lockShowing = false;
|
||||
visibilityAnimate(false, std::move(disappearanceCallback));
|
||||
}
|
||||
|
||||
@@ -1769,39 +1932,99 @@ void VoiceRecordBar::hideFast() {
|
||||
void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
|
||||
using namespace ::Media::Capture;
|
||||
if (type == StopType::Cancel) {
|
||||
if (_videoRecorder) {
|
||||
_videoRecorder->hide();
|
||||
}
|
||||
instance()->stop(crl::guard(this, [=](Result &&data) {
|
||||
_cancelRequests.fire({});
|
||||
}));
|
||||
} else if (type == StopType::Listen) {
|
||||
instance()->pause(true, crl::guard(this, [=](Result &&data) {
|
||||
if (data.bytes.isEmpty()) {
|
||||
// Close everything.
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
_paused = true;
|
||||
_data = std::move(data);
|
||||
if (const auto recorder = _videoRecorder.get()) {
|
||||
const auto weak = base::make_weak(recorder);
|
||||
recorder->pause([=](Ui::RoundVideoResult data) {
|
||||
crl::on_main(weak, [=, data = std::move(data)]() mutable {
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
_listen = std::make_unique<ListenWrap>(
|
||||
this,
|
||||
_st,
|
||||
&_show->session(),
|
||||
&_data,
|
||||
_cancelFont);
|
||||
_listenChanges.fire({});
|
||||
_paused = true;
|
||||
_data = std::move(data);
|
||||
_listen = std::make_unique<ListenWrap>(
|
||||
this,
|
||||
_st,
|
||||
&_show->session(),
|
||||
&_data,
|
||||
_cancelFont);
|
||||
_listenChanges.fire({});
|
||||
|
||||
// _lockShowing = false;
|
||||
}));
|
||||
using SilentPreview = ::Media::Streaming::RoundPreview;
|
||||
recorder->showPreview(
|
||||
std::make_shared<SilentPreview>(
|
||||
_data.content,
|
||||
recorder->previewSize()),
|
||||
_listen->videoPreview());
|
||||
});
|
||||
});
|
||||
instance()->pause(true);
|
||||
} else {
|
||||
instance()->pause(true, crl::guard(this, [=](Result &&data) {
|
||||
if (data.bytes.isEmpty()) {
|
||||
// Close everything.
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
_paused = true;
|
||||
_data = Ui::RoundVideoResult{
|
||||
.content = std::move(data.bytes),
|
||||
.waveform = std::move(data.waveform),
|
||||
.duration = data.duration,
|
||||
};
|
||||
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
_listen = std::make_unique<ListenWrap>(
|
||||
this,
|
||||
_st,
|
||||
&_show->session(),
|
||||
&_data,
|
||||
_cancelFont);
|
||||
_listenChanges.fire({});
|
||||
}));
|
||||
}
|
||||
} else if (type == StopType::Send) {
|
||||
if (_videoRecorder) {
|
||||
const auto weak = Ui::MakeWeak(this);
|
||||
_videoRecorder->hide([=](Ui::RoundVideoResult data) {
|
||||
crl::on_main([=, data = std::move(data)]() mutable {
|
||||
if (weak) {
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
const auto options = Api::SendOptions{
|
||||
.ttlSeconds = (ttlBeforeHide
|
||||
? std::numeric_limits<int>::max()
|
||||
: 0),
|
||||
};
|
||||
_sendVoiceRequests.fire({
|
||||
.bytes = data.content,
|
||||
//.waveform = {},
|
||||
.duration = data.duration,
|
||||
.options = options,
|
||||
.video = true,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
instance()->stop(crl::guard(this, [=](Result &&data) {
|
||||
if (data.bytes.isEmpty()) {
|
||||
// Close everything.
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
_data = std::move(data);
|
||||
_data = Ui::RoundVideoResult{
|
||||
.content = std::move(data.bytes),
|
||||
.waveform = std::move(data.waveform),
|
||||
.duration = data.duration,
|
||||
};
|
||||
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
@@ -1811,10 +2034,10 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
|
||||
: 0),
|
||||
};
|
||||
_sendVoiceRequests.fire({
|
||||
_data.bytes,
|
||||
_data.waveform,
|
||||
Duration(_data.samples),
|
||||
options,
|
||||
.bytes = _data.content,
|
||||
.waveform = _data.waveform,
|
||||
.duration = _data.duration,
|
||||
.options = options,
|
||||
});
|
||||
}));
|
||||
}
|
||||
@@ -1878,10 +2101,11 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
|
||||
options.ttlSeconds = std::numeric_limits<int>::max();
|
||||
}
|
||||
_sendVoiceRequests.fire({
|
||||
_data.bytes,
|
||||
_data.waveform,
|
||||
Duration(_data.samples),
|
||||
options,
|
||||
.bytes = _data.content,
|
||||
.waveform = _data.waveform,
|
||||
.duration = _data.duration,
|
||||
.options = options,
|
||||
.video = !_data.minithumbs.isNull(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1954,6 +2178,10 @@ rpl::producer<> VoiceRecordBar::recordingTipRequests() const {
|
||||
return _recordingTipRequests.events();
|
||||
}
|
||||
|
||||
auto VoiceRecordBar::errors() const -> rpl::producer<Error> {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::isLockPresent() const {
|
||||
return _lockShowing.current();
|
||||
}
|
||||
@@ -1963,7 +2191,8 @@ bool VoiceRecordBar::isListenState() const {
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::isTypeRecord() const {
|
||||
return (_send->type() == Ui::SendButton::Type::Record);
|
||||
return (_send->type() == Ui::SendButton::Type::Record)
|
||||
|| (_send->type() == Ui::SendButton::Type::Round);
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::isRecordingByAnotherBar() const {
|
||||
@@ -2085,8 +2314,12 @@ void VoiceRecordBar::showDiscardBox(
|
||||
};
|
||||
_show->showBox(Ui::MakeConfirmBox({
|
||||
.text = (isListenState()
|
||||
? tr::lng_record_listen_cancel_sure
|
||||
: tr::lng_record_lock_cancel_sure)(),
|
||||
? (_recordingVideo
|
||||
? tr::lng_record_listen_cancel_sure_round
|
||||
: tr::lng_record_listen_cancel_sure)
|
||||
: (_recordingVideo
|
||||
? tr::lng_record_lock_cancel_sure_round
|
||||
: tr::lng_record_lock_cancel_sure))(),
|
||||
.confirmed = std::move(sure),
|
||||
.confirmText = tr::lng_record_lock_discard(),
|
||||
.confirmStyle = &st::attentionBoxButton,
|
||||
@@ -2094,4 +2327,52 @@ void VoiceRecordBar::showDiscardBox(
|
||||
_warningShown = true;
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::createVideoRecorder() {
|
||||
if (_videoRecorder) {
|
||||
return true;
|
||||
}
|
||||
const auto hiding = [=](not_null<Ui::RoundVideoRecorder*> which) {
|
||||
if (_videoRecorder.get() == which) {
|
||||
_videoHiding.push_back(base::take(_videoRecorder));
|
||||
}
|
||||
};
|
||||
const auto hidden = [=](not_null<Ui::RoundVideoRecorder*> which) {
|
||||
if (_videoRecorder.get() == which) {
|
||||
_videoRecorder = nullptr;
|
||||
}
|
||||
_videoHiding.erase(
|
||||
ranges::remove(
|
||||
_videoHiding,
|
||||
which.get(),
|
||||
&std::unique_ptr<Ui::RoundVideoRecorder>::get),
|
||||
end(_videoHiding));
|
||||
};
|
||||
auto capturer = Core::App().calls().getVideoCapture();
|
||||
auto track = std::make_shared<Webrtc::VideoTrack>(
|
||||
Webrtc::VideoState::Active);
|
||||
capturer->setOutput(track->sink());
|
||||
capturer->setPreferredAspectRatio(1.);
|
||||
_videoCapturerLifetime = track->stateValue(
|
||||
) | rpl::start_with_next([=](Webrtc::VideoState state) {
|
||||
capturer->setState((state == Webrtc::VideoState::Active)
|
||||
? tgcalls::VideoState::Active
|
||||
: tgcalls::VideoState::Inactive);
|
||||
});
|
||||
_videoRecorder = std::make_unique<Ui::RoundVideoRecorder>(
|
||||
Ui::RoundVideoRecorderDescriptor{
|
||||
.container = _outerContainer,
|
||||
.hiding = hiding,
|
||||
.hidden = hidden,
|
||||
.capturer = std::move(capturer),
|
||||
.track = std::move(track),
|
||||
.placeholder = _show->session().local().readRoundPlaceholder(),
|
||||
});
|
||||
_videoRecorder->placeholderUpdates(
|
||||
) | rpl::start_with_next([=](QImage &&placeholder) {
|
||||
_show->session().local().writeRoundPlaceholder(placeholder);
|
||||
}, _videoCapturerLifetime);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/timer.h"
|
||||
#include "history/view/controls/compose_controls_common.h"
|
||||
#include "media/audio/media_audio_capture_common.h"
|
||||
#include "ui/controls/round_video_recorder.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
@@ -21,9 +22,14 @@ namespace style {
|
||||
struct RecordBar;
|
||||
} // namespace style
|
||||
|
||||
namespace Media::Capture {
|
||||
enum class Error : uchar;
|
||||
} // namespace Media::Capture
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class SendButton;
|
||||
class RoundVideoRecorder;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
@@ -56,6 +62,7 @@ public:
|
||||
using SendActionUpdate = Controls::SendActionUpdate;
|
||||
using VoiceToSend = Controls::VoiceToSend;
|
||||
using FilterCallback = Fn<bool()>;
|
||||
using Error = ::Media::Capture::Error;
|
||||
|
||||
VoiceRecordBar(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
@@ -87,6 +94,7 @@ public:
|
||||
[[nodiscard]] rpl::producer<not_null<QEvent*>> lockViewportEvents() const;
|
||||
[[nodiscard]] rpl::producer<> updateSendButtonTypeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> recordingTipRequests() const;
|
||||
[[nodiscard]] rpl::producer<Error> errors() const;
|
||||
|
||||
void requestToSendWithOptions(Api::SendOptions options);
|
||||
|
||||
@@ -123,14 +131,12 @@ private:
|
||||
void updateTTLGeometry(TTLAnimationType type, float64 progress);
|
||||
|
||||
void recordUpdated(quint16 level, int samples);
|
||||
|
||||
[[nodiscard]] bool recordingAnimationCallback(crl::time now);
|
||||
void checkTipRequired();
|
||||
|
||||
void stop(bool send);
|
||||
void stopRecording(StopType type, bool ttlBeforeHide = false);
|
||||
void visibilityAnimate(bool show, Fn<void()> &&callback);
|
||||
|
||||
[[nodiscard]] bool showRecordButton() const;
|
||||
void drawDuration(QPainter &p);
|
||||
void drawRedCircle(QPainter &p);
|
||||
void drawMessage(QPainter &p, float64 recordActive);
|
||||
@@ -153,6 +159,8 @@ private:
|
||||
[[nodiscard]] bool peekTTLState() const;
|
||||
[[nodiscard]] bool takeTTLState() const;
|
||||
|
||||
[[nodiscard]] bool createVideoRecorder();
|
||||
|
||||
const style::RecordBar &_st;
|
||||
const not_null<Ui::RpWidget*> _outerContainer;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
@@ -163,7 +171,7 @@ private:
|
||||
std::unique_ptr<Ui::AbstractButton> _ttlButton;
|
||||
std::unique_ptr<ListenWrap> _listen;
|
||||
|
||||
::Media::Capture::Result _data;
|
||||
Ui::RoundVideoResult _data;
|
||||
rpl::variable<bool> _paused;
|
||||
|
||||
base::Timer _startTimer;
|
||||
@@ -172,6 +180,7 @@ private:
|
||||
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
|
||||
rpl::event_stream<> _cancelRequests;
|
||||
rpl::event_stream<> _listenChanges;
|
||||
rpl::event_stream<Error> _errors;
|
||||
|
||||
int _centerY = 0;
|
||||
QRect _redCircleRect;
|
||||
@@ -192,9 +201,14 @@ private:
|
||||
float64 _redCircleProgress = 0.;
|
||||
|
||||
rpl::event_stream<> _recordingTipRequests;
|
||||
bool _recordingTipRequired = false;
|
||||
crl::time _recordingTipRequire = 0;
|
||||
bool _lockFromBottom = false;
|
||||
|
||||
std::unique_ptr<Ui::RoundVideoRecorder> _videoRecorder;
|
||||
std::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding;
|
||||
rpl::lifetime _videoCapturerLifetime;
|
||||
bool _recordingVideo = false;
|
||||
|
||||
const style::font &_cancelFont;
|
||||
|
||||
rpl::lifetime _recordingLifetime;
|
||||
|
||||
@@ -137,10 +137,14 @@ void VoiceRecordButton::init() {
|
||||
const auto state = *currentState;
|
||||
const auto icon = (state == Type::Send)
|
||||
? st::historySendIcon
|
||||
: st::historyRecordVoiceActive;
|
||||
: (state == Type::Record)
|
||||
? st::historyRecordVoiceActive
|
||||
: st::historyRecordRoundActive;
|
||||
const auto position = (state == Type::Send)
|
||||
? st::historyRecordSendIconPosition
|
||||
: QPoint(0, 0);
|
||||
: (state == Type::Record)
|
||||
? QPoint(0, 0)
|
||||
: st::historyRecordRoundIconPosition;
|
||||
icon.paint(
|
||||
p,
|
||||
-icon.width() / 2 + position.x(),
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
enum class Type {
|
||||
Send,
|
||||
Record,
|
||||
Round,
|
||||
};
|
||||
|
||||
void setType(Type state);
|
||||
|
||||
@@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_file_click_handler.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "chat_helpers/message_field.h" // FactcheckFieldIniter.
|
||||
#include "core/file_utilities.h"
|
||||
@@ -1265,11 +1266,10 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
|
||||
}
|
||||
}
|
||||
|
||||
if (!view || !list->hasCopyRestriction(view->data())) {
|
||||
AddCopyLinkAction(result, link);
|
||||
}
|
||||
AddCopyLinkAction(result, link);
|
||||
AddMessageActions(result, request, list);
|
||||
|
||||
const auto wasAmount = result->actions().size();
|
||||
if (const auto textItem = view ? view->textItem() : item) {
|
||||
AddEmojiPacksAction(
|
||||
result,
|
||||
@@ -1277,6 +1277,13 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
|
||||
HistoryView::EmojiPacksSource::Message,
|
||||
list->controller());
|
||||
}
|
||||
if (item) {
|
||||
const auto added = (result->actions().size() > wasAmount);
|
||||
if (!added) {
|
||||
result->addSeparator();
|
||||
}
|
||||
AddSelectRestrictionAction(result, item, !added);
|
||||
}
|
||||
if (hasWhoReactedItem) {
|
||||
AddWhoReactedAction(result, list, item, list->controller());
|
||||
}
|
||||
@@ -1841,6 +1848,36 @@ void AddEmojiPacksAction(
|
||||
controller);
|
||||
}
|
||||
|
||||
void AddSelectRestrictionAction(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<HistoryItem*> item,
|
||||
bool addIcon) {
|
||||
const auto peer = item->history()->peer;
|
||||
if ((peer->allowsForwarding() && !item->forbidsForward())
|
||||
|| item->isSponsored()) {
|
||||
return;
|
||||
}
|
||||
auto button = base::make_unique_q<Ui::Menu::MultilineAction>(
|
||||
menu->menu(),
|
||||
menu->st().menu,
|
||||
st::historyHasCustomEmoji,
|
||||
addIcon
|
||||
? st::historySponsoredAboutMenuLabelPosition
|
||||
: st::historyHasCustomEmojiPosition,
|
||||
(peer->isMegagroup()
|
||||
? tr::lng_context_noforwards_info_group
|
||||
: (peer->isChannel())
|
||||
? tr::lng_context_noforwards_info_channel
|
||||
: (peer->isUser() && peer->asUser()->isBot())
|
||||
? tr::lng_context_noforwards_info_channel
|
||||
: tr::lng_context_noforwards_info_bot)(
|
||||
tr::now,
|
||||
Ui::Text::RichLangValue),
|
||||
addIcon ? &st::menuIconCopyright : nullptr);
|
||||
button->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
menu->addAction(std::move(button));
|
||||
}
|
||||
|
||||
TextWithEntities TransribedText(not_null<HistoryItem*> item) {
|
||||
const auto media = item->media();
|
||||
const auto document = media ? media->document() : nullptr;
|
||||
|
||||
@@ -122,6 +122,10 @@ void AddEmojiPacksAction(
|
||||
not_null<HistoryItem*> item,
|
||||
EmojiPacksSource source,
|
||||
not_null<Window::SessionController*> controller);
|
||||
void AddSelectRestrictionAction(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<HistoryItem*> item,
|
||||
bool addIcon);
|
||||
|
||||
[[nodiscard]] TextWithEntities TransribedText(not_null<HistoryItem*> item);
|
||||
|
||||
|
||||
@@ -1614,12 +1614,21 @@ void Message::draw(Painter &p, const PaintContext &context) const {
|
||||
}
|
||||
const auto o = ScopedPainterOpacity(p, progress);
|
||||
const auto &st = st::msgSelectionCheck;
|
||||
const auto right = delegate()->elementIsChatWide()
|
||||
? std::min(
|
||||
int(_bubbleWidthLimit
|
||||
+ st::msgPhotoSkip
|
||||
+ st::msgSelectionOffset
|
||||
+ st::msgPadding.left()
|
||||
+ st.size),
|
||||
width())
|
||||
: width();
|
||||
const auto pos = QPoint(
|
||||
(width()
|
||||
(right
|
||||
- (st::msgSelectionOffset * progress - st.size) / 2
|
||||
- st::msgPadding.right() / 2
|
||||
- st.size),
|
||||
g.y() + (g.height() - st.size) / 2);
|
||||
rect::bottom(g) - st.size - st::msgSelectionBottomSkip);
|
||||
{
|
||||
p.setPen(QPen(st.border, st.width));
|
||||
p.setBrush(context.st->msgServiceBg());
|
||||
|
||||
@@ -1224,6 +1224,7 @@ void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) {
|
||||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
std::move(action));
|
||||
|
||||
_composeControls->cancelReplyMessage();
|
||||
|
||||
@@ -57,29 +57,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace HistoryView {
|
||||
|
||||
ScheduledMemento::ScheduledMemento(not_null<History*> history)
|
||||
: _history(history)
|
||||
, _forumTopic(nullptr) {
|
||||
: _history(history)
|
||||
, _forumTopic(nullptr) {
|
||||
const auto list = _history->session().scheduledMessages().list(_history);
|
||||
if (!list.ids.empty()) {
|
||||
_list.setScrollTopState({ .item = { .fullId = list.ids.front() } });
|
||||
_list.setScrollTopState({ .item = {.fullId = list.ids.front() } });
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledMemento::ScheduledMemento(not_null<Data::ForumTopic*> forumTopic)
|
||||
: _history(forumTopic->owningHistory())
|
||||
, _forumTopic(forumTopic) {
|
||||
: _history(forumTopic->owningHistory())
|
||||
, _forumTopic(forumTopic) {
|
||||
const auto list = _history->session().scheduledMessages().list(
|
||||
_forumTopic);
|
||||
if (!list.ids.empty()) {
|
||||
_list.setScrollTopState({ .item = { .fullId = list.ids.front() } });
|
||||
_list.setScrollTopState({ .item = {.fullId = list.ids.front() } });
|
||||
}
|
||||
}
|
||||
|
||||
object_ptr<Window::SectionWidget> ScheduledMemento::createWidget(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
Window::Column column,
|
||||
const QRect &geometry) {
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
Window::Column column,
|
||||
const QRect &geometry) {
|
||||
if (column == Window::Column::Third) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -97,30 +97,30 @@ ScheduledWidget::ScheduledWidget(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history,
|
||||
const Data::ForumTopic *forumTopic)
|
||||
: Window::SectionWidget(parent, controller, history->peer)
|
||||
, WindowListDelegate(controller)
|
||||
, _show(controller->uiShow())
|
||||
, _history(history)
|
||||
, _forumTopic(forumTopic)
|
||||
, _scroll(
|
||||
this,
|
||||
controller->chatStyle()->value(lifetime(), st::historyScroll),
|
||||
false)
|
||||
, _topBar(this, controller)
|
||||
, _topBarShadow(this)
|
||||
, _composeControls(std::make_unique<ComposeControls>(
|
||||
this,
|
||||
ComposeControlsDescriptor{
|
||||
.show = controller->uiShow(),
|
||||
.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
|
||||
listShowPremiumToast(emoji);
|
||||
},
|
||||
.mode = ComposeControls::Mode::Scheduled,
|
||||
.sendMenuDetails = [] { return SendMenu::Details(); },
|
||||
.regularWindow = controller,
|
||||
.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),
|
||||
}))
|
||||
, _cornerButtons(
|
||||
: Window::SectionWidget(parent, controller, history->peer)
|
||||
, WindowListDelegate(controller)
|
||||
, _show(controller->uiShow())
|
||||
, _history(history)
|
||||
, _forumTopic(forumTopic)
|
||||
, _scroll(
|
||||
this,
|
||||
controller->chatStyle()->value(lifetime(), st::historyScroll),
|
||||
false)
|
||||
, _topBar(this, controller)
|
||||
, _topBarShadow(this)
|
||||
, _composeControls(std::make_unique<ComposeControls>(
|
||||
this,
|
||||
ComposeControlsDescriptor{
|
||||
.show = controller->uiShow(),
|
||||
.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
|
||||
listShowPremiumToast(emoji);
|
||||
},
|
||||
.mode = ComposeControls::Mode::Scheduled,
|
||||
.sendMenuDetails = [] { return SendMenu::Details(); },
|
||||
.regularWindow = controller,
|
||||
.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),
|
||||
}))
|
||||
, _cornerButtons(
|
||||
_scroll.data(),
|
||||
controller->chatStyle(),
|
||||
static_cast<HistoryView::CornerButtonsDelegate*>(this)) {
|
||||
@@ -209,83 +209,83 @@ ScheduledWidget::~ScheduledWidget() = default;
|
||||
void ScheduledWidget::setupComposeControls() {
|
||||
auto writeRestriction = _forumTopic
|
||||
? [&] {
|
||||
auto topicWriteRestrictions = rpl::single(
|
||||
) | rpl::then(session().changes().topicUpdates(
|
||||
Data::TopicUpdate::Flag::Closed
|
||||
) | rpl::filter([=](const Data::TopicUpdate &update) {
|
||||
return (update.topic->history() == _history)
|
||||
&& (update.topic->rootId() == _forumTopic->rootId());
|
||||
}) | rpl::to_empty) | rpl::map([=] {
|
||||
return (!_forumTopic
|
||||
|| _forumTopic->canToggleClosed()
|
||||
|| !_forumTopic->closed())
|
||||
? std::optional<QString>()
|
||||
: tr::lng_forum_topic_closed(tr::now);
|
||||
});
|
||||
return rpl::combine(
|
||||
session().changes().peerFlagsValue(
|
||||
_history->peer,
|
||||
Data::PeerUpdate::Flag::Rights),
|
||||
Data::CanSendAnythingValue(_history->peer),
|
||||
std::move(topicWriteRestrictions)
|
||||
) | rpl::map([=](
|
||||
auto,
|
||||
auto,
|
||||
std::optional<QString> topicRestriction) {
|
||||
const auto allWithoutPolls = Data::AllSendRestrictions()
|
||||
& ~ChatRestriction::SendPolls;
|
||||
const auto canSendAnything = Data::CanSendAnyOf(
|
||||
_forumTopic,
|
||||
allWithoutPolls);
|
||||
const auto restriction = Data::RestrictionError(
|
||||
_history->peer,
|
||||
ChatRestriction::SendOther);
|
||||
auto text = !canSendAnything
|
||||
? (restriction
|
||||
? restriction
|
||||
: topicRestriction
|
||||
? std::move(topicRestriction)
|
||||
: tr::lng_group_not_accessible(tr::now))
|
||||
auto topicWriteRestrictions = rpl::single(
|
||||
) | rpl::then(session().changes().topicUpdates(
|
||||
Data::TopicUpdate::Flag::Closed
|
||||
) | rpl::filter([=](const Data::TopicUpdate &update) {
|
||||
return (update.topic->history() == _history)
|
||||
&& (update.topic->rootId() == _forumTopic->rootId());
|
||||
}) | rpl::to_empty) | rpl::map([=] {
|
||||
return (!_forumTopic
|
||||
|| _forumTopic->canToggleClosed()
|
||||
|| !_forumTopic->closed())
|
||||
? std::optional<QString>()
|
||||
: tr::lng_forum_topic_closed(tr::now);
|
||||
});
|
||||
return rpl::combine(
|
||||
session().changes().peerFlagsValue(
|
||||
_history->peer,
|
||||
Data::PeerUpdate::Flag::Rights),
|
||||
Data::CanSendAnythingValue(_history->peer),
|
||||
std::move(topicWriteRestrictions)
|
||||
) | rpl::map([=](
|
||||
auto,
|
||||
auto,
|
||||
std::optional<QString> topicRestriction) {
|
||||
const auto allWithoutPolls = Data::AllSendRestrictions()
|
||||
& ~ChatRestriction::SendPolls;
|
||||
const auto canSendAnything = Data::CanSendAnyOf(
|
||||
_forumTopic,
|
||||
allWithoutPolls);
|
||||
const auto restriction = Data::RestrictionError(
|
||||
_history->peer,
|
||||
ChatRestriction::SendOther);
|
||||
auto text = !canSendAnything
|
||||
? (restriction
|
||||
? restriction
|
||||
: topicRestriction
|
||||
? std::move(topicRestriction)
|
||||
: std::optional<QString>();
|
||||
return text ? Controls::WriteRestriction{
|
||||
.text = std::move(*text),
|
||||
.type = Controls::WriteRestrictionType::Rights,
|
||||
} : Controls::WriteRestriction();
|
||||
}) | rpl::type_erased();
|
||||
}()
|
||||
: tr::lng_group_not_accessible(tr::now))
|
||||
: topicRestriction
|
||||
? std::move(topicRestriction)
|
||||
: std::optional<QString>();
|
||||
return text ? Controls::WriteRestriction{
|
||||
.text = std::move(*text),
|
||||
.type = Controls::WriteRestrictionType::Rights,
|
||||
} : Controls::WriteRestriction();
|
||||
}) | rpl::type_erased();
|
||||
}()
|
||||
: [&] {
|
||||
return rpl::combine(
|
||||
session().changes().peerFlagsValue(
|
||||
_history->peer,
|
||||
Data::PeerUpdate::Flag::Rights),
|
||||
Data::CanSendAnythingValue(_history->peer)
|
||||
) | rpl::map([=] {
|
||||
const auto allWithoutPolls = Data::AllSendRestrictions()
|
||||
& ~ChatRestriction::SendPolls;
|
||||
const auto canSendAnything = Data::CanSendAnyOf(
|
||||
_history->peer,
|
||||
allWithoutPolls,
|
||||
false);
|
||||
const auto restriction = Data::RestrictionError(
|
||||
_history->peer,
|
||||
ChatRestriction::SendOther);
|
||||
auto text = !canSendAnything
|
||||
? (restriction
|
||||
? restriction
|
||||
: tr::lng_group_not_accessible(tr::now))
|
||||
: std::optional<QString>();
|
||||
return text ? Controls::WriteRestriction{
|
||||
.text = std::move(*text),
|
||||
.type = Controls::WriteRestrictionType::Rights,
|
||||
} : Controls::WriteRestriction();
|
||||
}) | rpl::type_erased();
|
||||
}();
|
||||
return rpl::combine(
|
||||
session().changes().peerFlagsValue(
|
||||
_history->peer,
|
||||
Data::PeerUpdate::Flag::Rights),
|
||||
Data::CanSendAnythingValue(_history->peer)
|
||||
) | rpl::map([=] {
|
||||
const auto allWithoutPolls = Data::AllSendRestrictions()
|
||||
& ~ChatRestriction::SendPolls;
|
||||
const auto canSendAnything = Data::CanSendAnyOf(
|
||||
_history->peer,
|
||||
allWithoutPolls,
|
||||
false);
|
||||
const auto restriction = Data::RestrictionError(
|
||||
_history->peer,
|
||||
ChatRestriction::SendOther);
|
||||
auto text = !canSendAnything
|
||||
? (restriction
|
||||
? restriction
|
||||
: tr::lng_group_not_accessible(tr::now))
|
||||
: std::optional<QString>();
|
||||
return text ? Controls::WriteRestriction{
|
||||
.text = std::move(*text),
|
||||
.type = Controls::WriteRestrictionType::Rights,
|
||||
} : Controls::WriteRestriction();
|
||||
}) | rpl::type_erased();
|
||||
}();
|
||||
_composeControls->setHistory({
|
||||
.history = _history.get(),
|
||||
.writeRestriction = std::move(writeRestriction),
|
||||
});
|
||||
});
|
||||
|
||||
_composeControls->height(
|
||||
) | rpl::start_with_next([=] {
|
||||
@@ -308,7 +308,7 @@ void ScheduledWidget::setupComposeControls() {
|
||||
|
||||
_composeControls->sendVoiceRequests(
|
||||
) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) {
|
||||
sendVoice(data.bytes, data.waveform, data.duration);
|
||||
sendVoice(std::move(data));
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->sendCommandRequests(
|
||||
@@ -393,8 +393,8 @@ void ScheduledWidget::setupComposeControls() {
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->setMimeDataHook([=](
|
||||
not_null<const QMimeData*> data,
|
||||
Ui::InputField::MimeAction action) {
|
||||
not_null<const QMimeData*> data,
|
||||
Ui::InputField::MimeAction action) {
|
||||
if (action == Ui::InputField::MimeAction::Check) {
|
||||
return Core::CanSendFiles(data);
|
||||
} else if (action == Ui::InputField::MimeAction::Insert) {
|
||||
@@ -426,7 +426,7 @@ void ScheduledWidget::chooseAttach() {
|
||||
|
||||
const auto filter = FileDialog::AllOrImagesFilter();
|
||||
FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=](
|
||||
FileDialog::OpenResult &&result) {
|
||||
FileDialog::OpenResult &&result) {
|
||||
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -434,7 +434,7 @@ void ScheduledWidget::chooseAttach() {
|
||||
if (!result.remoteContent.isEmpty()) {
|
||||
auto read = Images::Read({
|
||||
.content = result.remoteContent,
|
||||
});
|
||||
});
|
||||
if (!read.image.isNull() && !read.animated) {
|
||||
confirmSendingFiles(
|
||||
std::move(read.image),
|
||||
@@ -454,9 +454,9 @@ void ScheduledWidget::chooseAttach() {
|
||||
}
|
||||
|
||||
bool ScheduledWidget::confirmSendingFiles(
|
||||
not_null<const QMimeData*> data,
|
||||
std::optional<bool> overrideSendImagesAsPhotos,
|
||||
const QString &insertTextOnCancel) {
|
||||
not_null<const QMimeData*> data,
|
||||
std::optional<bool> overrideSendImagesAsPhotos,
|
||||
const QString &insertTextOnCancel) {
|
||||
const auto hasImage = data->hasImage();
|
||||
const auto premium = controller()->session().user()->isPremium();
|
||||
|
||||
@@ -488,8 +488,8 @@ bool ScheduledWidget::confirmSendingFiles(
|
||||
}
|
||||
|
||||
bool ScheduledWidget::confirmSendingFiles(
|
||||
Ui::PreparedList &&list,
|
||||
const QString &insertTextOnCancel) {
|
||||
Ui::PreparedList &&list,
|
||||
const QString &insertTextOnCancel) {
|
||||
if (_composeControls->confirmMediaEdit(list)) {
|
||||
return true;
|
||||
} else if (showSendingFilesError(list)) {
|
||||
@@ -507,11 +507,11 @@ bool ScheduledWidget::confirmSendingFiles(
|
||||
SendMenu::Details());
|
||||
|
||||
box->setConfirmedCallback(crl::guard(this, [=](
|
||||
Ui::PreparedList &&list,
|
||||
Ui::SendFilesWay way,
|
||||
TextWithTags &&caption,
|
||||
Api::SendOptions options,
|
||||
bool ctrlShiftEnter) {
|
||||
Ui::PreparedList &&list,
|
||||
Ui::SendFilesWay way,
|
||||
TextWithTags &&caption,
|
||||
Api::SendOptions options,
|
||||
bool ctrlShiftEnter) {
|
||||
sendingFilesConfirmed(
|
||||
std::move(list),
|
||||
way,
|
||||
@@ -529,11 +529,11 @@ bool ScheduledWidget::confirmSendingFiles(
|
||||
}
|
||||
|
||||
void ScheduledWidget::sendingFilesConfirmed(
|
||||
Ui::PreparedList &&list,
|
||||
Ui::SendFilesWay way,
|
||||
TextWithTags &&caption,
|
||||
Api::SendOptions options,
|
||||
bool ctrlShiftEnter) {
|
||||
Ui::PreparedList &&list,
|
||||
Ui::SendFilesWay way,
|
||||
TextWithTags &&caption,
|
||||
Api::SendOptions options,
|
||||
bool ctrlShiftEnter) {
|
||||
Expects(list.filesToProcess.empty());
|
||||
|
||||
if (showSendingFilesError(list, way.sendImagesAsPhotos())) {
|
||||
@@ -565,10 +565,10 @@ void ScheduledWidget::sendingFilesConfirmed(
|
||||
}
|
||||
|
||||
bool ScheduledWidget::confirmSendingFiles(
|
||||
QImage &&image,
|
||||
QByteArray &&content,
|
||||
std::optional<bool> overrideSendImagesAsPhotos,
|
||||
const QString &insertTextOnCancel) {
|
||||
QImage &&image,
|
||||
QByteArray &&content,
|
||||
std::optional<bool> overrideSendImagesAsPhotos,
|
||||
const QString &insertTextOnCancel) {
|
||||
if (image.isNull()) {
|
||||
return false;
|
||||
}
|
||||
@@ -604,8 +604,8 @@ void ScheduledWidget::checkReplyReturns() {
|
||||
}
|
||||
|
||||
void ScheduledWidget::uploadFile(
|
||||
const QByteArray &fileContent,
|
||||
SendMediaType type) {
|
||||
const QByteArray &fileContent,
|
||||
SendMediaType type) {
|
||||
const auto callback = [=](Api::SendOptions options) {
|
||||
session().api().sendFile(
|
||||
fileContent,
|
||||
@@ -617,13 +617,13 @@ void ScheduledWidget::uploadFile(
|
||||
}
|
||||
|
||||
bool ScheduledWidget::showSendingFilesError(
|
||||
const Ui::PreparedList &list) const {
|
||||
const Ui::PreparedList &list) const {
|
||||
return showSendingFilesError(list, std::nullopt);
|
||||
}
|
||||
|
||||
bool ScheduledWidget::showSendingFilesError(
|
||||
const Ui::PreparedList &list,
|
||||
std::optional<bool> compress) const {
|
||||
const Ui::PreparedList &list,
|
||||
std::optional<bool> compress) const {
|
||||
const auto text = [&] {
|
||||
using Error = Ui::PreparedList::Error;
|
||||
const auto peer = _history->peer;
|
||||
@@ -656,7 +656,7 @@ bool ScheduledWidget::showSendingFilesError(
|
||||
}
|
||||
|
||||
Api::SendAction ScheduledWidget::prepareSendAction(
|
||||
Api::SendOptions options) const {
|
||||
Api::SendOptions options) const {
|
||||
auto result = Api::SendAction(_history, options);
|
||||
result.options.sendAs = _composeControls->sendAsPeer();
|
||||
if (_forumTopic) {
|
||||
@@ -716,26 +716,22 @@ void ScheduledWidget::send(Api::SendOptions options) {
|
||||
_composeControls->focus();
|
||||
}
|
||||
|
||||
void ScheduledWidget::sendVoice(
|
||||
QByteArray bytes,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration) {
|
||||
void ScheduledWidget::sendVoice(const Controls::VoiceToSend &data) {
|
||||
const auto callback = [=](Api::SendOptions options) {
|
||||
sendVoice(bytes, waveform, duration, options);
|
||||
sendVoice(base::duplicate(data), options);
|
||||
};
|
||||
controller()->show(
|
||||
PrepareScheduleBox(this, _show, sendMenuDetails(), callback));
|
||||
}
|
||||
|
||||
void ScheduledWidget::sendVoice(
|
||||
QByteArray bytes,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration,
|
||||
const Controls::VoiceToSend &data,
|
||||
Api::SendOptions options) {
|
||||
session().api().sendVoiceMessage(
|
||||
bytes,
|
||||
waveform,
|
||||
duration,
|
||||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
prepareSendAction(options));
|
||||
_composeControls->clearListenState();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ namespace InlineBots {
|
||||
class Result;
|
||||
} // namespace InlineBots
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
struct VoiceToSend;
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
class Element;
|
||||
@@ -207,14 +211,9 @@ private:
|
||||
Api::SendOptions options) const;
|
||||
void send();
|
||||
void send(Api::SendOptions options);
|
||||
void sendVoice(const Controls::VoiceToSend &data);
|
||||
void sendVoice(
|
||||
QByteArray bytes,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration);
|
||||
void sendVoice(
|
||||
QByteArray bytes,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration,
|
||||
const Controls::VoiceToSend &data,
|
||||
Api::SendOptions options);
|
||||
void edit(
|
||||
not_null<HistoryItem*> item,
|
||||
|
||||
@@ -801,7 +801,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
|
||||
p,
|
||||
context,
|
||||
fullRight,
|
||||
fullBottom,
|
||||
fullBottom - st::msgDateImgDelta,
|
||||
2 * paintx + paintw,
|
||||
(unwrapped
|
||||
? InfoDisplayType::Background
|
||||
|
||||
@@ -110,7 +110,13 @@ QSize MediaGeneric::countOptimalSize() {
|
||||
}
|
||||
|
||||
QSize MediaGeneric::countCurrentSize(int newWidth) {
|
||||
return { maxWidth(), minHeight() };
|
||||
if (newWidth > maxWidth()) {
|
||||
newWidth = maxWidth();
|
||||
}
|
||||
for (auto &entry : _entries) {
|
||||
entry.object->resizeGetHeight(newWidth);
|
||||
}
|
||||
return { newWidth, minHeight() };
|
||||
}
|
||||
|
||||
void MediaGeneric::draw(Painter &p, const PaintContext &context) const {
|
||||
|
||||
@@ -151,6 +151,10 @@ rpl::producer<QString> PremiumGift::button() {
|
||||
: tr::lng_prize_open();
|
||||
}
|
||||
|
||||
bool PremiumGift::buttonMinistars() {
|
||||
return true;
|
||||
}
|
||||
|
||||
ClickHandlerPtr PremiumGift::createViewLink() {
|
||||
if (starGift() && outgoingGift()) {
|
||||
return nullptr;
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
QString title() override;
|
||||
TextWithEntities subtitle() override;
|
||||
rpl::producer<QString> button() override;
|
||||
bool buttonMinistars() override;
|
||||
QString cornerTagText() override;
|
||||
int buttonSkip() override;
|
||||
void draw(
|
||||
|
||||
@@ -16,9 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/premium_stars_colored.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_premium.h"
|
||||
@@ -98,6 +100,12 @@ ServiceBox::ServiceBox(
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
if (_content->buttonMinistars()) {
|
||||
_button.stars = std::make_unique<Ui::Premium::ColoredMiniStars>(
|
||||
[=](const QRect &) { repaint(); },
|
||||
Ui::Premium::MiniStars::Type::SlowStars);
|
||||
_button.lastFg = std::make_unique<QColor>();
|
||||
}
|
||||
}
|
||||
|
||||
ServiceBox::~ServiceBox() = default;
|
||||
@@ -117,7 +125,21 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const {
|
||||
const auto radius = st::msgServiceGiftBoxRadius;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(context.st->msgServiceBg());
|
||||
p.drawRoundedRect(QRect(QPoint(), _innerSize), radius, radius);
|
||||
p.drawRoundedRect(Rect(_innerSize), radius, radius);
|
||||
|
||||
if (_button.stars) {
|
||||
const auto &c = context.st->msgServiceFg()->c;
|
||||
if ((*_button.lastFg) != c) {
|
||||
_button.lastFg->setRgb(c.red(), c.green(), c.blue());
|
||||
const auto padding = _button.size.height() / 2;
|
||||
_button.stars->setColorOverride(QGradientStops{
|
||||
{ 0., anim::with_alpha(c, .3) },
|
||||
{ 1., c },
|
||||
});
|
||||
_button.stars->setCenter(
|
||||
Rect(_button.size) - QMargins(padding, 0, padding, 0));
|
||||
}
|
||||
}
|
||||
|
||||
const auto content = contentRect();
|
||||
auto top = content.top() + content.height();
|
||||
@@ -340,7 +362,15 @@ bool ServiceBox::Button::empty() const {
|
||||
|
||||
void ServiceBox::Button::drawBg(QPainter &p) const {
|
||||
const auto radius = size.height() / 2.;
|
||||
p.drawRoundedRect(0, 0, size.width(), size.height(), radius, radius);
|
||||
const auto r = Rect(size);
|
||||
p.drawRoundedRect(r, radius, radius);
|
||||
if (stars) {
|
||||
auto clipPath = QPainterPath();
|
||||
clipPath.addRoundedRect(r, radius, radius);
|
||||
p.setClipPath(clipPath);
|
||||
stars->paint(p);
|
||||
p.setClipping(false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
||||
|
||||
@@ -11,6 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
namespace Premium {
|
||||
class ColoredMiniStars;
|
||||
} // namespace Premium
|
||||
} // namespace Ui
|
||||
|
||||
namespace HistoryView {
|
||||
@@ -28,6 +31,9 @@ public:
|
||||
return top();
|
||||
}
|
||||
[[nodiscard]] virtual rpl::producer<QString> button() = 0;
|
||||
[[nodiscard]] virtual bool buttonMinistars() {
|
||||
return false;
|
||||
}
|
||||
[[nodiscard]] virtual QString cornerTagText() {
|
||||
return {};
|
||||
}
|
||||
@@ -106,6 +112,8 @@ private:
|
||||
|
||||
ClickHandlerPtr link;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
std::unique_ptr<Ui::Premium::ColoredMiniStars> stars;
|
||||
std::unique_ptr<QColor> lastFg;
|
||||
|
||||
mutable QPoint lastPoint;
|
||||
} _button;
|
||||
|
||||
@@ -129,9 +129,7 @@ infoTopBarBack: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(6px, 6px);
|
||||
rippleAreaSize: 42px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
infoTopBarTitle: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowBoldFg;
|
||||
@@ -623,9 +621,7 @@ infoMembersCancelSearch: CrossButton {
|
||||
|
||||
duration: 150;
|
||||
loadingPeriod: 1000;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
infoMembersSearchTop: 15px;
|
||||
|
||||
@@ -841,9 +837,7 @@ topBarSearch: IconButton {
|
||||
|
||||
rippleAreaPosition: point(0px, 7px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
topBarCloseChoose: IconButton(topBarSearch) {
|
||||
width: 56px;
|
||||
@@ -1146,9 +1140,7 @@ infoHoursDayLabel: infoHoursState;
|
||||
infoHoursOuter: RoundButton(defaultActiveButton) {
|
||||
textBg: transparent;
|
||||
textBgOver: transparent;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
infoHoursOuterMargin: margins(8px, 4px, 8px, 4px);
|
||||
infoHoursDaySkip: 6px;
|
||||
|
||||
@@ -1026,7 +1026,7 @@ void ListWidget::showContextMenu(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (link && !_provider->hasSelectRestriction()) {
|
||||
} else if (link) {
|
||||
const auto actionText = link->copyToClipboardContextItemText();
|
||||
if (!actionText.isEmpty()) {
|
||||
_contextMenu->addAction(
|
||||
|
||||
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_credits.h"
|
||||
@@ -39,7 +40,9 @@ GiftButton::GiftButton(
|
||||
QWidget *parent,
|
||||
not_null<GiftButtonDelegate*> delegate)
|
||||
: AbstractButton(parent)
|
||||
, _delegate(delegate) {
|
||||
, _delegate(delegate)
|
||||
, _stars(this, true, Ui::Premium::MiniStars::Type::SlowStars) {
|
||||
_stars.setColorOverride(Ui::Premium::CreditsIconGradientStops());
|
||||
}
|
||||
|
||||
GiftButton::~GiftButton() {
|
||||
@@ -159,6 +162,8 @@ void GiftButton::setGeometry(QRect inner, QMargins extend) {
|
||||
void GiftButton::resizeEvent(QResizeEvent *e) {
|
||||
if (!_button.isEmpty()) {
|
||||
_button.moveLeft((width() - _button.width()) / 2);
|
||||
const auto padding = _button.height() / 2;
|
||||
_stars.setCenter(_button - QMargins(padding, 0, padding, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +312,13 @@ void GiftButton::paintEvent(QPaintEvent *e) {
|
||||
if (!premium) {
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
{
|
||||
auto clipPath = QPainterPath();
|
||||
clipPath.addRoundedRect(geometry, radius, radius);
|
||||
p.setClipPath(clipPath);
|
||||
_stars.paint(p);
|
||||
p.setClipping(false);
|
||||
}
|
||||
|
||||
if (!_text.isEmpty()) {
|
||||
p.setPen(st::windowFg);
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/effects/premium_stars_colored.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
class StickerPremiumMark;
|
||||
@@ -101,6 +102,7 @@ private:
|
||||
Ui::Text::String _text;
|
||||
Ui::Text::String _price;
|
||||
std::shared_ptr<Ui::DynamicImage> _userpic;
|
||||
Ui::Premium::ColoredMiniStars _stars;
|
||||
bool _subscribed = false;
|
||||
|
||||
QRect _button;
|
||||
|
||||
@@ -1150,7 +1150,19 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
|
||||
PhoneOrHiddenValue(user),
|
||||
tr::lng_profile_copy_phone(tr::now)).text;
|
||||
const auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) {
|
||||
phoneLabel->fillContextMenu(request);
|
||||
if (request.selection.empty()) {
|
||||
const auto callback = [=] {
|
||||
auto phone = rpl::variable<TextWithEntities>(
|
||||
PhoneOrHiddenValue(user)).current().text;
|
||||
phone.replace(' ', QString()).replace('-', QString());
|
||||
TextUtilities::SetClipboardText({ phone });
|
||||
};
|
||||
request.menu->addAction(
|
||||
tr::lng_profile_copy_phone(tr::now),
|
||||
callback);
|
||||
} else {
|
||||
phoneLabel->fillContextMenu(request);
|
||||
}
|
||||
AddPhoneMenu(request.menu, user);
|
||||
};
|
||||
phoneLabel->setContextMenuHook(hook);
|
||||
@@ -2083,7 +2095,6 @@ void ActionsFiller::addBotCommandActions(not_null<UserData*> user) {
|
||||
rpl::single(true),
|
||||
openPrivacyPolicy,
|
||||
nullptr);
|
||||
|
||||
}
|
||||
|
||||
void ActionsFiller::addReportAction() {
|
||||
|
||||
@@ -384,11 +384,9 @@ void FillBotUsepic(
|
||||
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
|
||||
box->verticalLayout(),
|
||||
bot,
|
||||
st::mainMenuUserpic);
|
||||
st::infoPersonalChannelUserpic);
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
aboutLabel->setClickHandlerFilter([=](
|
||||
const ClickHandlerPtr &,
|
||||
Qt::MouseButton) {
|
||||
aboutLabel->setClickHandlerFilter([=](auto &&...) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->showPeerHistory(
|
||||
bot->id,
|
||||
@@ -397,14 +395,36 @@ void FillBotUsepic(
|
||||
}
|
||||
return false;
|
||||
});
|
||||
Ui::IconWithTitle(
|
||||
box->verticalLayout(),
|
||||
userpic,
|
||||
Ui::CreateChild<Ui::FlatLabel>(
|
||||
box->verticalLayout(),
|
||||
rpl::single(bot->name()),
|
||||
box->getDelegate()->style().title),
|
||||
aboutLabel);
|
||||
const auto title = Ui::CreateChild<Ui::RpWidget>(box->verticalLayout());
|
||||
const auto titleLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
title,
|
||||
rpl::single(bot->name()),
|
||||
box->getDelegate()->style().title);
|
||||
const auto icon = bot->isVerified() ? &st::infoVerifiedCheck : nullptr;
|
||||
title->resize(
|
||||
titleLabel->width() + (icon ? icon->width() : 0),
|
||||
titleLabel->height());
|
||||
title->widthValue(
|
||||
) | rpl::distinct_until_changed() | rpl::start_with_next([=](int w) {
|
||||
titleLabel->resizeToWidth(w
|
||||
- (icon ? icon->width() + st::lineWidth : 0));
|
||||
}, title->lifetime());
|
||||
if (icon) {
|
||||
title->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
auto p = Painter(title);
|
||||
p.fillRect(title->rect(), Qt::transparent);
|
||||
icon->paint(
|
||||
p,
|
||||
std::min(
|
||||
titleLabel->textMaxWidth() + st::lineWidth,
|
||||
title->width() - st::lineWidth - icon->width()),
|
||||
(title->height() - icon->height()) / 2,
|
||||
title->width());
|
||||
}, title->lifetime());
|
||||
}
|
||||
|
||||
Ui::IconWithTitle(box->verticalLayout(), userpic, title, aboutLabel);
|
||||
}
|
||||
|
||||
class BotAction final : public Ui::Menu::ItemBase {
|
||||
|
||||
@@ -165,9 +165,7 @@ introBackButton: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(8px, 8px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
introQrTop: -18px;
|
||||
|
||||
@@ -17,9 +17,7 @@ ivMenuToggle: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(6px, 6px);
|
||||
rippleAreaSize: 36px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
ivMenuPosition: point(-2px, 40px);
|
||||
ivBackIcon: icon {{ "box_button_back", menuIconColor }};
|
||||
@@ -29,6 +27,36 @@ ivBack: IconButton(ivMenuToggle) {
|
||||
iconOver: ivBackIcon;
|
||||
rippleAreaPosition: point(12px, 6px);
|
||||
}
|
||||
ivZoomButtonsSize: 26px;
|
||||
ivPlusMinusZoom: IconButton(ivMenuToggle) {
|
||||
width: ivZoomButtonsSize;
|
||||
height: ivZoomButtonsSize;
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: ivZoomButtonsSize;
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
ivResetZoomStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(12px);
|
||||
}
|
||||
ivResetZoom: RoundButton(defaultActiveButton) {
|
||||
textFg: windowFg;
|
||||
textFgOver: windowFgOver;
|
||||
textBg: windowBg;
|
||||
textBgOver: windowBgOver;
|
||||
|
||||
height: ivZoomButtonsSize;
|
||||
padding: margins(0px, 0px, 0px, 0px);
|
||||
|
||||
style: ivResetZoomStyle;
|
||||
|
||||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
ivResetZoomLabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowFg;
|
||||
style: ivResetZoomStyle;
|
||||
}
|
||||
ivResetZoomInnerPadding: 20px;
|
||||
ivBackIconDisabled: icon {{ "box_button_back", menuIconFg }};
|
||||
ivForwardIcon: icon {{ "box_button_back-flip_horizontal", menuIconColor }};
|
||||
ivForward: IconButton(ivBack) {
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "iv/iv_controller.h"
|
||||
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/qt/qt_key_modifiers.h"
|
||||
#include "base/invoke_queued.h"
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "base/qthelp_url.h"
|
||||
@@ -18,11 +19,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
#include "ui/widgets/rp_window.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/basic_click_handlers.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/webview_helpers.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "webview/webview_data_stream_memory.h"
|
||||
@@ -50,7 +54,185 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Iv {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QByteArray ComputeStyles() {
|
||||
constexpr auto kZoomStep = int(10);
|
||||
constexpr auto kZoomSmallStep = int(5);
|
||||
constexpr auto kZoomTinyStep = int(1);
|
||||
constexpr auto kDefaultZoom = int(100);
|
||||
|
||||
class ItemZoom final
|
||||
: public Ui::Menu::Action
|
||||
, public Ui::AbstractTooltipShower {
|
||||
public:
|
||||
ItemZoom(
|
||||
not_null<RpWidget*> parent,
|
||||
const not_null<Delegate*> delegate,
|
||||
const style::Menu &st)
|
||||
: Ui::Menu::Action(
|
||||
parent,
|
||||
st,
|
||||
Ui::CreateChild<QAction>(parent),
|
||||
nullptr,
|
||||
nullptr)
|
||||
, _delegate(delegate)
|
||||
, _st(st) {
|
||||
init();
|
||||
}
|
||||
|
||||
void init() {
|
||||
enableMouseSelecting();
|
||||
|
||||
AbstractButton::setDisabled(true);
|
||||
|
||||
class SmallButton final : public Ui::IconButton {
|
||||
public:
|
||||
SmallButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
QChar c,
|
||||
float64 skip,
|
||||
const style::color &color)
|
||||
: Ui::IconButton(parent, st::ivPlusMinusZoom)
|
||||
, _color(color)
|
||||
, _skip(style::ConvertFloatScale(skip))
|
||||
, _c(c) {
|
||||
}
|
||||
|
||||
void paintEvent(QPaintEvent *event) override {
|
||||
auto p = Painter(this);
|
||||
Ui::RippleButton::paintRipple(
|
||||
p,
|
||||
st::ivPlusMinusZoom.rippleAreaPosition);
|
||||
p.setPen(_color);
|
||||
p.setFont(st::normalFont);
|
||||
p.drawText(
|
||||
QRectF(rect()).translated(0, _skip),
|
||||
_c,
|
||||
style::al_center);
|
||||
}
|
||||
|
||||
private:
|
||||
const style::color _color;
|
||||
const float64 _skip;
|
||||
const QChar _c;
|
||||
|
||||
};
|
||||
|
||||
const auto processTooltip = [=, this](not_null<Ui::RpWidget*> w) {
|
||||
w->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::Enter) {
|
||||
Ui::Tooltip::Show(1000, this);
|
||||
} else if (e->type() == QEvent::Leave) {
|
||||
Ui::Tooltip::Hide();
|
||||
}
|
||||
}, w->lifetime());
|
||||
};
|
||||
|
||||
const auto reset = Ui::CreateChild<Ui::RoundButton>(
|
||||
this,
|
||||
rpl::single<QString>(QString()),
|
||||
st::ivResetZoom);
|
||||
processTooltip(reset);
|
||||
const auto resetLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
reset,
|
||||
tr::lng_background_reset_default(),
|
||||
st::ivResetZoomLabel);
|
||||
resetLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
reset->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
reset->setClickedCallback([this] {
|
||||
_delegate->ivSetZoom(kDefaultZoom);
|
||||
});
|
||||
reset->show();
|
||||
const auto plus = Ui::CreateChild<SmallButton>(
|
||||
this,
|
||||
'+',
|
||||
0,
|
||||
_st.itemFg);
|
||||
processTooltip(plus);
|
||||
const auto step = [] {
|
||||
return base::IsAltPressed()
|
||||
? kZoomTinyStep
|
||||
: base::IsCtrlPressed()
|
||||
? kZoomSmallStep
|
||||
: kZoomStep;
|
||||
};
|
||||
plus->setClickedCallback([this, step] {
|
||||
_delegate->ivSetZoom(_delegate->ivZoom() + step());
|
||||
});
|
||||
plus->show();
|
||||
const auto minus = Ui::CreateChild<SmallButton>(
|
||||
this,
|
||||
QChar(0x2013),
|
||||
-1,
|
||||
_st.itemFg);
|
||||
processTooltip(minus);
|
||||
minus->setClickedCallback([this, step] {
|
||||
_delegate->ivSetZoom(_delegate->ivZoom() - step());
|
||||
});
|
||||
minus->show();
|
||||
|
||||
_delegate->ivZoomValue(
|
||||
) | rpl::start_with_next([this](int value) {
|
||||
_text.setText(_st.itemStyle, QString::number(value) + '%');
|
||||
update();
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
sizeValue(),
|
||||
reset->sizeValue()
|
||||
) | rpl::start_with_next([=, this](const QSize &size, const QSize &) {
|
||||
reset->setFullWidth(0
|
||||
+ resetLabel->width()
|
||||
+ st::ivResetZoomInnerPadding);
|
||||
resetLabel->moveToLeft(
|
||||
(reset->width() - resetLabel->width()) / 2,
|
||||
(reset->height() - resetLabel->height()) / 2);
|
||||
reset->moveToRight(
|
||||
_st.itemPadding.right(),
|
||||
(size.height() - reset->height()) / 2);
|
||||
plus->moveToRight(
|
||||
_st.itemPadding.right() + reset->width(),
|
||||
(size.height() - plus->height()) / 2);
|
||||
minus->moveToRight(
|
||||
_st.itemPadding.right() + plus->width() + reset->width(),
|
||||
(size.height() - minus->height()) / 2);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void paintEvent(QPaintEvent *event) override {
|
||||
auto p = QPainter(this);
|
||||
p.setPen(_st.itemFg);
|
||||
_text.draw(p, {
|
||||
.position = QPoint(
|
||||
_st.itemIconPosition.x(),
|
||||
(height() - _text.minHeight()) / 2),
|
||||
.outerWidth = width(),
|
||||
.availableWidth = width(),
|
||||
});
|
||||
}
|
||||
|
||||
QString tooltipText() const override {
|
||||
#ifdef Q_OS_MAC
|
||||
return tr::lng_iv_zoom_tooltip_cmd(tr::now);
|
||||
#else
|
||||
return tr::lng_iv_zoom_tooltip_ctrl(tr::now);
|
||||
#endif
|
||||
}
|
||||
|
||||
QPoint tooltipPos() const override {
|
||||
return QCursor::pos();
|
||||
}
|
||||
|
||||
bool tooltipWindowActive() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<Delegate*> _delegate;
|
||||
const style::Menu &_st;
|
||||
Ui::Text::String _text;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QByteArray ComputeStyles(int zoom) {
|
||||
static const auto map = base::flat_map<QByteArray, const style::color*>{
|
||||
{ "shadow-fg", &st::shadowFg },
|
||||
{ "scroll-bg", &st::scrollBg },
|
||||
@@ -85,7 +267,7 @@ namespace {
|
||||
static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
|
||||
{ "iv-join-channel", tr::lng_iv_join_channel },
|
||||
};
|
||||
return Ui::ComputeStyles(map, phrases)
|
||||
return Ui::ComputeStyles(map, phrases, zoom)
|
||||
+ ';'
|
||||
+ Ui::ComputeSemiTransparentOverStyle(
|
||||
"light-button-bg-over",
|
||||
@@ -93,7 +275,7 @@ namespace {
|
||||
st::windowBg);
|
||||
}
|
||||
|
||||
[[nodiscard]] QByteArray WrapPage(const Prepared &page) {
|
||||
[[nodiscard]] QByteArray WrapPage(const Prepared &page, int zoom) {
|
||||
#ifdef Q_OS_MAC
|
||||
const auto classAttribute = ""_q;
|
||||
#else // Q_OS_MAC
|
||||
@@ -110,7 +292,7 @@ namespace {
|
||||
<html)"_q
|
||||
+ classAttribute
|
||||
+ R"( style=")"
|
||||
+ Ui::EscapeForAttribute(ComputeStyles())
|
||||
+ Ui::EscapeForAttribute(ComputeStyles(zoom))
|
||||
+ R"(">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -206,7 +388,8 @@ Controller::Controller(
|
||||
Fn<ShareBoxResult(ShareBoxDescriptor)> showShareBox)
|
||||
: _delegate(delegate)
|
||||
, _updateStyles([=] {
|
||||
const auto str = Ui::EscapeForScriptString(ComputeStyles());
|
||||
const auto zoom = _delegate->ivZoom();
|
||||
const auto str = Ui::EscapeForScriptString(ComputeStyles(zoom));
|
||||
if (_webview) {
|
||||
_webview->eval("IV.updateStyles('" + str + "');");
|
||||
}
|
||||
@@ -484,6 +667,16 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
|
||||
if (event->key() == Qt::Key_Escape) {
|
||||
escape();
|
||||
}
|
||||
if (event->modifiers() & Qt::ControlModifier) {
|
||||
if (event->key() == Qt::Key_Plus
|
||||
|| event->key() == Qt::Key_Equal) {
|
||||
_delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep);
|
||||
} else if (event->key() == Qt::Key_Minus) {
|
||||
_delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep);
|
||||
} else if (event->key() == Qt::Key_0) {
|
||||
_delegate->ivSetZoom(kDefaultZoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, window->lifetime());
|
||||
|
||||
@@ -595,7 +788,8 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
|
||||
|
||||
rpl::merge(
|
||||
Lang::Updated(),
|
||||
style::PaletteChanged()
|
||||
style::PaletteChanged(),
|
||||
_delegate->ivZoomValue() | rpl::to_empty
|
||||
) | rpl::start_with_next([=] {
|
||||
_updateStyles.call();
|
||||
}, _webview->lifetime());
|
||||
@@ -611,7 +805,8 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
|
||||
return Webview::DataResult::Failed;
|
||||
}
|
||||
return finishWith(
|
||||
WrapPage(_pages[index]), "text/html; charset=utf-8");
|
||||
WrapPage(_pages[index], _delegate->ivZoom()),
|
||||
"text/html; charset=utf-8");
|
||||
} else if (id.starts_with("page") && id.ends_with(".json")) {
|
||||
auto index = 0;
|
||||
const auto result = std::from_chars(
|
||||
@@ -897,6 +1092,10 @@ void Controller::showMenu() {
|
||||
showShareMenu();
|
||||
}, &st::menuIconShare);
|
||||
|
||||
_menu->addSeparator();
|
||||
_menu->addAction(
|
||||
base::make_unique_q<ItemZoom>(_menu, _delegate, _menu->menu()->st()));
|
||||
|
||||
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
|
||||
_menu->popup(_window->body()->mapToGlobal(
|
||||
QPoint(_window->body()->width(), 0) + st::ivMenuPosition));
|
||||
|
||||
@@ -18,6 +18,10 @@ public:
|
||||
virtual void ivSetLastSourceWindow(not_null<QWidget*> window) = 0;
|
||||
[[nodiscard]] virtual QRect ivGeometry() const = 0;
|
||||
virtual void ivSaveGeometry(not_null<Ui::RpWindow*> window) = 0;
|
||||
|
||||
[[nodiscard]] virtual int ivZoom() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<int> ivZoomValue() const = 0;
|
||||
virtual void ivSetZoom(int value) = 0;
|
||||
};
|
||||
|
||||
} // namespace Iv
|
||||
|
||||
@@ -117,4 +117,15 @@ void DelegateImpl::ivSaveGeometry(not_null<Ui::RpWindow*> window) {
|
||||
}
|
||||
}
|
||||
|
||||
int DelegateImpl::ivZoom() const {
|
||||
return Core::App().settings().ivZoom();
|
||||
}
|
||||
rpl::producer<int> DelegateImpl::ivZoomValue() const {
|
||||
return Core::App().settings().ivZoomValue();
|
||||
}
|
||||
void DelegateImpl::ivSetZoom(int value) {
|
||||
Core::App().settings().setIvZoom(value);
|
||||
Core::App().saveSettingsDelayed();
|
||||
}
|
||||
|
||||
} // namespace Iv
|
||||
|
||||
@@ -19,6 +19,10 @@ public:
|
||||
[[nodiscard]] QRect ivGeometry() const override;
|
||||
void ivSaveGeometry(not_null<Ui::RpWindow*> window) override;
|
||||
|
||||
[[nodiscard]] int ivZoom() const override;
|
||||
[[nodiscard]] rpl::producer<int> ivZoomValue() const override;
|
||||
void ivSetZoom(int value) override;
|
||||
|
||||
private:
|
||||
QPointer<QWidget> _lastSourceWindow;
|
||||
|
||||
|
||||
@@ -389,11 +389,17 @@ QByteArray Parser::block(const MTPDpageBlockUnsupported &data) {
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockTitle &data) {
|
||||
return tag("h1", { { "class", "title" } }, rich(data.vtext()));
|
||||
return tag("h1", {
|
||||
{ "class", "title" },
|
||||
{ "dir", "auto" },
|
||||
}, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockSubtitle &data) {
|
||||
return tag("h2", { { "class", "subtitle" } }, rich(data.vtext()));
|
||||
return tag("h2", {
|
||||
{ "class", "subtitle" },
|
||||
{ "dir", "auto" },
|
||||
}, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) {
|
||||
@@ -401,23 +407,29 @@ QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) {
|
||||
if (const auto date = data.vpublished_date().v) {
|
||||
inner += " \xE2\x80\xA2 " + tag("time", Date(date));
|
||||
}
|
||||
return tag("address", inner);
|
||||
return tag("address", { { "dir", "auto" } }, inner);
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockHeader &data) {
|
||||
return tag("h3", { { "class", "header" } }, rich(data.vtext()));
|
||||
return tag("h3", {
|
||||
{ "class", "header" },
|
||||
{ "dir", "auto" },
|
||||
}, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockSubheader &data) {
|
||||
return tag("h4", { { "class", "subheader" } }, rich(data.vtext()));
|
||||
return tag("h4", {
|
||||
{ "class", "subheader" },
|
||||
{ "dir", "auto" },
|
||||
}, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockParagraph &data) {
|
||||
return tag("p", rich(data.vtext()));
|
||||
return tag("p", { { "dir", "auto" } }, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockPreformatted &data) {
|
||||
auto list = Attributes();
|
||||
auto list = Attributes{ { "dir", "auto" } };
|
||||
const auto language = utf(data.vlanguage());
|
||||
if (!language.isEmpty()) {
|
||||
list.push_back({ "data-language", language });
|
||||
@@ -428,7 +440,10 @@ QByteArray Parser::block(const MTPDpageBlockPreformatted &data) {
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockFooter &data) {
|
||||
return tag("footer", { { "class", "footer" } }, rich(data.vtext()));
|
||||
return tag("footer", {
|
||||
{ "class", "footer" },
|
||||
{ "dir", "auto" },
|
||||
}, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockDivider &data) {
|
||||
@@ -447,19 +462,21 @@ QByteArray Parser::block(const MTPDpageBlockBlockquote &data) {
|
||||
const auto caption = rich(data.vcaption());
|
||||
const auto cite = caption.isEmpty()
|
||||
? QByteArray()
|
||||
: tag("cite", caption);
|
||||
return tag("blockquote", rich(data.vtext()) + cite);
|
||||
: tag("cite", { { "dir", "auto" } }, caption);
|
||||
return tag("blockquote", {
|
||||
{ "dir", "auto" }
|
||||
}, rich(data.vtext()) + cite);
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockPullquote &data) {
|
||||
const auto caption = rich(data.vcaption());
|
||||
const auto cite = caption.isEmpty()
|
||||
? QByteArray()
|
||||
: tag("cite", caption);
|
||||
return tag(
|
||||
"div",
|
||||
{ { "class", "pullquote" } },
|
||||
rich(data.vtext()) + cite);
|
||||
: tag("cite", { { "dir", "auto" } }, caption);
|
||||
return tag("div", {
|
||||
{ "class", "pullquote" },
|
||||
{ "dir", "auto" },
|
||||
}, rich(data.vtext()) + cite);
|
||||
}
|
||||
|
||||
QByteArray Parser::block(
|
||||
@@ -763,7 +780,10 @@ QByteArray Parser::block(const MTPDpageBlockAudio &data) {
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockKicker &data) {
|
||||
return tag("h5", { { "class", "kicker" } }, rich(data.vtext()));
|
||||
return tag("h5", {
|
||||
{ "class", "kicker" },
|
||||
{ "dir", "auto" },
|
||||
}, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockTable &data) {
|
||||
@@ -780,7 +800,7 @@ QByteArray Parser::block(const MTPDpageBlockTable &data) {
|
||||
}
|
||||
auto title = rich(data.vtitle());
|
||||
if (!title.isEmpty()) {
|
||||
title = tag("caption", title);
|
||||
title = tag("caption", { { "dir", "auto" } }, title);
|
||||
}
|
||||
auto result = tag("table", attibutes, title + list(data.vrows()));
|
||||
result = tag("figure", { { "class", "table" } }, result);
|
||||
@@ -800,7 +820,8 @@ QByteArray Parser::block(const MTPDpageBlockDetails &data) {
|
||||
return tag(
|
||||
"details",
|
||||
attributes,
|
||||
tag("summary", rich(data.vtitle())) + list(data.vblocks()));
|
||||
(tag("summary", { { "dir", "auto" } }, rich(data.vtitle()))
|
||||
+ list(data.vblocks())));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) {
|
||||
@@ -810,7 +831,10 @@ QByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) {
|
||||
}
|
||||
auto title = rich(data.vtitle());
|
||||
if (!title.isEmpty()) {
|
||||
title = tag("h4", { { "class", "related-title" } }, title);
|
||||
title = tag("h4", {
|
||||
{ "class", "related-title" },
|
||||
{ "dir", "auto" },
|
||||
}, title);
|
||||
}
|
||||
return tag("section", { { "class", "related" } }, title + result);
|
||||
}
|
||||
@@ -899,7 +923,7 @@ QByteArray Parser::block(const MTPDpageTableCell &data) {
|
||||
} else {
|
||||
style += "vertical-align:top;";
|
||||
}
|
||||
auto attributes = Attributes{ { "style", style } };
|
||||
auto attributes = Attributes{ { "style", style }, { "dir", "auto" } };
|
||||
if (const auto cs = data.vcolspan()) {
|
||||
attributes.push_back({ "colspan", Number(cs->v) });
|
||||
}
|
||||
@@ -910,7 +934,7 @@ QByteArray Parser::block(const MTPDpageTableCell &data) {
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageListItemText &data) {
|
||||
return tag("li", rich(data.vtext()));
|
||||
return tag("li", { { "dir", "auto" } }, rich(data.vtext()));
|
||||
}
|
||||
|
||||
QByteArray Parser::block(const MTPDpageListItemBlocks &data) {
|
||||
@@ -920,7 +944,7 @@ QByteArray Parser::block(const MTPDpageListItemBlocks &data) {
|
||||
QByteArray Parser::block(const MTPDpageListOrderedItemText &data) {
|
||||
return tag(
|
||||
"li",
|
||||
{ { "value", utf(data.vnum()) } },
|
||||
{ { "value", utf(data.vnum()) }, { "dir", "auto" } },
|
||||
rich(data.vtext()));
|
||||
}
|
||||
|
||||
@@ -1073,11 +1097,11 @@ QByteArray Parser::caption(const MTPPageCaption &caption) {
|
||||
auto text = rich(caption.data().vtext());
|
||||
const auto credit = rich(caption.data().vcredit());
|
||||
if (!credit.isEmpty()) {
|
||||
text += tag("cite", credit);
|
||||
text += tag("cite", { { "dir", "auto" } }, credit);
|
||||
} else if (text.isEmpty()) {
|
||||
return QByteArray();
|
||||
}
|
||||
return tag("figcaption", text);
|
||||
return tag("figcaption", { { "dir", "auto" } }, text);
|
||||
}
|
||||
|
||||
Photo Parser::parse(const MTPPhoto &photo) {
|
||||
|
||||
@@ -88,13 +88,15 @@ public:
|
||||
void start(
|
||||
Webrtc::DeviceResolvedId id,
|
||||
Fn<void(Update)> updated,
|
||||
Fn<void()> error);
|
||||
Fn<void()> error,
|
||||
Fn<void(Chunk)> externalProcessing);
|
||||
void stop(Fn<void(Result&&)> callback = nullptr);
|
||||
void pause(bool value, Fn<void(Result&&)> callback);
|
||||
|
||||
private:
|
||||
void process();
|
||||
|
||||
bool initializeFFmpeg();
|
||||
[[nodiscard]] bool processFrame(int32 offset, int32 framesize);
|
||||
void fail();
|
||||
|
||||
@@ -104,6 +106,7 @@ private:
|
||||
// Returns number of packets written or -1 on error
|
||||
[[nodiscard]] int writePackets();
|
||||
|
||||
Fn<void(Chunk)> _externalProcessing;
|
||||
Fn<void(Update)> _updated;
|
||||
Fn<void()> _error;
|
||||
|
||||
@@ -131,7 +134,7 @@ Instance::Instance() : _inner(std::make_unique<Inner>(&_thread)) {
|
||||
_thread.start();
|
||||
}
|
||||
|
||||
void Instance::start() {
|
||||
void Instance::start(Fn<void(Chunk)> externalProcessing) {
|
||||
_updates.fire_done();
|
||||
const auto id = Audio::Current().captureDeviceId();
|
||||
InvokeQueued(_inner.get(), [=] {
|
||||
@@ -141,9 +144,9 @@ void Instance::start() {
|
||||
});
|
||||
}, [=] {
|
||||
crl::on_main(this, [=] {
|
||||
_updates.fire_error({});
|
||||
_updates.fire_error(Error::Other);
|
||||
});
|
||||
});
|
||||
}, externalProcessing);
|
||||
crl::on_main(this, [=] {
|
||||
_started = true;
|
||||
});
|
||||
@@ -167,13 +170,15 @@ void Instance::stop(Fn<void(Result&&)> callback) {
|
||||
}
|
||||
|
||||
void Instance::pause(bool value, Fn<void(Result&&)> callback) {
|
||||
Expects(callback != nullptr || !value);
|
||||
InvokeQueued(_inner.get(), [=] {
|
||||
_inner->pause(value, [=](Result &&result) {
|
||||
crl::on_main([=, result = std::move(result)]() mutable {
|
||||
callback(std::move(result));
|
||||
});
|
||||
});
|
||||
auto done = callback
|
||||
? [=](Result &&result) {
|
||||
crl::on_main([=, result = std::move(result)]() mutable {
|
||||
callback(std::move(result));
|
||||
});
|
||||
}
|
||||
: std::move(callback);
|
||||
_inner->pause(value, std::move(done));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -304,7 +309,9 @@ void Instance::Inner::fail() {
|
||||
void Instance::Inner::start(
|
||||
Webrtc::DeviceResolvedId id,
|
||||
Fn<void(Update)> updated,
|
||||
Fn<void()> error) {
|
||||
Fn<void()> error,
|
||||
Fn<void(Chunk)> externalProcessing) {
|
||||
_externalProcessing = std::move(externalProcessing);
|
||||
_updated = std::move(updated);
|
||||
_error = std::move(error);
|
||||
if (_paused) {
|
||||
@@ -329,8 +336,19 @@ void Instance::Inner::start(
|
||||
d->device = nullptr;
|
||||
fail();
|
||||
return;
|
||||
} else if (!_externalProcessing) {
|
||||
if (!initializeFFmpeg()) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
}
|
||||
_timer.callEach(50);
|
||||
_captured.clear();
|
||||
_captured.reserve(kCaptureBufferSlice);
|
||||
DEBUG_LOG(("Audio Capture: started!"));
|
||||
}
|
||||
|
||||
bool Instance::Inner::initializeFFmpeg() {
|
||||
// Create encoding context
|
||||
|
||||
d->ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize);
|
||||
@@ -347,14 +365,12 @@ void Instance::Inner::start(
|
||||
}
|
||||
if (!fmt) {
|
||||
LOG(("Audio Error: Unable to find opus AVOutputFormat for capture"));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((res = avformat_alloc_output_context2(&d->fmtContext, (AVOutputFormat*)fmt, 0, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to avformat_alloc_output_context2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
d->fmtContext->pb = d->ioContext;
|
||||
d->fmtContext->flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
@@ -364,21 +380,18 @@ void Instance::Inner::start(
|
||||
d->codec = avcodec_find_encoder(fmt->audio_codec);
|
||||
if (!d->codec) {
|
||||
LOG(("Audio Error: Unable to avcodec_find_encoder for capture"));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
d->stream = avformat_new_stream(d->fmtContext, d->codec);
|
||||
if (!d->stream) {
|
||||
LOG(("Audio Error: Unable to avformat_new_stream for capture"));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
d->stream->id = d->fmtContext->nb_streams - 1;
|
||||
d->codecContext = avcodec_alloc_context3(d->codec);
|
||||
if (!d->codecContext) {
|
||||
LOG(("Audio Error: Unable to avcodec_alloc_context3 for capture"));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
av_opt_set_int(d->codecContext, "refcounted_frames", 1, 0);
|
||||
@@ -401,8 +414,7 @@ void Instance::Inner::start(
|
||||
// Open audio stream
|
||||
if ((res = avcodec_open2(d->codecContext, d->codec, nullptr)) < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_open2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Alloc source samples
|
||||
@@ -443,39 +455,27 @@ void Instance::Inner::start(
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
if (res < 0 || !d->swrContext) {
|
||||
LOG(("Audio Error: Unable to swr_alloc_set_opts2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
} else if ((res = swr_init(d->swrContext)) < 0) {
|
||||
LOG(("Audio Error: Unable to swr_init for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
d->maxDstSamples = d->srcSamples;
|
||||
if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
d->dstSamplesSize = av_samples_get_buffer_size(0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);
|
||||
|
||||
if ((res = avcodec_parameters_from_context(d->stream->codecpar, d->codecContext)) < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_parameters_from_context for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write file header
|
||||
if ((res = avformat_write_header(d->fmtContext, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to avformat_write_header for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
fail();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
_timer.callEach(50);
|
||||
_captured.clear();
|
||||
_captured.reserve(kCaptureBufferSlice);
|
||||
DEBUG_LOG(("Audio Capture: started!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
void Instance::Inner::pause(bool value, Fn<void(Result&&)> callback) {
|
||||
@@ -483,11 +483,16 @@ void Instance::Inner::pause(bool value, Fn<void(Result&&)> callback) {
|
||||
if (!_paused) {
|
||||
return;
|
||||
}
|
||||
callback({
|
||||
d->fullSamples ? d->data : QByteArray(),
|
||||
d->fullSamples ? CollectWaveform(d->waveform) : VoiceWaveform(),
|
||||
qint32(d->fullSamples),
|
||||
});
|
||||
if (callback) {
|
||||
callback({
|
||||
.bytes = d->fullSamples ? d->data : QByteArray(),
|
||||
.waveform = (d->fullSamples
|
||||
? CollectWaveform(d->waveform)
|
||||
: VoiceWaveform()),
|
||||
.duration = ((d->fullSamples * crl::time(1000))
|
||||
/ int64(kCaptureFrequency)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Inner::stop(Fn<void(Result&&)> callback) {
|
||||
@@ -559,7 +564,7 @@ void Instance::Inner::stop(Fn<void(Result&&)> callback) {
|
||||
_captured = QByteArray();
|
||||
|
||||
// Finish stream
|
||||
if (needResult && hadDevice) {
|
||||
if (needResult && hadDevice && d->fmtContext) {
|
||||
av_write_trailer(d->fmtContext);
|
||||
}
|
||||
|
||||
@@ -622,7 +627,11 @@ void Instance::Inner::stop(Fn<void(Result&&)> callback) {
|
||||
}
|
||||
|
||||
if (needResult) {
|
||||
callback({ result, waveform, samples });
|
||||
callback({
|
||||
.bytes = result,
|
||||
.waveform = waveform,
|
||||
.duration = (samples * crl::time(1000)) / kCaptureFrequency,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,6 +667,13 @@ void Instance::Inner::process() {
|
||||
if (ErrorHappened(d->device)) {
|
||||
fail();
|
||||
return;
|
||||
} else if (_externalProcessing) {
|
||||
_externalProcessing({
|
||||
.finished = crl::now(),
|
||||
.samples = base::take(_captured),
|
||||
.frequency = kCaptureFrequency,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Count new recording level and update view
|
||||
|
||||
@@ -7,16 +7,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
struct AVFrame;
|
||||
|
||||
namespace Media {
|
||||
namespace Capture {
|
||||
|
||||
struct Update {
|
||||
int samples = 0;
|
||||
ushort level = 0;
|
||||
|
||||
bool finished = false;
|
||||
};
|
||||
|
||||
enum class Error : uchar {
|
||||
Other,
|
||||
AudioInit,
|
||||
VideoInit,
|
||||
AudioTimeout,
|
||||
VideoTimeout,
|
||||
Encoding,
|
||||
};
|
||||
|
||||
struct Chunk {
|
||||
crl::time finished = 0;
|
||||
QByteArray samples;
|
||||
int frequency = 0;
|
||||
};
|
||||
|
||||
struct Result;
|
||||
@@ -34,7 +50,7 @@ public:
|
||||
return _available;
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<Update, rpl::empty_error> updated() const {
|
||||
[[nodiscard]] rpl::producer<Update, Error> updated() const {
|
||||
return _updates.events();
|
||||
}
|
||||
|
||||
@@ -45,9 +61,9 @@ public:
|
||||
return _started.changes();
|
||||
}
|
||||
|
||||
void start();
|
||||
void start(Fn<void(Chunk)> externalProcessing = nullptr);
|
||||
void stop(Fn<void(Result&&)> callback = nullptr);
|
||||
void pause(bool value, Fn<void(Result&&)> callback);
|
||||
void pause(bool value, Fn<void(Result&&)> callback = nullptr);
|
||||
|
||||
private:
|
||||
class Inner;
|
||||
@@ -55,7 +71,7 @@ private:
|
||||
|
||||
bool _available = false;
|
||||
rpl::variable<bool> _started = false;
|
||||
rpl::event_stream<Update, rpl::empty_error> _updates;
|
||||
rpl::event_stream<Update, Error> _updates;
|
||||
QThread _thread;
|
||||
std::unique_ptr<Inner> _inner;
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace Media::Capture {
|
||||
struct Result {
|
||||
QByteArray bytes;
|
||||
VoiceWaveform waveform;
|
||||
int samples = 0;
|
||||
crl::time duration;
|
||||
bool video = false;
|
||||
};
|
||||
|
||||
} // namespace Media::Capture
|
||||
|
||||
@@ -869,7 +869,7 @@ void Instance::pause(AudioMsgId::Type type) {
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::stop(AudioMsgId::Type type) {
|
||||
void Instance::stop(AudioMsgId::Type type, bool asFinished) {
|
||||
if (const auto data = getData(type)) {
|
||||
if (data->streamed) {
|
||||
clearStreamed(data);
|
||||
@@ -877,6 +877,9 @@ void Instance::stop(AudioMsgId::Type type) {
|
||||
data->resumeOnCallEnd = false;
|
||||
_playerStopped.fire_copy({type});
|
||||
}
|
||||
if (asFinished) {
|
||||
_tracksFinished.fire_copy(type);
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::stopAndClear(not_null<Data*> data) {
|
||||
@@ -1200,6 +1203,21 @@ Streaming::Instance *Instance::roundVideoStreamed(HistoryItem *item) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Streaming::Instance *Instance::roundVideoPreview(
|
||||
not_null<DocumentData*> document) const {
|
||||
if (const auto data = getData(AudioMsgId::Type::Voice)) {
|
||||
if (const auto streamed = data->streamed.get()) {
|
||||
if (streamed->id.audio() == document) {
|
||||
const auto player = &streamed->instance.player();
|
||||
if (player->ready() && !player->videoSize().isEmpty()) {
|
||||
return &streamed->instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
View::PlaybackProgress *Instance::roundVideoPlayback(
|
||||
HistoryItem *item) const {
|
||||
return roundVideoStreamed(item)
|
||||
|
||||
@@ -72,7 +72,7 @@ public:
|
||||
|
||||
void play(AudioMsgId::Type type);
|
||||
void pause(AudioMsgId::Type type);
|
||||
void stop(AudioMsgId::Type type);
|
||||
void stop(AudioMsgId::Type type, bool asFinished = false);
|
||||
void playPause(AudioMsgId::Type type);
|
||||
bool next(AudioMsgId::Type type);
|
||||
bool previous(AudioMsgId::Type type);
|
||||
@@ -109,6 +109,9 @@ public:
|
||||
[[nodiscard]] View::PlaybackProgress *roundVideoPlayback(
|
||||
HistoryItem *item) const;
|
||||
|
||||
[[nodiscard]] Streaming::Instance *roundVideoPreview(
|
||||
not_null<DocumentData*> document) const;
|
||||
|
||||
[[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const {
|
||||
if (const auto data = getData(type)) {
|
||||
return data->current;
|
||||
|
||||
@@ -717,6 +717,8 @@ void Widget::handleSongChange() {
|
||||
0,
|
||||
name.size(),
|
||||
QString()));
|
||||
} else if (document->isVideoMessage()) {
|
||||
textWithEntities.text = tr::lng_media_round(tr::now);
|
||||
} else {
|
||||
textWithEntities.text = tr::lng_media_audio(tr::now);
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ void ReplyArea::sendVoice(VoiceToSend &&data) {
|
||||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
std::move(action));
|
||||
|
||||
_controls->clearListenState();
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 "media/streaming/media_streaming_round_preview.h"
|
||||
|
||||
namespace Media::Streaming {
|
||||
|
||||
RoundPreview::RoundPreview(const QByteArray &bytes, int size)
|
||||
: _bytes(bytes)
|
||||
, _reader(
|
||||
Clip::MakeReader(_bytes, [=](Clip::Notification update) {
|
||||
clipCallback(update);
|
||||
}))
|
||||
, _size(size) {
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> RoundPreview::clone() {
|
||||
Unexpected("RoundPreview::clone.");
|
||||
}
|
||||
|
||||
QImage RoundPreview::image(int size) {
|
||||
if (!_reader || !_reader->started()) {
|
||||
return QImage();
|
||||
}
|
||||
return _reader->current({
|
||||
.frame = QSize(_size, _size),
|
||||
.factor = style::DevicePixelRatio(),
|
||||
.radius = ImageRoundRadius::Ellipse,
|
||||
}, crl::now());
|
||||
}
|
||||
|
||||
void RoundPreview::subscribeToUpdates(Fn<void()> callback) {
|
||||
_repaint = std::move(callback);
|
||||
}
|
||||
|
||||
void RoundPreview::clipCallback(Clip::Notification notification) {
|
||||
switch (notification) {
|
||||
case Clip::Notification::Reinit: {
|
||||
if (_reader->state() == ::Media::Clip::State::Error) {
|
||||
_reader.setBad();
|
||||
} else if (_reader->ready() && !_reader->started()) {
|
||||
_reader->start({
|
||||
.frame = QSize(_size, _size),
|
||||
.factor = style::DevicePixelRatio(),
|
||||
.radius = ImageRoundRadius::Ellipse,
|
||||
});
|
||||
}
|
||||
} break;
|
||||
|
||||
case Clip::Notification::Repaint: break;
|
||||
}
|
||||
|
||||
if (const auto onstack = _repaint) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Media::Streaming
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/dynamic_image.h"
|
||||
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
|
||||
namespace Media::Streaming {
|
||||
|
||||
class RoundPreview final : public Ui::DynamicImage {
|
||||
public:
|
||||
RoundPreview(const QByteArray &bytes, int size);
|
||||
|
||||
std::shared_ptr<DynamicImage> clone() override;
|
||||
|
||||
QImage image(int size) override;
|
||||
void subscribeToUpdates(Fn<void()> callback) override;
|
||||
|
||||
private:
|
||||
void clipCallback(Clip::Notification notification);
|
||||
|
||||
const QByteArray _bytes;
|
||||
Clip::ReaderPointer _reader;
|
||||
Fn<void()> _repaint;
|
||||
int _size = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Streaming
|
||||
@@ -480,6 +480,8 @@ storiesLike: IconButton(storiesAttach) {
|
||||
}
|
||||
storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }};
|
||||
storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }};
|
||||
storiesRecordRound: icon {{ "chat/input_video", storiesComposeGrayIcon }};
|
||||
storiesRecordRoundOver: icon {{ "chat/input_video", storiesComposeGrayIcon }};
|
||||
storiesRemoveSet: IconButton(stickerPanRemoveSet) {
|
||||
icon: icon {{ "simple_close", storiesComposeGrayIcon }};
|
||||
iconOver: icon {{ "simple_close", storiesComposeGrayIcon }};
|
||||
@@ -686,6 +688,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
|
||||
}
|
||||
record: storiesRecordVoice;
|
||||
recordOver: storiesRecordVoiceOver;
|
||||
round: storiesRecordRound;
|
||||
roundOver: storiesRecordRoundOver;
|
||||
sendDisabledFg: storiesComposeGrayText;
|
||||
}
|
||||
attach: storiesAttach;
|
||||
|
||||
@@ -752,14 +752,17 @@ FillMenuResult FillSendMenu(
|
||||
&icons.menuPrice);
|
||||
}
|
||||
|
||||
return showForEffect
|
||||
? AttachSendMenuEffect(
|
||||
if (showForEffect) {
|
||||
return AttachSendMenuEffect(
|
||||
menu,
|
||||
showForEffect,
|
||||
details,
|
||||
action,
|
||||
desiredPositionOverride)
|
||||
: FillMenuResult::Prepared;
|
||||
desiredPositionOverride);
|
||||
}
|
||||
const auto position = desiredPositionOverride.value_or(QCursor::pos());
|
||||
menu->prepareGeometryFor(position);
|
||||
return FillMenuResult::Prepared;
|
||||
}
|
||||
|
||||
void SetupMenuAndShortcuts(
|
||||
|
||||
@@ -108,9 +108,7 @@ passportContactErrorMargin: margins(0px, 0px, 0px, 14px);
|
||||
passportRowPadding: margins(22px, 8px, 25px, 8px);
|
||||
passportRowIconSkip: 10px;
|
||||
passportRowSkip: 2px;
|
||||
passportRowRipple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
passportRowRipple: defaultRippleAnimationBgOver;
|
||||
passportRowReadyIcon: icon {{ "passport_ready", windowActiveTextFg }};
|
||||
passportRowEmptyIcon: icon {{ "passport_empty", menuIconFgOver }};
|
||||
passportRowTitleFg: windowFg;
|
||||
|
||||
@@ -204,7 +204,7 @@ LinuxIntegration::LinuxIntegration()
|
||||
return value->get_uint32() == 1;
|
||||
}
|
||||
return std::nullopt;
|
||||
})
|
||||
}())
|
||||
, _darkModeWatcher(
|
||||
"org.freedesktop.appearance",
|
||||
"color-scheme",
|
||||
|
||||
@@ -367,6 +367,7 @@ bool SkipSoundForCustom() {
|
||||
|
||||
return (UserNotificationState == QUNS_NOT_PRESENT)
|
||||
|| (UserNotificationState == QUNS_PRESENTATION_MODE)
|
||||
|| (FocusAssistBlocks && Core::App().settings().skipToastsInFocus())
|
||||
|| Core::App().screenIsLocked();
|
||||
}
|
||||
|
||||
@@ -387,7 +388,8 @@ bool SkipToastForCustom() {
|
||||
QuerySystemNotificationSettings();
|
||||
|
||||
return (UserNotificationState == QUNS_PRESENTATION_MODE)
|
||||
|| (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN);
|
||||
|| (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN)
|
||||
|| (FocusAssistBlocks && Core::App().settings().skipToastsInFocus());
|
||||
}
|
||||
|
||||
void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
|
||||
|
||||
@@ -226,6 +226,9 @@ Main::Session &PreviewController::session() const {
|
||||
|
||||
[[nodiscard]] QString ExtractUsername(QString text) {
|
||||
text = text.trimmed();
|
||||
if (text.startsWith(QChar('@'))) {
|
||||
return text.mid(1);
|
||||
}
|
||||
static const auto expression = QRegularExpression(
|
||||
"^(https://)?([a-zA-Z0-9\\.]+/)?([a-zA-Z0-9_\\.]+)");
|
||||
const auto match = expression.match(text);
|
||||
|
||||
@@ -1200,6 +1200,7 @@ void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) {
|
||||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
std::move(action));
|
||||
|
||||
_composeControls->cancelReplyMessage();
|
||||
|
||||
@@ -679,7 +679,7 @@ object_ptr<Ui::GenericBox> ChooseCameraDeviceBox(
|
||||
const style::Radio *radioSt) {
|
||||
return Box(
|
||||
ChooseMediaDeviceBox,
|
||||
tr::lng_settings_call_device_default(),
|
||||
tr::lng_settings_call_camera(),
|
||||
Core::App().mediaDevices().devicesValue(DeviceType::Camera),
|
||||
std::move(currentId),
|
||||
std::move(chosen),
|
||||
|
||||
@@ -52,6 +52,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "statistics/widgets/chart_header_widget.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/effects/premium_stars_colored.h"
|
||||
@@ -772,9 +774,15 @@ void ReceiptCreditsBox(
|
||||
GenericEntryPhoto(content, callback, stUser.photoSize)));
|
||||
AddViewMediaHandler(thumb->entity(), controller, e);
|
||||
} else if (peer && !e.gift) {
|
||||
content->add(object_ptr<Ui::CenterWrap<>>(
|
||||
content,
|
||||
object_ptr<Ui::UserpicButton>(content, peer, stUser)));
|
||||
if (e.subscriptionUntil.isNull() && s.until.isNull()) {
|
||||
content->add(object_ptr<Ui::CenterWrap<>>(
|
||||
content,
|
||||
object_ptr<Ui::UserpicButton>(content, peer, stUser)));
|
||||
} else {
|
||||
content->add(object_ptr<Ui::CenterWrap<>>(
|
||||
content,
|
||||
SubscriptionUserpic(content, peer, stUser.photoSize)));
|
||||
}
|
||||
} else if (e.gift || isPrize) {
|
||||
struct State final {
|
||||
DocumentData *sticker = nullptr;
|
||||
@@ -1541,6 +1549,35 @@ object_ptr<Ui::RpWidget> PaidMediaThumbnail(
|
||||
photoSize);
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> SubscriptionUserpic(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<PeerData*> peer,
|
||||
int photoSize) {
|
||||
auto widget = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto raw = widget.data();
|
||||
widget->resize(photoSize, photoSize);
|
||||
const auto userpicMedia = Ui::MakeUserpicThumbnail(peer, false);
|
||||
userpicMedia->subscribeToUpdates([=] { raw->update(); });
|
||||
const auto creditsIconSize = photoSize / 3;
|
||||
const auto creditsIconCallback =
|
||||
Ui::PaintOutlinedColoredCreditsIconCallback(
|
||||
creditsIconSize,
|
||||
1.5);
|
||||
widget->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
p.fillRect(Rect(Size(photoSize)), Qt::transparent);
|
||||
auto image = userpicMedia->image(photoSize);
|
||||
{
|
||||
auto q = QPainter(&image);
|
||||
q.translate(photoSize, photoSize);
|
||||
q.translate(-creditsIconSize, -creditsIconSize);
|
||||
creditsIconCallback(q);
|
||||
}
|
||||
p.drawImage(0, 0, image);
|
||||
}, widget->lifetime());
|
||||
return widget;
|
||||
}
|
||||
|
||||
void SmallBalanceBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
|
||||
@@ -126,6 +126,11 @@ void ShowRefundInfoBox(
|
||||
int totalCount,
|
||||
int photoSize);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> SubscriptionUserpic(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<PeerData*> peer,
|
||||
int photoSize);
|
||||
|
||||
struct SmallBalanceBot {
|
||||
UserId botId = 0;
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/new_badges.h"
|
||||
@@ -144,6 +145,20 @@ Cover::Cover(
|
||||
|
||||
_phone->setSelectable(true);
|
||||
_phone->setContextCopyText(tr::lng_profile_copy_phone(tr::now));
|
||||
const auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) {
|
||||
if (request.selection.empty()) {
|
||||
const auto c = [=] {
|
||||
auto phone = rpl::variable<TextWithEntities>(
|
||||
Info::Profile::PhoneValue(_user)).current().text;
|
||||
phone.replace(' ', QString()).replace('-', QString());
|
||||
TextUtilities::SetClipboardText({ phone });
|
||||
};
|
||||
request.menu->addAction(tr::lng_profile_copy_phone(tr::now), c);
|
||||
} else {
|
||||
_phone->fillContextMenu(request);
|
||||
}
|
||||
};
|
||||
_phone->setContextMenuHook(hook);
|
||||
|
||||
initViewers();
|
||||
setupChildGeometry();
|
||||
|
||||
@@ -864,6 +864,27 @@ NotifyViewCheckboxes SetupNotifyViewOptions(
|
||||
void SetupAdvancedNotifications(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
if (Platform::IsWindows()) {
|
||||
const auto skipInFocus = container->add(object_ptr<Button>(
|
||||
container,
|
||||
tr::lng_settings_skip_in_focus(),
|
||||
st::settingsButtonNoIcon
|
||||
))->toggleOn(rpl::single(Core::App().settings().skipToastsInFocus()));
|
||||
|
||||
skipInFocus->toggledChanges(
|
||||
) | rpl::filter([](bool checked) {
|
||||
return (checked != Core::App().settings().skipToastsInFocus());
|
||||
}) | rpl::start_with_next([=](bool checked) {
|
||||
Core::App().settings().setSkipToastsInFocus(checked);
|
||||
Core::App().saveSettingsDelayed();
|
||||
if (checked && Platform::Notifications::SkipToastForCustom()) {
|
||||
using Change = Window::Notifications::ChangeType;
|
||||
Core::App().notifications().notifySettingsChanged(
|
||||
Change::DesktopEnabled);
|
||||
}
|
||||
}, skipInFocus->lifetime());
|
||||
}
|
||||
|
||||
Ui::AddSkip(container, st::settingsCheckboxesSkip);
|
||||
Ui::AddDivider(container);
|
||||
Ui::AddSkip(container, st::settingsCheckboxesSkip);
|
||||
|
||||
@@ -124,7 +124,8 @@ Uploader::Entry::Entry(
|
||||
: file->thumbId) {
|
||||
if (file->type == SendMediaType::File
|
||||
|| file->type == SendMediaType::ThemeFile
|
||||
|| file->type == SendMediaType::Audio) {
|
||||
|| file->type == SendMediaType::Audio
|
||||
|| file->type == SendMediaType::Round) {
|
||||
setDocSize(file->filesize);
|
||||
}
|
||||
}
|
||||
@@ -294,7 +295,8 @@ void Uploader::upload(
|
||||
file->partssize);
|
||||
} else if (file->type == SendMediaType::File
|
||||
|| file->type == SendMediaType::ThemeFile
|
||||
|| file->type == SendMediaType::Audio) {
|
||||
|| file->type == SendMediaType::Audio
|
||||
|| file->type == SendMediaType::Round) {
|
||||
const auto document = file->thumb.isNull()
|
||||
? session().data().processDocument(file->document)
|
||||
: session().data().processDocument(
|
||||
@@ -356,7 +358,8 @@ void Uploader::notifyFailed(const Entry &entry) {
|
||||
_photoFailed.fire_copy(entry.itemId);
|
||||
} else if (type == SendMediaType::File
|
||||
|| type == SendMediaType::ThemeFile
|
||||
|| type == SendMediaType::Audio) {
|
||||
|| type == SendMediaType::Audio
|
||||
|| type == SendMediaType::Round) {
|
||||
const auto document = session().data().document(entry.file->id);
|
||||
if (document->uploading()) {
|
||||
document->status = FileUploadFailed;
|
||||
@@ -385,7 +388,8 @@ QByteArray Uploader::readDocPart(not_null<Entry*> entry) {
|
||||
const auto checked = [&](QByteArray result) {
|
||||
if ((entry->file->type == SendMediaType::File
|
||||
|| entry->file->type == SendMediaType::ThemeFile
|
||||
|| entry->file->type == SendMediaType::Audio)
|
||||
|| entry->file->type == SendMediaType::Audio
|
||||
|| entry->file->type == SendMediaType::Round)
|
||||
&& entry->docSize <= kUseBigFilesFrom) {
|
||||
entry->md5Hash.feed(result.data(), result.size());
|
||||
}
|
||||
@@ -756,7 +760,8 @@ void Uploader::partLoaded(const MTPBool &result, mtpRequestId requestId) {
|
||||
_photoProgress.fire_copy(itemId);
|
||||
} else if (entry.file->type == SendMediaType::File
|
||||
|| entry.file->type == SendMediaType::ThemeFile
|
||||
|| entry.file->type == SendMediaType::Audio) {
|
||||
|| entry.file->type == SendMediaType::Audio
|
||||
|| entry.file->type == SendMediaType::Round) {
|
||||
const auto document = session().data().document(entry.file->id);
|
||||
if (document->uploading()) {
|
||||
document->uploadingData->offset = std::min(
|
||||
@@ -856,7 +861,8 @@ void Uploader::finishFront() {
|
||||
});
|
||||
} else if (entry.file->type == SendMediaType::File
|
||||
|| entry.file->type == SendMediaType::ThemeFile
|
||||
|| entry.file->type == SendMediaType::Audio) {
|
||||
|| entry.file->type == SendMediaType::Audio
|
||||
|| entry.file->type == SendMediaType::Round) {
|
||||
QByteArray docMd5(32, Qt::Uninitialized);
|
||||
hashMd5Hex(entry.md5Hash.result(), docMd5.data());
|
||||
|
||||
|
||||
@@ -498,6 +498,7 @@ FileLoadTask::FileLoadTask(
|
||||
const QByteArray &voice,
|
||||
crl::time duration,
|
||||
const VoiceWaveform &waveform,
|
||||
bool video,
|
||||
const FileLoadTo &to,
|
||||
const TextWithTags &caption)
|
||||
: _id(base::RandomValue<uint64>())
|
||||
@@ -507,7 +508,7 @@ FileLoadTask::FileLoadTask(
|
||||
, _content(voice)
|
||||
, _duration(duration)
|
||||
, _waveform(waveform)
|
||||
, _type(SendMediaType::Audio)
|
||||
, _type(video ? SendMediaType::Round : SendMediaType::Audio)
|
||||
, _caption(caption) {
|
||||
}
|
||||
|
||||
@@ -696,6 +697,7 @@ void FileLoadTask::process(Args &&args) {
|
||||
auto isSong = false;
|
||||
auto isVideo = false;
|
||||
auto isVoice = (_type == SendMediaType::Audio);
|
||||
auto isRound = (_type == SendMediaType::Round);
|
||||
auto isSticker = false;
|
||||
|
||||
auto fullimage = QImage();
|
||||
@@ -711,7 +713,7 @@ void FileLoadTask::process(Args &&args) {
|
||||
// Voice sending is supported only from memory for now.
|
||||
// Because for voice we force mime type and don't read MediaInformation.
|
||||
// For a real file we always read mime type and read MediaInformation.
|
||||
Assert(!isVoice);
|
||||
Assert(!isVoice && !isRound);
|
||||
|
||||
filesize = info.size();
|
||||
filename = info.fileName();
|
||||
@@ -736,6 +738,9 @@ void FileLoadTask::process(Args &&args) {
|
||||
if (isVoice) {
|
||||
filename = filedialogDefaultName(u"audio"_q, u".ogg"_q, QString(), true);
|
||||
filemime = "audio/ogg";
|
||||
} else if (isRound) {
|
||||
filename = filedialogDefaultName(u"round"_q, u".mp4"_q, QString(), true);
|
||||
filemime = "video/mp4";
|
||||
} else {
|
||||
if (_information) {
|
||||
if (auto image = std::get_if<Ui::PreparedFileInformation::Image>(
|
||||
@@ -815,7 +820,41 @@ void FileLoadTask::process(Args &&args) {
|
||||
auto photo = MTP_photoEmpty(MTP_long(0));
|
||||
auto document = MTP_documentEmpty(MTP_long(0));
|
||||
|
||||
if (!isVoice) {
|
||||
if (isRound) {
|
||||
_information = readMediaInformation(u"video/mp4"_q);
|
||||
if (auto video = std::get_if<Ui::PreparedFileInformation::Video>(
|
||||
&_information->media)) {
|
||||
isVideo = true;
|
||||
auto coverWidth = video->thumbnail.width();
|
||||
auto coverHeight = video->thumbnail.height();
|
||||
if (video->isGifv && !_album) {
|
||||
attributes.push_back(MTP_documentAttributeAnimated());
|
||||
}
|
||||
auto flags = MTPDdocumentAttributeVideo::Flags(
|
||||
MTPDdocumentAttributeVideo::Flag::f_round_message);
|
||||
if (video->supportsStreaming) {
|
||||
flags |= MTPDdocumentAttributeVideo::Flag::f_supports_streaming;
|
||||
}
|
||||
const auto realSeconds = video->duration / 1000.;
|
||||
attributes.push_back(MTP_documentAttributeVideo(
|
||||
MTP_flags(flags),
|
||||
MTP_double(realSeconds),
|
||||
MTP_int(coverWidth),
|
||||
MTP_int(coverHeight),
|
||||
MTPint(), // preload_prefix_size
|
||||
MTPdouble(), // video_start_ts
|
||||
MTPstring())); // video_codec
|
||||
|
||||
if (args.generateGoodThumbnail) {
|
||||
goodThumbnail = video->thumbnail;
|
||||
{
|
||||
QBuffer buffer(&goodThumbnailBytes);
|
||||
goodThumbnail.save(&buffer, "JPG", kThumbnailQuality);
|
||||
}
|
||||
}
|
||||
thumbnail = PrepareFileThumbnail(std::move(video->thumbnail));
|
||||
}
|
||||
} else if (!isVoice) {
|
||||
if (!_information) {
|
||||
_information = readMediaInformation(filemime);
|
||||
filemime = _information->filemime;
|
||||
@@ -869,7 +908,7 @@ void FileLoadTask::process(Args &&args) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice) {
|
||||
if (!fullimage.isNull() && fullimage.width() > 0 && !isSong && !isVideo && !isVoice && !isRound) {
|
||||
auto w = fullimage.width(), h = fullimage.height();
|
||||
attributes.push_back(MTP_documentAttributeImageSize(MTP_int(w), MTP_int(h)));
|
||||
|
||||
@@ -975,7 +1014,7 @@ void FileLoadTask::process(Args &&args) {
|
||||
MTPVector<MTPVideoSize>(),
|
||||
MTP_int(_dcId),
|
||||
MTP_vector<MTPDocumentAttribute>(attributes));
|
||||
_type = SendMediaType::File;
|
||||
_type = isRound ? SendMediaType::Round : SendMediaType::File;
|
||||
}
|
||||
|
||||
if (_information) {
|
||||
|
||||
@@ -31,6 +31,7 @@ extern const char kOptionSendLargePhotos[];
|
||||
enum class SendMediaType {
|
||||
Photo,
|
||||
Audio,
|
||||
Round,
|
||||
File,
|
||||
ThemeFile,
|
||||
Secure,
|
||||
@@ -231,6 +232,7 @@ public:
|
||||
const QByteArray &voice,
|
||||
crl::time duration,
|
||||
const VoiceWaveform &waveform,
|
||||
bool video,
|
||||
const FileLoadTo &to,
|
||||
const TextWithTags &caption);
|
||||
~FileLoadTask();
|
||||
|
||||
@@ -94,6 +94,7 @@ enum { // Local Storage Keys
|
||||
lskCustomEmojiKeys = 0x17, // no data
|
||||
lskSearchSuggestions = 0x18, // no data
|
||||
lskWebviewTokens = 0x19, // data: QByteArray bots, QByteArray other
|
||||
lskRoundPlaceholder = 0x1a, // no data
|
||||
};
|
||||
|
||||
auto EmptyMessageDraftSources()
|
||||
@@ -220,6 +221,7 @@ base::flat_set<QString> Account::collectGoodNames() const {
|
||||
_featuredCustomEmojiKey,
|
||||
_archivedCustomEmojiKey,
|
||||
_searchSuggestionsKey,
|
||||
_roundPlaceholderKey,
|
||||
};
|
||||
auto result = base::flat_set<QString>{
|
||||
"map0",
|
||||
@@ -306,6 +308,7 @@ Account::ReadMapResult Account::readMapWith(
|
||||
quint64 legacyBackgroundKeyDay = 0, legacyBackgroundKeyNight = 0;
|
||||
quint64 userSettingsKey = 0, recentHashtagsAndBotsKey = 0, exportSettingsKey = 0;
|
||||
quint64 searchSuggestionsKey = 0;
|
||||
quint64 roundPlaceholderKey = 0;
|
||||
QByteArray webviewStorageTokenBots, webviewStorageTokenOther;
|
||||
while (!map.stream.atEnd()) {
|
||||
quint32 keyType;
|
||||
@@ -415,6 +418,9 @@ Account::ReadMapResult Account::readMapWith(
|
||||
case lskSearchSuggestions: {
|
||||
map.stream >> searchSuggestionsKey;
|
||||
} break;
|
||||
case lskRoundPlaceholder: {
|
||||
map.stream >> roundPlaceholderKey;
|
||||
} break;
|
||||
case lskWebviewTokens: {
|
||||
map.stream
|
||||
>> webviewStorageTokenBots
|
||||
@@ -456,6 +462,7 @@ Account::ReadMapResult Account::readMapWith(
|
||||
_recentHashtagsAndBotsKey = recentHashtagsAndBotsKey;
|
||||
_exportSettingsKey = exportSettingsKey;
|
||||
_searchSuggestionsKey = searchSuggestionsKey;
|
||||
_roundPlaceholderKey = roundPlaceholderKey;
|
||||
_oldMapVersion = mapData.version;
|
||||
_webviewStorageIdBots.token = webviewStorageTokenBots;
|
||||
_webviewStorageIdOther.token = webviewStorageTokenOther;
|
||||
@@ -570,6 +577,7 @@ void Account::writeMap() {
|
||||
+ Serialize::bytearraySize(_webviewStorageIdBots.token)
|
||||
+ Serialize::bytearraySize(_webviewStorageIdOther.token);
|
||||
}
|
||||
if (_roundPlaceholderKey) mapSize += sizeof(quint32) + sizeof(quint64);
|
||||
|
||||
EncryptedDescriptor mapData(mapSize);
|
||||
if (!self.isEmpty()) {
|
||||
@@ -640,6 +648,10 @@ void Account::writeMap() {
|
||||
<< _webviewStorageIdBots.token
|
||||
<< _webviewStorageIdOther.token;
|
||||
}
|
||||
if (_roundPlaceholderKey) {
|
||||
mapData.stream << quint32(lskRoundPlaceholder);
|
||||
mapData.stream << quint64(_roundPlaceholderKey);
|
||||
}
|
||||
map.writeEncrypted(mapData, _localKey);
|
||||
|
||||
_mapChanged = false;
|
||||
@@ -669,6 +681,7 @@ void Account::reset() {
|
||||
_legacyBackgroundKeyDay = _legacyBackgroundKeyNight = 0;
|
||||
_settingsKey = _recentHashtagsAndBotsKey = _exportSettingsKey = 0;
|
||||
_searchSuggestionsKey = 0;
|
||||
_roundPlaceholderKey = 0;
|
||||
_oldMapVersion = 0;
|
||||
_fileLocations.clear();
|
||||
_fileLocationPairs.clear();
|
||||
@@ -3178,6 +3191,52 @@ Webview::StorageId Account::resolveStorageIdOther() {
|
||||
return _webviewStorageIdOther;
|
||||
}
|
||||
|
||||
QImage Account::readRoundPlaceholder() {
|
||||
if (!_roundPlaceholder.isNull()) {
|
||||
return _roundPlaceholder;
|
||||
} else if (!_roundPlaceholderKey) {
|
||||
return QImage();
|
||||
}
|
||||
|
||||
FileReadDescriptor placeholder;
|
||||
if (!ReadEncryptedFile(
|
||||
placeholder,
|
||||
_roundPlaceholderKey,
|
||||
_basePath,
|
||||
_localKey)) {
|
||||
ClearKey(_roundPlaceholderKey, _basePath);
|
||||
_roundPlaceholderKey = 0;
|
||||
writeMapDelayed();
|
||||
return QImage();
|
||||
}
|
||||
|
||||
auto bytes = QByteArray();
|
||||
placeholder.stream >> bytes;
|
||||
_roundPlaceholder = Images::Read({ .content = bytes }).image;
|
||||
return _roundPlaceholder;
|
||||
}
|
||||
|
||||
void Account::writeRoundPlaceholder(const QImage &placeholder) {
|
||||
if (placeholder.isNull()) {
|
||||
return;
|
||||
}
|
||||
_roundPlaceholder = placeholder;
|
||||
|
||||
auto bytes = QByteArray();
|
||||
auto buffer = QBuffer(&bytes);
|
||||
placeholder.save(&buffer, "JPG", 87);
|
||||
|
||||
quint32 size = Serialize::bytearraySize(bytes);
|
||||
if (!_roundPlaceholderKey) {
|
||||
_roundPlaceholderKey = GenerateKey(_basePath);
|
||||
writeMapQueued();
|
||||
}
|
||||
EncryptedDescriptor data(size);
|
||||
data.stream << bytes;
|
||||
FileWriteDescriptor file(_roundPlaceholderKey, _basePath);
|
||||
file.writeEncrypted(data, _localKey);
|
||||
}
|
||||
|
||||
bool Account::encrypt(
|
||||
const void *src,
|
||||
void *dst,
|
||||
|
||||
@@ -174,6 +174,9 @@ public:
|
||||
[[nodiscard]] Webview::StorageId resolveStorageIdBots();
|
||||
[[nodiscard]] Webview::StorageId resolveStorageIdOther();
|
||||
|
||||
[[nodiscard]] QImage readRoundPlaceholder();
|
||||
void writeRoundPlaceholder(const QImage &placeholder);
|
||||
|
||||
[[nodiscard]] bool encrypt(
|
||||
const void *src,
|
||||
void *dst,
|
||||
@@ -302,6 +305,7 @@ private:
|
||||
FileKey _featuredCustomEmojiKey = 0;
|
||||
FileKey _archivedCustomEmojiKey = 0;
|
||||
FileKey _searchSuggestionsKey = 0;
|
||||
FileKey _roundPlaceholderKey = 0;
|
||||
|
||||
qint64 _cacheTotalSizeLimit = 0;
|
||||
qint64 _cacheBigFileTotalSizeLimit = 0;
|
||||
@@ -325,6 +329,8 @@ private:
|
||||
bool _mapChanged = false;
|
||||
bool _locationsChanged = false;
|
||||
|
||||
QImage _roundPlaceholder;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] Webview::StorageId TonSiteStorageId();
|
||||
|
||||
@@ -889,7 +889,9 @@ void FillPeerQrBox(
|
||||
usernameValue()).current().toUpper();
|
||||
const auto link = rpl::variable<QString>(linkValue());
|
||||
const auto textWidth = font->width(username);
|
||||
const auto top = userpicMedia->image(photoSize);
|
||||
const auto top = photoSize
|
||||
? userpicMedia->image(photoSize)
|
||||
: QImage();
|
||||
const auto weak = Ui::MakeWeak(box);
|
||||
|
||||
crl::async([=] {
|
||||
|
||||
@@ -592,7 +592,7 @@ bool Panel::showWebview(
|
||||
callback(tr::lng_bot_terms(tr::now), [=] {
|
||||
File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now));
|
||||
}, &st::menuIconGroupLog);
|
||||
callback(tr::lng_profile_bot_privacy(tr::now), [=] {
|
||||
callback(tr::lng_bot_privacy(tr::now), [=] {
|
||||
_delegate->botOpenPrivacyPolicy();
|
||||
}, &st::menuIconAntispam);
|
||||
}
|
||||
@@ -1263,7 +1263,11 @@ void Panel::processButtonMessage(
|
||||
.text = args["text"].toString(),
|
||||
});
|
||||
if (button.get() == _secondaryButton.get()) {
|
||||
_secondaryPosition = ParsePosition(args["position"].toString());
|
||||
const auto position = ParsePosition(args["position"].toString());
|
||||
if (_secondaryPosition != position) {
|
||||
_secondaryPosition = position;
|
||||
layoutButtons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ msgMargin: margins(16px, 6px, 56px, 2px);
|
||||
msgMarginTopAttached: 0px;
|
||||
msgShadow: 2px;
|
||||
msgSelectionOffset: 30px;
|
||||
msgSelectionBottomSkip: 5px;
|
||||
|
||||
historyReplyTop: 2px;
|
||||
historyReplyBottom: 2px;
|
||||
@@ -550,9 +551,7 @@ historyAdminLogCancelSearch: CrossButton {
|
||||
|
||||
duration: 150;
|
||||
loadingPeriod: 1000;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
historyAdminLogSearchTop: 11px;
|
||||
historyAdminLogSearchSlideDuration: 150;
|
||||
@@ -976,9 +975,7 @@ backgroundSwitchToDark: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(4px, 4px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
backgroundSwitchToLight: IconButton(backgroundSwitchToDark) {
|
||||
icon: icon {{ "menu/header_mode_day", boxTitleCloseFg }};
|
||||
|
||||
@@ -379,7 +379,7 @@ void VenuesController::rowPaintIcon(
|
||||
static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
|
||||
{ "maps-places-in-area", tr::lng_maps_places_in_area },
|
||||
};
|
||||
return Ui::ComputeStyles(map, phrases, Window::Theme::IsNightMode());
|
||||
return Ui::ComputeStyles(map, phrases, 100, Window::Theme::IsNightMode());
|
||||
}
|
||||
|
||||
[[nodiscard]] QByteArray ReadResource(const QString &name) {
|
||||
|
||||
1443
Telegram/SourceFiles/ui/controls/round_video_recorder.cpp
Normal file
1443
Telegram/SourceFiles/ui/controls/round_video_recorder.cpp
Normal file
File diff suppressed because it is too large
Load Diff
131
Telegram/SourceFiles/ui/controls/round_video_recorder.h
Normal file
131
Telegram/SourceFiles/ui/controls/round_video_recorder.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/path_shift_gradient.h"
|
||||
|
||||
#include <crl/crl_object_on_queue.h>
|
||||
|
||||
namespace Media::Capture {
|
||||
struct Chunk;
|
||||
struct Update;
|
||||
enum class Error : uchar;
|
||||
} // namespace Media::Capture
|
||||
|
||||
namespace tgcalls {
|
||||
class VideoCaptureInterface;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Webrtc {
|
||||
class VideoTrack;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class DynamicImage;
|
||||
class RoundVideoRecorder;
|
||||
|
||||
struct RoundVideoRecorderDescriptor {
|
||||
not_null<RpWidget*> container;
|
||||
Fn<void(not_null<RoundVideoRecorder*>)> hiding;
|
||||
Fn<void(not_null<RoundVideoRecorder*>)> hidden;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> capturer;
|
||||
std::shared_ptr<Webrtc::VideoTrack> track;
|
||||
QImage placeholder;
|
||||
};
|
||||
|
||||
struct RoundVideoResult {
|
||||
QByteArray content;
|
||||
QVector<signed char> waveform;
|
||||
crl::time duration = 0;
|
||||
QImage minithumbs;
|
||||
int minithumbsCount = 0;
|
||||
int minithumbSize = 0;
|
||||
};
|
||||
|
||||
struct RoundVideoPartial {
|
||||
RoundVideoResult video;
|
||||
crl::time from = 0;
|
||||
crl::time till = 0;
|
||||
};
|
||||
|
||||
class RoundVideoRecorder final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit RoundVideoRecorder(RoundVideoRecorderDescriptor &&descriptor);
|
||||
~RoundVideoRecorder();
|
||||
|
||||
[[nodiscard]] int previewSize() const;
|
||||
[[nodiscard]] Fn<void(Media::Capture::Chunk)> audioChunkProcessor();
|
||||
[[nodiscard]] rpl::producer<QImage> placeholderUpdates() const;
|
||||
|
||||
void pause(Fn<void(RoundVideoResult)> done = nullptr);
|
||||
void resume(RoundVideoPartial partial);
|
||||
void hide(Fn<void(RoundVideoResult)> done = nullptr);
|
||||
|
||||
void showPreview(
|
||||
std::shared_ptr<Ui::DynamicImage> silent,
|
||||
std::shared_ptr<Ui::DynamicImage> sounded);
|
||||
|
||||
using Update = Media::Capture::Update;
|
||||
using Error = Media::Capture::Error;
|
||||
[[nodiscard]] rpl::producer<Update, Error> updated();
|
||||
|
||||
private:
|
||||
class Private;
|
||||
struct PreviewFrame {
|
||||
QImage image;
|
||||
bool silent = false;
|
||||
};
|
||||
|
||||
void setup();
|
||||
void prepareFrame(bool blurred = false);
|
||||
void preparePlaceholder(const QImage &placeholder);
|
||||
void createImages();
|
||||
void progressTo(float64 progress);
|
||||
void fade(bool visible);
|
||||
|
||||
[[nodiscard]] Fn<void()> updater() const;
|
||||
[[nodiscard]] PreviewFrame lookupPreviewFrame() const;
|
||||
|
||||
const RoundVideoRecorderDescriptor _descriptor;
|
||||
style::owned_color _gradientBg;
|
||||
style::owned_color _gradientFg;
|
||||
PathShiftGradient _gradient;
|
||||
std::unique_ptr<RpWidget> _preview;
|
||||
crl::object_on_queue<Private> _private;
|
||||
Ui::Animations::Simple _progressAnimation;
|
||||
Ui::Animations::Simple _fadeAnimation;
|
||||
Ui::Animations::Simple _fadeContentAnimation;
|
||||
rpl::event_stream<QImage> _placeholderUpdates;
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> _silentPreview;
|
||||
std::shared_ptr<Ui::DynamicImage> _soundedPreview;
|
||||
Ui::Animations::Simple _fadePreviewAnimation;
|
||||
PreviewFrame _cachedPreviewFrame;
|
||||
|
||||
float64 _progress = 0.;
|
||||
QImage _frameOriginal;
|
||||
QImage _framePlaceholder;
|
||||
QImage _framePrepared;
|
||||
QImage _shadow;
|
||||
int _lastAddedIndex = 0;
|
||||
int _preparedIndex = 0;
|
||||
int _side = 0;
|
||||
int _progressStroke = 0;
|
||||
int _extent = 0;
|
||||
int _skipFrames = 0;
|
||||
bool _progressReceived = false;
|
||||
bool _visible = false;
|
||||
bool _paused = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -86,6 +86,7 @@ void SendButton::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
switch (_type) {
|
||||
case Type::Record: paintRecord(p, over); break;
|
||||
case Type::Round: paintRound(p, over); break;
|
||||
case Type::Save: paintSave(p, over); break;
|
||||
case Type::Cancel: paintCancel(p, over); break;
|
||||
case Type::Send: paintSend(p, over); break;
|
||||
@@ -108,6 +109,20 @@ void SendButton::paintRecord(QPainter &p, bool over) {
|
||||
icon.paintInCenter(p, rect());
|
||||
}
|
||||
|
||||
void SendButton::paintRound(QPainter &p, bool over) {
|
||||
if (!isDisabled()) {
|
||||
paintRipple(
|
||||
p,
|
||||
(width() - _st.inner.rippleAreaSize) / 2,
|
||||
_st.inner.rippleAreaPosition.y());
|
||||
}
|
||||
|
||||
const auto &icon = (isDisabled() || !over)
|
||||
? _st.round
|
||||
: _st.roundOver;
|
||||
icon.paintInCenter(p, rect());
|
||||
}
|
||||
|
||||
void SendButton::paintSave(QPainter &p, bool over) {
|
||||
const auto &saveIcon = over
|
||||
? st::historyEditSaveIconOver
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
Schedule,
|
||||
Save,
|
||||
Record,
|
||||
Round,
|
||||
Cancel,
|
||||
Slowmode,
|
||||
};
|
||||
@@ -47,6 +48,7 @@ private:
|
||||
[[nodiscard]] bool isSlowmode() const;
|
||||
|
||||
void paintRecord(QPainter &p, bool over);
|
||||
void paintRound(QPainter &p, bool over);
|
||||
void paintSave(QPainter &p, bool over);
|
||||
void paintCancel(QPainter &p, bool over);
|
||||
void paintSend(QPainter &p, bool over);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user