Compare commits

...

57 Commits

Author SHA1 Message Date
John Preston
78937d716f Beta version 5.6.4.
- Add recording of video messages in case a camera is available.
- Add "Respect the Focus settings" for custom notifications on Windows.
2024-10-24 13:40:52 +04:00
John Preston
9713abc002 Fix build with GCC. 2024-10-24 13:36:22 +04:00
John Preston
b44b45cca0 Fix docker build with openh264 in ffmpeg. 2024-10-24 13:26:46 +04:00
John Preston
9e2cf0ed73 Nice faded show/hide of round recorder. 2024-10-24 13:26:46 +04:00
John Preston
b01d7ea5b9 Show recording init errors. 2024-10-24 13:26:46 +04:00
John Preston
ae89b65a98 Fix message selection checks position. 2024-10-24 13:26:46 +04:00
John Preston
9b9c3d788d Skip some first frames. 2024-10-24 13:26:46 +04:00
John Preston
ccc6c6daa5 Save last camera image as a placeholder. 2024-10-24 13:26:46 +04:00
John Preston
9ce6636c6a Create nice camera init effect. 2024-10-24 13:26:46 +04:00
John Preston
6287d306c2 Add better discard confirmations. 2024-10-24 13:26:46 +04:00
John Preston
6cfa053328 Add recorded round video preview. 2024-10-24 13:26:45 +04:00
John Preston
9514b6eecd Show mini-thumbnails when pausing recording. 2024-10-24 13:26:45 +04:00
John Preston
c8d4818d22 Blurred frame while paused. 2024-10-24 13:26:45 +04:00
John Preston
4142ada729 Concatenate two recordings. 2024-10-24 13:26:45 +04:00
John Preston
d7ffdbd78d Prepare for round record pause/resume. 2024-10-24 13:26:45 +04:00
John Preston
e8d87d37bb Use a shorter phrase for bot policy. 2024-10-24 13:26:45 +04:00
John Preston
343ffc23eb Add some dir="auto" to IV page.
I hope this fixes #28551.
2024-10-24 13:26:45 +04:00
John Preston
95e0086eed Remov unused variable. 2024-10-24 13:26:45 +04:00
John Preston
c010ecfe38 Allow sending one-time round videos. 2024-10-24 13:26:45 +04:00
John Preston
302e9371c8 Show record init errors. 2024-10-24 13:26:45 +04:00
John Preston
7060c0e6d7 Fix crash in context menu without an item.
Fixes #28552.
2024-10-24 13:26:42 +04:00
John Preston
20a4c7f9f4 Wait for both audio and video to start. 2024-10-24 13:24:44 +04:00
John Preston
e59e4afd3e Show recording progress. 2024-10-24 13:24:44 +04:00
John Preston
f74dd3ca1e Add author to the top of Reply in Another Chat. 2024-10-24 13:24:44 +04:00
John Preston
511cfc524f Adjust bitrates for video messages. 2024-10-24 13:24:44 +04:00
John Preston
4cf6173d25 Cut video messages round with white bg. 2024-10-24 13:24:44 +04:00
John Preston
17996757fd Show round video preview. 2024-10-24 13:24:44 +04:00
John Preston
6bc1049858 Use nice icon for video message recording. 2024-10-24 13:24:43 +04:00
John Preston
ff44f626ba Allow switching between voice/video. 2024-10-24 13:24:43 +04:00
John Preston
552343fa37 PoC video messages sending. 2024-10-24 13:24:43 +04:00
John Preston
4dc7fd8cd1 Add AAC/H264 encoders, MP4 muxer. 2024-10-24 13:24:43 +04:00
John Preston
285c96fd2e Add "Respect the Focus settings" on Windows. 2024-10-24 13:24:43 +04:00
John Preston
e6af33367e Accept bot username in chatbot setup. 2024-10-24 13:24:43 +04:00
23rd
7092fe2242 Added default ripple animation style with windowBgOver color. 2024-10-24 09:05:27 +04:00
23rd
a32ff46579 Added special hotkeys to change IV zoom more gradually. 2024-10-24 09:05:27 +04:00
23rd
7f20cf59d1 Added ministars to button in service messages for gifts. 2024-10-24 09:05:27 +04:00
23rd
a8a1b08127 Added ministars to buttons in section of peer gifts. 2024-10-24 09:05:27 +04:00
23rd
1a1e777b87 Fixed display of generic media with small width. 2024-10-24 09:05:27 +04:00
23rd
9e76e64064 Improved parts alignments of round video messages. 2024-10-24 09:05:27 +04:00
23rd
975ae17ef9 Moved selection marks to bottom of message bubbles. 2024-10-24 09:05:27 +04:00
23rd
ed9dcef66f Fixed display of selection marks for messages with huge code blocks. 2024-10-24 09:05:27 +04:00
23rd
b1e537e54e Added subscription icon to channel photo in ReceiptCreditsBox. 2024-10-24 09:05:27 +04:00
23rd
e5886862c3 Changed behavior to copy phone number from profile sections as trimmed. 2024-10-24 09:05:27 +04:00
Ilya Fedin
d85b668d4f Fix lambda execution for portal dark mode getter 2024-10-24 07:04:52 +02:00
Grigory
b363d8bfb5 fix(ui/webview_helpers): append 0 if color channel value is 1 char long 2024-10-19 18:43:11 +02:00
John Preston
754d467440 Version 5.6.3: Fix build with Xcode. 2024-10-15 22:13:06 +04:00
John Preston
598f08d6c7 Version 5.6.3.
- Add ability to change page scale in Instant View pages.
- Fix unnecessary timestamp jump to a new line.
- Fix secondary button positioning in miniapps.
- Fix a crash in QR code copy for a chat without a photo.
- Fix a crash in share options menu showing.
- Fix a crash on monitor disconnect.
2024-10-15 21:01:27 +04:00
23rd
224fdc1864 Returned back freedom to copy links even in non-forwardable histories.
This reverts commits b64c610abb
and 9cd194e60e.
2024-10-15 13:04:57 +03:00
23rd
e646b4dc9a Slightly improved position of selection marks for wide chats. 2024-10-15 12:59:15 +03:00
23rd
9077db2e97 Added verified mark of web view bots to box of confirmation to open. 2024-10-15 12:46:13 +03:00
23rd
7e14277ead Added ability to change zoom in IV. 2024-10-15 12:46:13 +03:00
John Preston
d351a7d697 Fix crash on monitor arrangement change. 2024-10-15 12:13:53 +04:00
John Preston
70ed43b811 Fix crash on send options menu show.
Fixes #28532.
2024-10-15 12:13:53 +04:00
John Preston
913083ebc6 Fix miniapp buttons positioning. 2024-10-15 12:13:52 +04:00
John Preston
588a95a7ae Fix time jumping to a new line. 2024-10-15 12:13:52 +04:00
23rd
c0a0ad4ec5 Added info to context menu for non-forwardable history. 2024-10-15 01:15:04 +03:00
23rd
5eafe96525 Fixed crash in share QR box on copy without profile photo. 2024-10-15 01:15:00 +03:00
114 changed files with 3791 additions and 540 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -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 {

View File

@@ -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}.";

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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.");
}

View File

@@ -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));
}

View File

@@ -317,6 +317,7 @@ public:
QByteArray result,
VoiceWaveform waveform,
crl::time duration,
bool video,
const SendAction &action);
void sendFiles(
Ui::PreparedList &&list,

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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 },

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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(

View File

@@ -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();

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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 &centerRect) const;
[[nodiscard]] int computeTopMargin(int height) const;
[[nodiscard]] QRect computeWaveformRect(const QRect &centerRect) 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

View File

@@ -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;

View File

@@ -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(),

View File

@@ -31,6 +31,7 @@ public:
enum class Type {
Send,
Record,
Round,
};
void setType(Type state);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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());

View File

@@ -1224,6 +1224,7 @@ void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) {
data.bytes,
data.waveform,
data.duration,
data.video,
std::move(action));
_composeControls->cancelReplyMessage();

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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(

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(

View File

@@ -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);

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -165,9 +165,7 @@ introBackButton: IconButton(defaultIconButton) {
rippleAreaPosition: point(8px, 8px);
rippleAreaSize: 40px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
introQrTop: -18px;

View File

@@ -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) {

View File

@@ -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));

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -244,6 +244,7 @@ void ReplyArea::sendVoice(VoiceToSend &&data) {
data.bytes,
data.waveform,
data.duration,
data.video,
std::move(action));
_controls->clearListenState();

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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(

View File

@@ -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;

View File

@@ -204,7 +204,7 @@ LinuxIntegration::LinuxIntegration()
return value->get_uint32() == 1;
}
return std::nullopt;
})
}())
, _darkModeWatcher(
"org.freedesktop.appearance",
"color-scheme",

View File

@@ -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) {

View File

@@ -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);

View File

@@ -1200,6 +1200,7 @@ void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) {
data.bytes,
data.waveform,
data.duration,
data.video,
std::move(action));
_composeControls->cancelReplyMessage();

View File

@@ -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),

View File

@@ -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,

View File

@@ -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;
};

View File

@@ -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();

View File

@@ -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);

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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,

View File

@@ -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();

View File

@@ -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([=] {

View File

@@ -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();
}
}
}

View File

@@ -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 }};

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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

View File

@@ -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