Merge remote-tracking branch 'tdesktop/dev' into dev

This commit is contained in:
Eric Kotato 2020-01-23 21:24:57 +03:00
commit a7f2922f01
190 changed files with 3438 additions and 7317 deletions

View file

@ -1,86 +0,0 @@
@echo off
IF "%BUILD_DIR%"=="" SET BUILD_DIR=C:\TBuild
SET LIB_DIR=%BUILD_DIR%\Libraries
SET SRC_DIR=%BUILD_DIR%\tdesktop
SET QT_VERSION=5_6_2
call:configureBuild
call:getDependencies
call:setupGYP
cd %SRC_DIR%
echo Finished!
GOTO:EOF
:: FUNCTIONS
:logInfo
echo [INFO] %~1
GOTO:EOF
:logError
echo [ERROR] %~1
GOTO:EOF
:getDependencies
call:logInfo "Clone dependencies repository"
git clone -q --depth 1 --branch master https://github.com/telegramdesktop/dependencies_windows.git %LIB_DIR%
cd %LIB_DIR%
git clone --depth 1 --branch 0.9.1 https://github.com/ericniebler/range-v3
if exist prepare.bat (
call prepare.bat
) else (
call:logError "Error cloning dependencies, trying again"
rmdir %LIB_DIR% /S /Q
call:getDependencies
)
GOTO:EOF
:setupGYP
call:logInfo "Setup GYP/Ninja and generate VS solution"
cd %LIB_DIR%
git clone https://github.com/telegramdesktop/gyp.git
cd gyp
git checkout tdesktop
SET PATH=%PATH%;%BUILD_DIR%\Libraries\gyp;%BUILD_DIR%\Libraries\ninja;
cd %SRC_DIR%
git submodule init
git submodule update
cd %SRC_DIR%\Telegram
call gyp\refresh.bat --api-id 17349 --api-hash 344583e45741c457fe1862106095a5eb --ci-build
GOTO:EOF
:configureBuild
call:logInfo "Configuring build"
call:logInfo "Build version: %BUILD_VERSION%"
set TDESKTOP_BUILD_DEFINES=
echo %BUILD_VERSION% | findstr /C:"disable_register_custom_scheme">nul && (
set TDESKTOP_BUILD_DEFINES=%TDESKTOP_BUILD_DEFINES%,TDESKTOP_DISABLE_REGISTER_CUSTOM_SCHEME
)
echo %BUILD_VERSION% | findstr /C:"disable_crash_reports">nul && (
set TDESKTOP_BUILD_DEFINES=%TDESKTOP_BUILD_DEFINES%,DESKTOP_APP_DISABLE_CRASH_REPORTS
)
echo %BUILD_VERSION% | findstr /C:"disable_network_proxy">nul && (
set TDESKTOP_BUILD_DEFINES=%TDESKTOP_BUILD_DEFINES%,TDESKTOP_DISABLE_NETWORK_PROXY
)
echo %BUILD_VERSION% | findstr /C:"disable_desktop_file_generation">nul && (
set TDESKTOP_BUILD_DEFINES=%TDESKTOP_BUILD_DEFINES%,TDESKTOP_DISABLE_DESKTOP_FILE_GENERATION
)
echo %BUILD_VERSION% | findstr /C:"disable_gtk_integration">nul && (
set TDESKTOP_BUILD_DEFINES=%TDESKTOP_BUILD_DEFINES%,TDESKTOP_DISABLE_GTK_INTEGRATION
)
if not "%TDESKTOP_BUILD_DEFINES%" == "" (
set "TDESKTOP_BUILD_DEFINES=%TDESKTOP_BUILD_DEFINES:~1%"
)
call:logInfo "Build Defines: %TDESKTOP_BUILD_DEFINES%"
GOTO:EOF

View file

@ -8,7 +8,7 @@ about: Report errors or unexpected behavior.
Thanks for reporting issues of Telegram Desktop!
To make it easier for us to help you please enter detailed information below.
-->
-->
### Steps to reproduce
1.
2.

103
.github/workflows/issue_closer.yml vendored Normal file
View file

@ -0,0 +1,103 @@
name: Issue closer.
on:
issues:
types: opened
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Get the latest version.
run: |
tag=$(git ls-remote --tags git://github.com/$GITHUB_REPOSITORY | cut -f 2 | tail -n1)
echo $tag
echo ::set-env name=LATEST_TAG::$tag
- name: Check a version from an issue.
uses: actions/github-script@0.4.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
let errorStr = "Version not found.";
let item1 = "Version of Telegram Desktop";
let item2 = "Used theme";
let body = context.payload.issue.body;
console.log("Body of issue:\n" + body);
let index1 = body.indexOf(item1) + item1.length;
let index2 = body.indexOf(item2);
index2 = (index2 == -1) ? Number.MAX_SAFE_INTEGER : index2;
console.log("Index 1: " + index1);
console.log("Index 2: " + index2);
if (index1 == -1) {
console.log(errorStr);
return;
}
function parseVersion(str) {
let pattern = /[0-9]\.[0-9][0-9.]{0,}/g;
return str.match(pattern);
}
function firstNum(version) {
return version[0].split(".")[0];
}
let issueVer = parseVersion(body.substring(index1, index2));
if (issueVer == undefined) {
console.log(errorStr);
return;
}
console.log("Version from issue: " + issueVer[0]);
let latestVer = parseVersion(process.env.LATEST_TAG);
if (latestVer == undefined) {
console.log(errorStr);
return;
}
console.log("Version from tags: " + latestVer[0]);
let issueNum = firstNum(issueVer);
let latestNum = firstNum(latestVer);
if (issueNum <= latestNum && issueNum < 5) {
console.log("Seems the version of this issue is fine!");
return;
}
let message = `
Sorry, but according to the version you specify in this issue, \
you are using the [Telegram for macOS](https://macos.telegram.org), \
not the [Telegram Desktop](https://desktop.telegram.org).
You can report your issue to [the group](https://t.me/macswift) \
or to [the repository of Telegram for macOS](https://github.com/overtake/TelegramSwift).
If I made a mistake and closed your issue wrongly, please reopen it. Thanks!
`;
let params = {
owner: context.issue.owner,
repo: context.issue.repo,
issue_number: context.issue.number
};
github.issues.createComment({
...params,
body: message
});
github.issues.addLabels({
...params,
labels: ['TG macOS Swift']
});
github.issues.update({
...params,
state: 'closed'
});

View file

@ -102,7 +102,7 @@ jobs:
git clone $GIT/desktop-app/zlib.git
cd zlib
CFLAGS="$MIN_MAC $UNGUARDED" LDFLAGS="$MIN_MAC" ./configure
CFLAGS="$MIN_MAC $UNGUARDED" LDFLAGS="$MIN_MAC" ./configure --prefix=$PREFIX
make -j$(nproc)
sudo make install
@ -429,4 +429,4 @@ jobs:
name: Upload artifact.
with:
name: Telegram
path: $REPO_NAME\out\Debug\artifact\
path: ${{ env.REPO_NAME }}/out/Debug/artifact/

View file

@ -23,7 +23,6 @@ jobs:
env:
SDK: "10.0.18362.0"
VC: "call vcvars32.bat && cd Libraries"
PY2: 'C:\hostedtoolcache\windows\Python\2.7.16\x64'
GIT: "https://github.com"
QT: "5_12_5"
OPENSSL_VER: "1_1_1"
@ -186,6 +185,16 @@ jobs:
run: |
cd %LibrariesPath%
echo Find any version of Python 2.
for /D %%a in (C:\hostedtoolcache\windows\Python\2.*) do (
SET PY2=%%a\x64
)
IF [%PY2%] == [] (
echo Python 2 is not found.
exit 1
)
echo Found %PY2%.
git clone %GIT%/telegramdesktop/gyp.git
cd gyp
SET PATH=%PY2%;%cd%;%PATH%

3
.gitmodules vendored
View file

@ -28,9 +28,6 @@
[submodule "Telegram/lib_base"]
path = Telegram/lib_base
url = https://github.com/desktop-app/lib_base.git
[submodule "Telegram/gyp/helpers"]
path = Telegram/gyp/helpers
url = https://github.com/desktop-app/gyp_helpers.git
[submodule "Telegram/codegen"]
path = Telegram/codegen
url = https://github.com/desktop-app/codegen.git

View file

@ -224,7 +224,7 @@ buildRange() {
rm -rf *
cd "$EXTERNAL"
git clone --depth 1 --branch 0.9.1 https://github.com/ericniebler/range-v3
git clone --depth 1 --branch 0.10.0 https://github.com/ericniebler/range-v3
cd "$EXTERNAL/range-v3"
cp -r * "$RANGE_PATH/"

View file

@ -43,7 +43,7 @@ travisStartFold() {
fi
echo "travis_fold:start:$NAME"
sameLineInfoMessage "$TITLE"
sameLineInfoMessage "$TITLE"
TRAVIS_LAST_FOLD="$NAME"
}

View file

@ -524,6 +524,10 @@ PRIVATE
info/media/info_media_widget.h
info/members/info_members_widget.cpp
info/members/info_members_widget.h
info/polls/info_polls_results_inner_widget.cpp
info/polls/info_polls_results_inner_widget.h
info/polls/info_polls_results_widget.cpp
info/polls/info_polls_results_widget.h
info/profile/info_profile_actions.cpp
info/profile/info_profile_actions.h
info/profile/info_profile_cover.cpp
@ -1096,7 +1100,11 @@ elseif (build_osx)
else()
set(bundle_identifier "com.tdesktop.Telegram$<$<CONFIG:Debug>:Debug>")
set(bundle_entitlements "Telegram.entitlements")
set(output_name "Telegram")
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
set(output_name "telegram-desktop")
else()
set(output_name "Telegram")
endif()
endif()
set_target_properties(Telegram PROPERTIES
@ -1193,3 +1201,17 @@ if ((NOT disable_autoupdate OR NOT LINUX) AND NOT build_macstore AND NOT build_w
set_target_properties(Packer PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
endif()
endif()
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
include(GNUInstallDirs)
install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "telegram.png")
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "telegram.png")
install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "telegram.png")
install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "telegram.png")
install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "telegram.png")
install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "telegram.png")
install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "telegram.png")
install(FILES "../lib/xdg/telegramdesktop.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications" RENAME "${TDESKTOP_LAUNCHER_BASENAME}.desktop")
install(FILES "../lib/xdg/telegramdesktop.appdata.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo" RENAME "${TDESKTOP_LAUNCHER_BASENAME}.appdata.xml")
endif()

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

View file

@ -158,6 +158,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_deleted_message" = "Deleted message";
"lng_pinned_message" = "Pinned message";
"lng_pinned_poll" = "Pinned poll";
"lng_pinned_quiz" = "Pinned quiz";
"lng_pinned_unpin_sure" = "Would you like to unpin this message?";
"lng_pinned_pin_sure" = "Would you like to pin this message?";
"lng_pinned_pin" = "Pin";
@ -1294,6 +1295,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_scheduled_messages" = "Scheduled Messages";
"lng_reminder_messages" = "Reminders";
"lng_scheduled_date" = "Scheduled for {date}";
"lng_scheduled_date_until_online" = "Scheduled until online";
"lng_scheduled_send_until_online" = "Send when online";
"lng_scheduled_send_now" = "Send message now?";
"lng_scheduled_send_now_many#one" = "Send {count} message now?";
"lng_scheduled_send_now_many#other" = "Send {count} messages now?";
@ -1453,6 +1457,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_send_album" = "Send as an album";
"lng_send_photo" = "Send as a photo";
"lng_send_file" = "Send as a file";
"lng_send_media_invalid_files" = "Sorry, no valid files found.";
"lng_forward_choose" = "Choose recipient...";
"lng_forward_cant" = "Sorry, no way to forward here :(";
@ -1797,6 +1802,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_restricted_send_inline_all" = "Posting inline content isn't allowed in this group.";
"lng_restricted_send_polls_all" = "Posting polls isn't allowed in this group.";
"lng_restricted_send_public_polls" = "Sorry, public polls can't be forwarded to channels.";
"lng_exceptions_list_title" = "Exceptions";
"lng_removed_list_title" = "Removed users";
@ -2166,10 +2173,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_launch_exe_dont_ask" = "Don't ask me again";
"lng_polls_anonymous" = "Anonymous Poll";
"lng_polls_public" = "Poll";
"lng_polls_anonymous_quiz" = "Anonymous Quiz";
"lng_polls_public_quiz" = "Quiz";
"lng_polls_closed" = "Final results";
"lng_polls_votes_count#one" = "{count} vote";
"lng_polls_votes_count#other" = "{count} votes";
"lng_polls_votes_none" = "No votes";
"lng_polls_answers_count#one" = "{count} answer";
"lng_polls_answers_count#other" = "{count} answers";
"lng_polls_answers_none" = "No answers";
"lng_polls_submit_votes" = "Vote";
"lng_polls_view_results" = "View results";
"lng_polls_retract" = "Retract vote";
"lng_polls_stop" = "Stop poll";
"lng_polls_stop_warning" = "If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone.";
@ -2183,8 +2198,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_create_limit#one" = "You can add {count} more option.";
"lng_polls_create_limit#other" = "You can add {count} more options.";
"lng_polls_create_maximum" = "You have added the maximum number of options.";
"lng_polls_create_settings" = "Settings";
"lng_polls_create_hint" = "Tap to select the right option";
"lng_polls_create_anonymous" = "Anonymous Votes";
"lng_polls_create_multiple_choice" = "Multiple Answers";
"lng_polls_create_quiz_mode" = "Quiz Mode";
"lng_polls_create_button" = "Create";
"lng_polls_poll_results_title" = "Poll results";
"lng_polls_quiz_results_title" = "Quiz results";
"lng_polls_show_more#one" = "Show more ({count})";
"lng_polls_show_more#other" = "Show more ({count})";
"lng_polls_votes_collapse" = "Collapse";
"lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM.";
"lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}.";
"lng_outdated_now" = "So that Telegram Desktop can update to newer versions.";

View file

@ -71,7 +71,7 @@ inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int =
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
inputMediaGeoLive#ce4e82fd flags:# stopped:flags.0?true geo_point:InputGeoPoint period:flags.1?int = InputMedia;
inputMediaPoll#6b3765b poll:Poll = InputMedia;
inputMediaPoll#abe9ca25 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> = InputMedia;
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
@ -351,6 +351,7 @@ updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector<int> = Update;
updateTheme#8216fba3 theme:Theme = Update;
updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update;
updateLoginToken#564fe691 = Update;
updateMessagePollVote#42f88f2c poll_id:long user_id:int options:Vector<bytes> = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -546,6 +547,7 @@ keyboardButtonGame#50f41ccf text:string = KeyboardButton;
keyboardButtonBuy#afd93fbb text:string = KeyboardButton;
keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton;
inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton;
keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = KeyboardButton;
keyboardButtonRow#77608b83 buttons:Vector<KeyboardButton> = KeyboardButtonRow;
@ -1015,11 +1017,11 @@ help.userInfo#1eb3758 message:string entities:Vector<MessageEntity> author:strin
pollAnswer#6ca9c2e9 text:string option:bytes = PollAnswer;
poll#d5529d06 id:long flags:# closed:flags.0?true question:string answers:Vector<PollAnswer> = Poll;
poll#d5529d06 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector<PollAnswer> = Poll;
pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true option:bytes voters:int = PollAnswerVoters;
pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters;
pollResults#5755785a flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int = PollResults;
pollResults#c87024a2 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<int> = PollResults;
chatOnlines#f041e250 onlines:int = ChatOnlines;
@ -1077,16 +1079,11 @@ restrictionReason#d072acb4 platform:string reason:string text:string = Restricti
inputTheme#3c5693e9 id:long access_hash:long = InputTheme;
inputThemeSlug#f5890df1 slug:string = InputTheme;
themeDocumentNotModified#483d270c = Theme;
theme#28f1114 flags:# creator:flags.0?true default:flags.1?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?ThemeSettings installs_count:int = Theme;
account.themesNotModified#f41eb622 = account.Themes;
account.themes#7f676421 hash:int themes:Vector<Theme> = account.Themes;
wallet.liteResponse#764386d7 response:bytes = wallet.LiteResponse;
wallet.secretSalt#dd484d64 salt:bytes = wallet.KeySecretSalt;
auth.loginToken#629f1980 expires:int token:bytes = auth.LoginToken;
auth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken;
auth.loginTokenSuccess#390d5c5e authorization:auth.Authorization = auth.LoginToken;
@ -1107,6 +1104,12 @@ themeSettings#9c14984a flags:# base_theme:BaseTheme accent_color:int message_top
webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector<Document> settings:flags.1?ThemeSettings = WebPageAttribute;
messageUserVote#a28e5559 user_id:int option:bytes date:int = MessageUserVote;
messageUserVoteInputOption#36377430 user_id:int date:int = MessageUserVote;
messageUserVoteMultiple#e8fe0de user_id:int options:Vector<bytes> date:int = MessageUserVote;
messages.votesList#823f649 flags:# count:int votes:Vector<MessageUserVote> users:Vector<User> next_offset:flags.0?string = messages.VotesList;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -1343,6 +1346,7 @@ messages.getScheduledHistory#e2c2685b peer:InputPeer hash:int = messages.Message
messages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector<int> = messages.Messages;
messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates;
messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector<int> = Updates;
messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList;
updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
@ -1354,7 +1358,7 @@ photos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
upload.getFile#b15a9afc flags:# precise:flags.0?true location:InputFileLocation offset:int limit:int = upload.File;
upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File;
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile;
@ -1451,7 +1455,4 @@ langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLangua
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
folders.deleteFolder#1c295881 folder_id:int = Updates;
wallet.sendLiteRequest#e2c9d33e body:bytes = wallet.LiteResponse;
wallet.getKeySecretSalt#b57f346 revoke:Bool = wallet.KeySecretSalt;
// LAYER 108
// LAYER 109

View file

@ -9,7 +9,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="1.9.4.0" />
Version="1.9.7.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
</application>
</application>
</compatibility>
</assembly>

View file

@ -21,6 +21,7 @@ struct SendOptions {
enum class SendType {
Normal,
Scheduled,
ScheduledToUser, // For "Send when online".
};
struct SendAction {

View file

@ -5842,12 +5842,24 @@ void ApiWrap::createPoll(
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
}
const auto inputFlags = data.quiz()
? MTPDinputMediaPoll::Flag::f_correct_answers
: MTPDinputMediaPoll::Flag(0);
auto correct = QVector<MTPbytes>();
for (const auto &answer : data.answers) {
if (answer.correct) {
correct.push_back(MTP_bytes(answer.option));
}
}
const auto replyTo = action.replyTo;
history->sendRequestId = request(MTPmessages_SendMedia(
MTP_flags(sendFlags),
peer->input,
MTP_int(replyTo),
MTP_inputMediaPoll(PollDataToMTP(&data)),
MTP_inputMediaPoll(
MTP_flags(inputFlags),
PollDataToMTP(&data),
MTP_vector<MTPbytes>(correct)),
MTP_string(),
MTP_long(rand_value<uint64>()),
MTPReplyMarkup(),
@ -5879,13 +5891,13 @@ void ApiWrap::sendPollVotes(
const auto hideSending = [=] {
if (showSending) {
if (const auto item = _session->data().message(itemId)) {
poll->sendingVote = QByteArray();
poll->sendingVotes.clear();
_session->data().requestItemRepaint(item);
}
}
};
if (showSending) {
poll->sendingVote = options.front();
poll->sendingVotes = options;
_session->data().requestItemRepaint(item);
}
@ -5921,12 +5933,24 @@ void ApiWrap::closePoll(not_null<HistoryItem*> item) {
return;
}
const auto inputFlags = poll->quiz()
? MTPDinputMediaPoll::Flag::f_correct_answers
: MTPDinputMediaPoll::Flag(0);
auto correct = QVector<MTPbytes>();
for (const auto &answer : poll->answers) {
if (answer.correct) {
correct.push_back(MTP_bytes(answer.option));
}
}
const auto requestId = request(MTPmessages_EditMessage(
MTP_flags(MTPmessages_EditMessage::Flag::f_media),
item->history()->peer->input,
MTP_int(item->id),
MTPstring(),
MTP_inputMediaPoll(PollDataToMTP(poll)),
MTP_inputMediaPoll(
MTP_flags(inputFlags),
PollDataToMTP(poll, true),
MTP_vector<MTPbytes>(correct)),
MTPReplyMarkup(),
MTPVector<MTPMessageEntity>(),
MTP_int(0) // schedule_date

View file

@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "numbers.h"
#include "observer_peer.h"
#include "main/main_session.h"
#include "styles/style_boxes.h"
#include "styles/style_overview.h"
#include "styles/style_mediaview.h"
#include "styles/style_chat_helpers.h"
@ -180,6 +181,8 @@ namespace App {
prepareCorners(MessageInSelectedCorners, st::historyMessageRadius, st::msgInBgSelected, &st::msgInShadowSelected);
prepareCorners(MessageOutCorners, st::historyMessageRadius, st::msgOutBg, &st::msgOutShadow);
prepareCorners(MessageOutSelectedCorners, st::historyMessageRadius, st::msgOutBgSelected, &st::msgOutShadowSelected);
prepareCorners(SendFilesBoxAlbumGroupCorners, st::sendBoxAlbumGroupRadius, st::callFingerprintBg);
}
void createCorners() {

View file

@ -63,6 +63,8 @@ enum RoundCorners : int {
MessageOutCorners,
MessageOutSelectedCorners,
SendFilesBoxAlbumGroupCorners,
RoundCornersCount
};

View file

@ -514,6 +514,29 @@ editMediaButton: IconButton {
ripple: defaultRippleAnimation;
}
// SendFilesBox
sendBoxAlbumGroupEditInternalSkip: 9px;
sendBoxAlbumGroupSkipRight: 6px;
sendBoxAlbumGroupSkipTop: 6px;
sendBoxAlbumGroupRadius: 12px;
sendBoxAlbumGroupHeight: 25px;
sendBoxAlbumGroupEditButtonIcon: editMediaButtonIconPhoto;
sendBoxAlbumGroupEditButtonIconPosition: point(4px, -1px);
sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) {
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgRipple;
}
}
sendBoxAlbumGroupDeleteButtonIconPosition: point(-3px, 0px);
sendBoxAlbumGroupDeleteButtonIcon: icon {{ "history_file_cancel", msgServiceFg}};
sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "history_file_cancel", menuIconFg, point(6px, 6px) }};
// End of SendFilesBox
calendarTitleHeight: boxTitleHeight;
calendarPrevious: IconButton {
width: calendarTitleHeight;
@ -812,15 +835,15 @@ createPollField: InputField(defaultInputField) {
}
createPollFieldPadding: margins(22px, 5px, 22px, 5px);
createPollOptionField: InputField(createPollField) {
textMargins: margins(22px, 8px, 40px, 8px);
textMargins: margins(22px, 11px, 40px, 11px);
placeholderMargins: margins(2px, 0px, 2px, 0px);
heightMax: 64px;
heightMax: 68px;
}
createPollLimitLabel: FlatLabel(defaultFlatLabel) {
minWidth: 274px;
align: align(topleft);
}
createPollLimitPadding: margins(22px, 10px, 22px, 5px);
createPollLimitPadding: margins(22px, 10px, 22px, 16px);
createPollOptionRemove: CrossButton {
width: 22px;
height: 22px;
@ -841,7 +864,7 @@ createPollOptionRemove: CrossButton {
color: windowBgOver;
}
}
createPollOptionRemovePosition: point(10px, 7px);
createPollOptionRemovePosition: point(11px, 9px);
createPollWarning: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
palette: TextPalette(defaultTextPalette) {
@ -849,6 +872,8 @@ createPollWarning: FlatLabel(defaultFlatLabel) {
}
}
createPollWarningPosition: point(16px, 6px);
createPollCheckboxMargin: margins(23px, 10px, 23px, 10px);
createPollFieldTitlePadding: margins(22px, 7px, 10px, 6px);
callSettingsButton: IconButton {
width: 50px;
@ -922,6 +947,33 @@ customBadgeField: InputField(defaultInputField) {
heightMin: 32px;
}
pollResultsQuestion: FlatLabel(defaultFlatLabel) {
minWidth: 320px;
textFg: windowBoldFg;
style: TextStyle(defaultTextStyle) {
font: font(16px semibold);
linkFont: font(16px semibold);
linkFontOver: font(16px semibold underline);
}
}
pollResultsVotesCount: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
}
pollResultsHeaderPadding: margins(22px, 22px, 22px, 8px);
pollResultsShowMore: SettingsButton {
textFg: lightButtonFg;
textFgOver: lightButtonFgOver;
textBg: windowBg;
textBgOver: windowBgOver;
font: semiboldFont;
height: 20px;
padding: margins(71px, 10px, 8px, 8px);
ripple: defaultRippleAnimation;
}
fontsBoxTextStyle: TextStyle(defaultTextStyle) {
font: font(13px);
linkFont: font(13px);

View file

@ -12,10 +12,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/toast/toast.h"
#include "main/main_session.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
@ -42,13 +45,16 @@ public:
Options(
not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session);
not_null<Main::Session*> session,
bool chooseCorrectEnabled);
[[nodiscard]] bool isValid() const;
[[nodiscard]] rpl::producer<bool> isValidChanged() const;
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
void focusFirst();
void enableChooseCorrect(bool enabled);
[[nodiscard]] rpl::producer<int> usedCount() const;
[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
[[nodiscard]] rpl::producer<> backspaceInFront() const;
@ -56,23 +62,31 @@ public:
private:
class Option {
public:
static Option Create(
Option(
not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
int position);
int position,
std::shared_ptr<Ui::RadiobuttonGroup> group);
Option(const Option &other) = delete;
Option &operator=(const Option &other) = delete;
void toggleRemoveAlways(bool toggled);
void enableChooseCorrect(
std::shared_ptr<Ui::RadiobuttonGroup> group);
void show(anim::type animated);
void destroy(FnMut<void()> done);
//[[nodisacrd]] bool hasShadow() const;
//void destroyShadow();
[[nodisacrd]] bool hasShadow() const;
void createShadow();
void destroyShadow();
[[nodiscard]] bool isEmpty() const;
[[nodiscard]] bool isGood() const;
[[nodiscard]] bool isTooLong() const;
[[nodiscard]] bool isCorrect() const;
[[nodiscard]] bool hasFocus() const;
void setFocus() const;
void clearValue();
@ -86,29 +100,18 @@ private:
[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;
inline bool operator<(const Option &other) const {
return field() < other.field();
}
friend inline bool operator<(
const Option &option,
Ui::InputField *field) {
return option.field() < field;
}
friend inline bool operator<(
Ui::InputField *field,
const Option &option) {
return field < option.field();
}
private:
Option() = default;
void createShadow();
void createRemove();
void createWarning();
void toggleCorrectSpace(bool visible);
void updateFieldGeometry();
base::unique_qptr<Ui::SlideWrap<Ui::InputField>> _field;
base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;
not_null<Ui::RpWidget*> _content;
base::unique_qptr<Ui::FadeWrapScaled<Ui::Radiobutton>> _correct;
Ui::Animations::Simple _correctShown;
bool _hasCorrect = false;
Ui::InputField *_field = nullptr;
base::unique_qptr<Ui::PlainShadow> _shadow;
base::unique_qptr<Ui::CrossButton> _remove;
rpl::variable<bool> *_removeAlways = nullptr;
@ -116,23 +119,26 @@ private:
};
[[nodiscard]] bool full() const;
//[[nodiscard]] bool correctShadows() const;
//void fixShadows();
[[nodiscard]] bool correctShadows() const;
void fixShadows();
void removeEmptyTail();
void addEmptyOption();
void checkLastOption();
void validateState();
void fixAfterErase();
void destroy(Option &&option);
void removeDestroyed(not_null<Ui::InputField*> field);
void destroy(std::unique_ptr<Option> option);
void removeDestroyed(not_null<Option*> field);
int findField(not_null<Ui::InputField*> field) const;
[[nodiscard]] auto createChooseCorrectGroup()
-> std::shared_ptr<Ui::RadiobuttonGroup>;
not_null<QWidget*> _outer;
not_null<Ui::VerticalLayout*> _container;
const not_null<Main::Session*> _session;
std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
int _position = 0;
std::vector<Option> _list;
std::set<Option, std::less<>> _destroyed;
std::vector<std::unique_ptr<Option>> _list;
std::vector<std::unique_ptr<Option>> _destroyed;
rpl::variable<bool> _valid = false;
rpl::variable<int> _usedCount = 0;
rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
@ -187,58 +193,85 @@ void FocusAtEnd(not_null<Ui::InputField*> field) {
field->ensureCursorVisible();
}
Options::Option Options::Option::Create(
not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
int position) {
auto result = Option();
const auto field = container->insert(
position,
object_ptr<Ui::SlideWrap<Ui::InputField>>(
container,
object_ptr<Ui::InputField>(
container,
st::createPollOptionField,
Ui::InputField::Mode::NoNewlines,
tr::lng_polls_create_option_add())));
InitField(outer, field->entity(), session);
field->entity()->setMaxLength(kOptionLimit + kErrorLimit);
result._field.reset(field);
Options::Option::Option(
not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
int position,
std::shared_ptr<Ui::RadiobuttonGroup> group)
: _wrap(container->insert(
position,
object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
container,
object_ptr<Ui::RpWidget>(container))))
, _content(_wrap->entity())
, _field(
Ui::CreateChild<Ui::InputField>(
_content.get(),
st::createPollOptionField,
Ui::InputField::Mode::NoNewlines,
tr::lng_polls_create_option_add())) {
InitField(outer, _field, session);
_field->setMaxLength(kOptionLimit + kErrorLimit);
_field->show();
result.createShadow();
result.createRemove();
result.createWarning();
return result;
_wrap->hide(anim::type::instant);
_content->widthValue(
) | rpl::start_with_next([=] {
updateFieldGeometry();
}, _field->lifetime());
_field->heightValue(
) | rpl::start_with_next([=](int height) {
_content->resize(_content->width(), height);
}, _field->lifetime());
QObject::connect(_field, &Ui::InputField::changed, [=] {
Ui::PostponeCall(crl::guard(_field, [=] {
if (_hasCorrect) {
_correct->toggle(isGood(), anim::type::normal);
}
}));
});
createShadow();
createRemove();
createWarning();
enableChooseCorrect(group);
_correctShown.stop();
if (_correct) {
_correct->finishAnimating();
}
updateFieldGeometry();
}
//bool Options::Option::hasShadow() const {
// return (_shadow != nullptr);
//}
bool Options::Option::hasShadow() const {
return (_shadow != nullptr);
}
void Options::Option::createShadow() {
Expects(_field != nullptr);
Expects(_content != nullptr);
if (_shadow) {
return;
}
const auto value = Ui::CreateChild<Ui::PlainShadow>(field().get());
value->show();
_shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));
_shadow->show();
field()->sizeValue(
) | rpl::start_with_next([=](QSize size) {
const auto left = st::createPollFieldPadding.left();
value->setGeometry(
_shadow->setGeometry(
left,
size.height() - st::lineWidth,
size.width() - left,
st::lineWidth);
}, value->lifetime());
_shadow.reset(value);
}, _shadow->lifetime());
}
//void Options::Option::destroyShadow() {
// _shadow = nullptr;
//}
void Options::Option::destroyShadow() {
_shadow = nullptr;
}
void Options::Option::createRemove() {
using namespace rpl::mappers;
@ -313,6 +346,10 @@ bool Options::Option::isTooLong() const {
return (field()->getLastText().size() > kOptionLimit);
}
bool Options::Option::isCorrect() const {
return isGood() && _correct && _correct->entity()->Checkbox::checked();
}
bool Options::Option::hasFocus() const {
return field()->hasFocus();
}
@ -333,8 +370,66 @@ void Options::Option::toggleRemoveAlways(bool toggled) {
*_removeAlways = toggled;
}
void Options::Option::enableChooseCorrect(
std::shared_ptr<Ui::RadiobuttonGroup> group) {
if (!group) {
if (_correct) {
_hasCorrect = false;
_correct->hide(anim::type::normal);
toggleCorrectSpace(false);
}
return;
}
static auto Index = 0;
const auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Radiobutton>>(
_content.get(),
object_ptr<Ui::Radiobutton>(
_content.get(),
group,
++Index,
QString(),
st::defaultCheckbox));
button->entity()->resize(
button->entity()->height(),
button->entity()->height());
button->hide(anim::type::instant);
_content->sizeValue(
) | rpl::start_with_next([=](QSize size) {
const auto left = st::createPollFieldPadding.left();
button->moveToLeft(
left,
(size.height() - button->heightNoMargins()) / 2);
}, button->lifetime());
_correct.reset(button);
_hasCorrect = true;
if (isGood()) {
_correct->show(anim::type::normal);
} else {
_correct->hide(anim::type::instant);
}
toggleCorrectSpace(true);
}
void Options::Option::toggleCorrectSpace(bool visible) {
_correctShown.start(
[=] { updateFieldGeometry(); },
visible ? 0. : 1.,
visible ? 1. : 0.,
st::fadeWrapDuration);
}
void Options::Option::updateFieldGeometry() {
const auto shown = _correctShown.value(_hasCorrect ? 1. : 0.);
const auto skip = st::defaultRadio.diameter
+ st::defaultCheckbox.textPosition.x();
const auto left = anim::interpolate(0, skip, shown);
const auto width = _content->width() - left;
_field->resizeToWidth(_content->width() - left);
_field->moveToLeft(left, 0);
}
not_null<Ui::InputField*> Options::Option::field() const {
return _field->entity();
return _field;
}
void Options::Option::removePlaceholder() const {
@ -344,10 +439,12 @@ void Options::Option::removePlaceholder() const {
PollAnswer Options::Option::toPollAnswer(int index) const {
Expects(index >= 0 && index < kMaxOptionsCount);
return PollAnswer{
auto result = PollAnswer{
field()->getLastText().trimmed(),
QByteArray(1, ('0' + index))
};
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
return result;
}
rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
@ -357,10 +454,14 @@ rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
Options::Options(
not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session)
not_null<Main::Session*> session,
bool chooseCorrectEnabled)
: _outer(outer)
, _container(container)
, _session(session)
, _chooseCorrectGroup(chooseCorrectEnabled
? createChooseCorrectGroup()
: nullptr)
, _position(_container->count()) {
checkLastOption();
}
@ -390,19 +491,18 @@ rpl::producer<> Options::backspaceInFront() const {
}
void Options::Option::show(anim::type animated) {
_field->hide(anim::type::instant);
_field->show(animated);
_wrap->show(animated);
}
void Options::Option::destroy(FnMut<void()> done) {
if (anim::Disabled() || _field->isHidden()) {
if (anim::Disabled() || _wrap->isHidden()) {
Ui::PostponeCall(std::move(done));
return;
}
_field->hide(anim::type::normal);
_wrap->hide(anim::type::normal);
base::call_delayed(
st::slideWrapDuration * 2,
_field.get(),
_content.get(),
std::move(done));
}
@ -410,8 +510,8 @@ std::vector<PollAnswer> Options::toPollAnswers() const {
auto result = std::vector<PollAnswer>();
result.reserve(_list.size());
auto counter = int(0);
const auto makeAnswer = [&](const Option &option) {
return option.toPollAnswer(counter++);
const auto makeAnswer = [&](const std::unique_ptr<Option> &option) {
return option->toPollAnswer(counter++);
};
ranges::copy(
_list
@ -424,29 +524,45 @@ std::vector<PollAnswer> Options::toPollAnswers() const {
void Options::focusFirst() {
Expects(!_list.empty());
_list.front().setFocus();
_list.front()->setFocus();
}
std::shared_ptr<Ui::RadiobuttonGroup> Options::createChooseCorrectGroup() {
auto result = std::make_shared<Ui::RadiobuttonGroup>(0);
result->setChangedCallback([=](int) {
validateState();
});
return result;
}
void Options::enableChooseCorrect(bool enabled) {
_chooseCorrectGroup = enabled
? createChooseCorrectGroup()
: nullptr;
validateState();
for (auto &option : _list) {
option->enableChooseCorrect(_chooseCorrectGroup);
}
}
bool Options::correctShadows() const {
// Last one should be without shadow.
const auto noShadow = ranges::find(
_list,
true,
ranges::not_fn(&Option::hasShadow));
return (noShadow == end(_list) - 1);
}
void Options::fixShadows() {
if (correctShadows()) {
return;
}
for (auto &option : _list) {
option->createShadow();
}
_list.back()->destroyShadow();
}
//
//bool Options::correctShadows() const {
// // Last one should be without shadow if all options were used.
// const auto noShadow = ranges::find(
// _list,
// true,
// ranges::not_fn(&Option::hasShadow));
// return (noShadow == end(_list) - (full() ? 1 : 0));
//}
//
//void Options::fixShadows() {
// if (correctShadows()) {
// return;
// }
// for (auto &option : _list) {
// option.createShadow();
// }
// if (full()) {
// _list.back().destroyShadow();
// }
//}
void Options::removeEmptyTail() {
// Only one option at the end of options list can be empty.
@ -465,7 +581,7 @@ void Options::removeEmptyTail() {
return;
}
if (focusLast) {
emptyItem->setFocus();
(*emptyItem)->setFocus();
}
for (auto i = emptyItem + 1; i != end; ++i) {
destroy(std::move(*i));
@ -474,44 +590,46 @@ void Options::removeEmptyTail() {
fixAfterErase();
}
void Options::destroy(Option &&option) {
const auto field = option.field();
option.destroy([=] { removeDestroyed(field); });
_destroyed.emplace(std::move(option));
void Options::destroy(std::unique_ptr<Option> option) {
const auto value = option.get();
option->destroy([=] { removeDestroyed(value); });
_destroyed.push_back(std::move(option));
}
void Options::fixAfterErase() {
Expects(!_list.empty());
const auto last = _list.end() - 1;
last->setPlaceholder();
last->toggleRemoveAlways(false);
(*last)->setPlaceholder();
(*last)->toggleRemoveAlways(false);
if (last != begin(_list)) {
(last - 1)->setPlaceholder();
(last - 1)->toggleRemoveAlways(false);
(*(last - 1))->setPlaceholder();
(*(last - 1))->toggleRemoveAlways(false);
}
fixShadows();
}
void Options::addEmptyOption() {
if (full()) {
return;
} else if (!_list.empty() && _list.back().isEmpty()) {
} else if (!_list.empty() && _list.back()->isEmpty()) {
return;
}
if (_list.size() > 1) {
(_list.end() - 2)->removePlaceholder();
(_list.end() - 2)->toggleRemoveAlways(true);
(*(_list.end() - 2))->removePlaceholder();
(*(_list.end() - 2))->toggleRemoveAlways(true);
}
_list.push_back(Option::Create(
_list.push_back(std::make_unique<Option>(
_outer,
_container,
_session,
_position + _list.size() + _destroyed.size()));
const auto field = _list.back().field();
_position + _list.size() + _destroyed.size(),
_chooseCorrectGroup));
const auto field = _list.back()->field();
QObject::connect(field, &Ui::InputField::submitted, [=] {
const auto index = findField(field);
if (_list[index].isGood() && index + 1 < _list.size()) {
_list[index + 1].setFocus();
if (_list[index]->isGood() && index + 1 < _list.size()) {
_list[index + 1]->setFocus();
}
});
QObject::connect(field, &Ui::InputField::changed, [=] {
@ -534,25 +652,25 @@ void Options::addEmptyOption() {
const auto index = findField(field);
if (index > 0) {
_list[index - 1].setFocus();
_list[index - 1]->setFocus();
} else {
_backspaceInFront.fire({});
}
return base::EventFilterResult::Cancel;
});
_list.back().removeClicks(
) | rpl::start_with_next([=] {
_list.back()->removeClicks(
) | rpl::take(1) | rpl::start_with_next([=] {
Ui::PostponeCall(crl::guard(field, [=] {
Expects(!_list.empty());
const auto item = begin(_list) + findField(field);
if (item == _list.end() - 1) {
item->clearValue();
(*item)->clearValue();
return;
}
if (item->hasFocus()) {
(item + 1)->setFocus();
if ((*item)->hasFocus()) {
(*(item + 1))->setFocus();
}
destroy(std::move(*item));
_list.erase(item);
@ -561,21 +679,28 @@ void Options::addEmptyOption() {
}));
}, field->lifetime());
_list.back().show((_list.size() == 1)
_list.back()->show((_list.size() == 1)
? anim::type::instant
: anim::type::normal);
//fixShadows();
fixShadows();
}
void Options::removeDestroyed(not_null<Ui::InputField*> field) {
_destroyed.erase(_destroyed.find(field));
void Options::removeDestroyed(not_null<Option*> option) {
const auto i = ranges::find(
_destroyed,
option.get(),
&std::unique_ptr<Option>::get);
Assert(i != end(_destroyed));
_destroyed.erase(i);
}
void Options::validateState() {
checkLastOption();
_valid = (ranges::count_if(_list, &Option::isGood) > 1)
&& (ranges::find_if(_list, &Option::isTooLong) == end(_list));
const auto lastEmpty = !_list.empty() && _list.back().isEmpty();
&& (ranges::find_if(_list, &Option::isTooLong) == end(_list))
&& (!_chooseCorrectGroup
|| ranges::find_if(_list, &Option::isCorrect) != end(_list));
const auto lastEmpty = !_list.empty() && _list.back()->isEmpty();
_usedCount = _list.size() - (lastEmpty ? 1 : 0);
}
@ -599,8 +724,12 @@ void Options::checkLastOption() {
CreatePollBox::CreatePollBox(
QWidget*,
not_null<Main::Session*> session,
PollData::Flags chosen,
PollData::Flags disabled,
Api::SendType sendType)
: _session(session)
, _chosen(chosen)
, _disabled(disabled)
, _sendType(sendType) {
}
@ -668,11 +797,17 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
const auto question = setupQuestion(container);
AddDivider(container);
AddSkip(container);
AddSubsectionTitle(container, tr::lng_polls_create_options());
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_polls_create_options(),
st::settingsSubsectionTitle),
st::createPollFieldTitlePadding);
const auto options = lifetime().make_state<Options>(
getDelegate()->outerContainer(),
container,
_session);
_session,
(_chosen & PollData::Flag::Quiz));
auto limit = options->usedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count);
setCloseByOutsideClick(!count);
@ -684,11 +819,68 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
container->resizeToWidth(container->widthNoMargins());
});
container->add(
object_ptr<Ui::FlatLabel>(
object_ptr<Ui::DividerLabel>(
container,
std::move(limit),
st::createPollLimitLabel),
st::createPollLimitPadding);
object_ptr<Ui::FlatLabel>(
container,
std::move(limit),
st::boxDividerLabel),
st::createPollLimitPadding));
AddSkip(container);
AddSubsectionTitle(container, tr::lng_polls_create_settings());
const auto anonymous = (!(_disabled & PollData::Flag::PublicVotes))
? container->add(
object_ptr<Ui::Checkbox>(
container,
tr::lng_polls_create_anonymous(tr::now),
!(_chosen & PollData::Flag::PublicVotes),
st::defaultCheckbox),
st::createPollCheckboxMargin)
: nullptr;
const auto hasMultiple = !(_chosen & PollData::Flag::Quiz)
|| !(_disabled & PollData::Flag::Quiz);
const auto multiple = hasMultiple
? container->add(
object_ptr<Ui::Checkbox>(
container,
tr::lng_polls_create_multiple_choice(tr::now),
(_chosen & PollData::Flag::MultiChoice),
st::defaultCheckbox),
st::createPollCheckboxMargin)
: nullptr;
const auto quiz = container->add(
object_ptr<Ui::Checkbox>(
container,
tr::lng_polls_create_quiz_mode(tr::now),
(_chosen & PollData::Flag::Quiz),
st::defaultCheckbox),
st::createPollCheckboxMargin);
quiz->setDisabled(_disabled & PollData::Flag::Quiz);
if (multiple) {
multiple->setDisabled((_disabled & PollData::Flag::MultiChoice)
|| (_chosen & PollData::Flag::Quiz));
multiple->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::MouseButtonPress) && quiz->checked();
}) | rpl::start_with_next([=] {
Ui::Toast::Show("Quiz has only one right answer.");
}, multiple->lifetime());
}
using namespace rpl::mappers;
quiz->checkedChanges(
) | rpl::start_with_next([=](bool checked) {
if (multiple) {
if (checked && multiple->checked()) {
multiple->setChecked(false);
}
multiple->setDisabled(checked
|| (_disabled & PollData::Flag::MultiChoice));
}
options->enableChooseCorrect(checked);
}, quiz->lifetime());
const auto isValidQuestion = [=] {
const auto text = question->getLastText().trimmed();
@ -706,9 +898,16 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
};
const auto collectResult = [=] {
auto result = PollData(id);
using Flag = PollData::Flag;
auto result = PollData(&_session->data(), id);
result.question = question->getLastText().trimmed();
result.answers = options->toPollAnswers();
const auto publicVotes = (anonymous && !anonymous->checked());
const auto multiChoice = (multiple && multiple->checked());
result.setFlags(Flag(0)
| (publicVotes ? Flag::PublicVotes : Flag(0))
| (multiChoice ? Flag::MultiChoice : Flag(0))
| (quiz->checked() ? Flag::Quiz : Flag(0)));
return result;
};
const auto send = [=](Api::SendOptions options) {
@ -743,7 +942,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
tr::lng_polls_create_button(),
[=] { send({}); });
if (_sendType == Api::SendType::Normal) {
SetupSendMenu(
SetupSendMenuAndShortcuts(
submit.data(),
[=] { return SendMenuType::Scheduled; },
sendSilent,

View file

@ -31,6 +31,8 @@ public:
CreatePollBox(
QWidget*,
not_null<Main::Session*> session,
PollData::Flags chosen,
PollData::Flags disabled,
Api::SendType sendType);
rpl::producer<Result> submitRequests() const;
@ -47,6 +49,8 @@ private:
not_null<Ui::VerticalLayout*> container);
const not_null<Main::Session*> _session;
const PollData::Flags _chosen = PollData::Flags();
const PollData::Flags _disabled = PollData::Flags();
const Api::SendType _sendType = Api::SendType();
Fn<void()> _setInnerFocus;
Fn<rpl::producer<bool>()> _dataIsValidValue;

View file

@ -488,97 +488,30 @@ void EditCaptionBox::updateEditMediaButton() {
void EditCaptionBox::createEditMediaButton() {
const auto callback = [=](FileDialog::OpenResult &&result) {
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
return;
}
const auto isValidFile = [](QString mimeType) {
if (mimeType == qstr("image/webp")) {
Ui::show(
Box<InformBox>(tr::lng_edit_media_invalid_file(tr::now)),
Ui::LayerOption::KeepOther);
return false;
}
return true;
auto showBoxErrorCallback = [](tr::phrase<> t) {
Ui::show(Box<InformBox>(t(tr::now)), Ui::LayerOption::KeepOther);
};
if (!result.remoteContent.isEmpty()) {
auto list = Storage::PreparedList::PreparedFileFromFilesDialog(
std::move(result),
_isAlbum,
std::move(showBoxErrorCallback),
st::sendMediaPreviewSize);
auto list = Storage::PrepareMediaFromImage(
QImage(),
std::move(result.remoteContent),
st::sendMediaPreviewSize);
if (!isValidFile(list.files.front().mime)) {
return;
}
if (_isAlbum) {
const auto albumMimes = {
"image/jpeg",
"image/png",
"video/mp4",
};
const auto file = &list.files.front();
if (ranges::find(albumMimes, file->mime) == end(albumMimes)
|| file->type == Storage::PreparedFile::AlbumType::None) {
Ui::show(
Box<InformBox>(tr::lng_edit_media_album_error(tr::now)),
Ui::LayerOption::KeepOther);
return;
}
}
_preparedList = std::move(list);
} else if (!result.paths.isEmpty()) {
auto list = Storage::PrepareMediaList(
QStringList(result.paths.front()),
st::sendMediaPreviewSize);
// Don't rewrite _preparedList if new list is not valid for album.
if (_isAlbum) {
using Info = FileMediaInformation;
const auto media = &list.files.front().information->media;
const auto valid = media->match([&](const Info::Image &data) {
return Storage::ValidateThumbDimensions(
data.data.width(),
data.data.height())
&& !data.animated;
}, [&](Info::Video &data) {
data.isGifv = false;
return true;
}, [](auto &&other) {
return false;
});
if (!valid) {
Ui::show(
Box<InformBox>(tr::lng_edit_media_album_error(tr::now)),
Ui::LayerOption::KeepOther);
return;
}
}
const auto info = QFileInfo(result.paths.front());
if (!isValidFile(Core::MimeTypeForFile(info).name())) {
return;
}
_preparedList = std::move(list);
} else {
return;
if (list) {
_preparedList = std::move(*list);
updateEditPreview();
}
updateEditPreview();
};
const auto buttonCallback = [=] {
const auto filters = _isAlbum
? QStringList(qsl("Image and Video Files (*.png *.jpg *.mp4)"))
: QStringList(FileDialog::AllFilesFilter());
? FileDialog::AlbumFilesFilter()
: FileDialog::AllFilesFilter();
FileDialog::GetOpenPath(
this,
tr::lng_choose_file(tr::now),
filters.join(qsl(";;")),
filters,
crl::guard(this, callback));
};

View file

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "confirm_box.h"
#include "history/view/history_view_schedule_box.h"
#include "core/file_utilities.h"
#include "core/mime_type.h"
@ -48,8 +49,102 @@ namespace {
constexpr auto kMinPreviewWidth = 20;
constexpr auto kShrinkDuration = crl::time(150);
constexpr auto kDragDuration = crl::time(200);
const auto kStickerMimeString = qstr("image/webp");
const auto kAnimatedStickerMimeString = qstr("application/x-tgsticker");
enum class ButtonType {
Edit,
Delete,
None,
};
inline bool CanAddUrls(const QList<QUrl> &urls) {
return !urls.isEmpty() && ranges::find_if(
urls,
[](const QUrl &url) { return !url.isLocalFile(); }
) == urls.end();
}
inline bool IsFirstAlbumItem(const Storage::PreparedList &list) {
using AlbumType = Storage::PreparedFile::AlbumType;
return (list.files.size() > 0)
&& (list.files.front().type != AlbumType::None);
}
inline bool IsSingleItem(const Storage::PreparedList &list) {
return list.files.size() == 1;
}
QRect PaintAlbumThumbButtons(
Painter &p,
QPoint point,
int outerWidth,
float64 shrinkProgress) {
const auto skipInternal = st::sendBoxAlbumGroupEditInternalSkip;
const auto size = st::sendBoxAlbumGroupHeight;
const auto skipRight = st::sendBoxAlbumGroupSkipRight;
const auto skipTop = st::sendBoxAlbumGroupSkipTop;
const auto groupWidth = size * 2 + skipInternal;
// If the width is tiny, it would be better to not display the buttons.
if (groupWidth > outerWidth) {
return QRect();
}
// If the width is too small,
// it would be better to display the buttons in the center.
const auto groupX = point.x() + ((groupWidth + skipRight * 2 > outerWidth)
? (outerWidth - groupWidth) / 2
: outerWidth - skipRight - groupWidth);
const auto groupY = point.y() + skipTop;
const auto deleteLeft = skipInternal + size;
p.setOpacity(1.0 - shrinkProgress);
QRect groupRect(groupX, groupY, groupWidth, size);
App::roundRect(
p,
groupRect,
st::callFingerprintBg,
SendFilesBoxAlbumGroupCorners);
const auto editP = st::sendBoxAlbumGroupEditButtonIconPosition;
const auto deleteP = st::sendBoxAlbumGroupDeleteButtonIconPosition;
st::sendBoxAlbumGroupEditButtonIcon.paintInCenter(
p,
QRect(groupX + editP.x(), groupY + editP.y(), size, size));
st::sendBoxAlbumGroupDeleteButtonIcon.paintInCenter(
p,
QRect(
groupX + deleteLeft + deleteP.x(),
groupY + deleteP.y(),
size,
size));
p.setOpacity(1);
return groupRect;
}
void FileDialogCallback(
FileDialog::OpenResult &&result,
bool isAlbum,
Fn<void(Storage::PreparedList)> callback) {
auto showBoxErrorCallback = [](tr::phrase<> text) {
Ui::show(Box<InformBox>(text(tr::now)), Ui::LayerOption::KeepOther);
};
auto list = Storage::PreparedList::PreparedFileFromFilesDialog(
std::move(result),
isAlbum,
std::move(showBoxErrorCallback),
st::sendMediaPreviewSize);
if (!list) {
return;
}
callback(std::move(*list));
}
class SingleMediaPreview : public Ui::RpWidget {
public:
@ -123,7 +218,10 @@ class AlbumThumb {
public:
AlbumThumb(
const Storage::PreparedFile &file,
const Ui::GroupMediaLayout &layout);
const Ui::GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> editCallback,
Fn<void()> deleteCallback);
void moveToLayout(const Ui::GroupMediaLayout &layout);
void animateLayoutToInitial();
@ -141,6 +239,8 @@ public:
void paintFile(Painter &p, int left, int top, int outerWidth);
bool containsPoint(QPoint position) const;
bool buttonsContainPoint(QPoint position) const;
ButtonType buttonTypeFromPoint(QPoint position) const;
int distanceTo(QPoint position) const;
bool isPointAfter(QPoint position) const;
void moveInAlbum(QPoint to);
@ -148,6 +248,8 @@ public:
void suggestMove(float64 delta, Fn<void()> callback);
void finishAnimations();
void updateFileRow(int row);
private:
QRect countRealGeometry() const;
QRect countCurrentGeometry(float64 progress) const;
@ -173,11 +275,19 @@ private:
Ui::Animations::Simple _suggestedMoveAnimation;
int _lastShrinkValue = 0;
QRect _lastRectOfButtons;
object_ptr<Ui::IconButton> _editMedia = nullptr;
object_ptr<Ui::IconButton> _deleteMedia = nullptr;
};
AlbumThumb::AlbumThumb(
const Storage::PreparedFile &file,
const Ui::GroupMediaLayout &layout)
const Ui::GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> editCallback,
Fn<void()> deleteCallback)
: _layout(layout)
, _fullPreview(file.preview)
, _shrinkSize(int(std::ceil(st::historyMessageRadius / 1.4)))
@ -218,7 +328,11 @@ AlbumThumb::AlbumThumb(
const auto availableFileWidth = st::sendMediaPreviewSize
- st::sendMediaFileThumbSkip
- st::sendMediaFileThumbSize;
- st::sendMediaFileThumbSize
// Right buttons.
- st::sendBoxAlbumGroupButtonFile.width * 2
- st::sendBoxAlbumGroupEditInternalSkip
- st::sendBoxAlbumGroupSkipRight;
const auto filepath = file.path;
if (filepath.isEmpty()) {
_name = filedialogDefaultName(
@ -245,6 +359,45 @@ AlbumThumb::AlbumThumb(
_nameWidth = st::semiboldFont->width(_name);
}
_statusWidth = st::normalFont->width(_status);
_editMedia.create(parent, st::sendBoxAlbumGroupButtonFile);
_deleteMedia.create(parent, st::sendBoxAlbumGroupButtonFile);
const auto duration = st::historyAttach.ripple.hideDuration;
_editMedia->setClickedCallback(App::LambdaDelayed(
duration,
parent,
std::move(editCallback)));
_deleteMedia->setClickedCallback(App::LambdaDelayed(
duration,
parent,
std::move(deleteCallback)));
_editMedia->setIconOverride(&st::editMediaButtonIconFile);
_deleteMedia->setIconOverride(&st::sendBoxAlbumGroupDeleteButtonIconFile);
updateFileRow(-1);
}
void AlbumThumb::updateFileRow(int row) {
if (row < 0) {
_editMedia->hide();
_deleteMedia->hide();
return;
}
_editMedia->show();
_deleteMedia->show();
const auto fileHeight = st::sendMediaFileThumbSize
+ st::sendMediaFileThumbSkip;
const auto top = row * fileHeight + st::sendBoxAlbumGroupSkipTop;
const auto size = st::editMediaButtonSize;
auto right = st::sendBoxAlbumGroupSkipRight + size;
_deleteMedia->moveToRight(right, top);
right += st::sendBoxAlbumGroupEditInternalSkip + size;
_editMedia->moveToRight(right, top);
}
void AlbumThumb::resetLayoutAnimation() {
@ -337,6 +490,12 @@ void AlbumThumb::paintInAlbum(
}
st::historyFileThumbPlay.paintInCenter(p, inner);
}
_lastRectOfButtons = PaintAlbumThumbButtons(
p,
{ x, y },
geometry.width(),
shrinkProgress);
}
void AlbumThumb::prepareCache(QSize size, int shrink) {
@ -490,6 +649,12 @@ void AlbumThumb::paintPhoto(Painter &p, int left, int top, int outerWidth) {
top,
outerWidth,
_photo);
_lastRectOfButtons = PaintAlbumThumbButtons(
p,
{ left, top },
st::sendMediaPreviewSize,
0);
}
void AlbumThumb::paintFile(Painter &p, int left, int top, int outerWidth) {
@ -520,6 +685,19 @@ bool AlbumThumb::containsPoint(QPoint position) const {
return _layout.geometry.contains(position);
}
bool AlbumThumb::buttonsContainPoint(QPoint position) const {
return _lastRectOfButtons.contains(position);
}
ButtonType AlbumThumb::buttonTypeFromPoint(QPoint position) const {
if (!buttonsContainPoint(position)) {
return ButtonType::None;
}
return (position.x() < _lastRectOfButtons.center().x())
? ButtonType::Edit
: ButtonType::Delete;
}
int AlbumThumb::distanceTo(QPoint position) const {
const auto delta = (_layout.geometry.center() - position);
return QPoint::dotProduct(delta, delta);
@ -601,14 +779,12 @@ SingleMediaPreview *SingleMediaPreview::Create(
preview.height())) {
return nullptr;
}
const auto sticker = (file.information->filemime == kStickerMimeString)
|| (file.information->filemime == kAnimatedStickerMimeString);
return Ui::CreateChild<SingleMediaPreview>(
parent,
controller,
preview,
animated,
sticker,
Core::IsMimeSticker(file.information->filemime),
animationPreview ? file.path : QString());
}
@ -960,6 +1136,14 @@ public:
void setSendWay(SendFilesWay way);
std::vector<int> takeOrder();
auto thumbDeleted() {
return _thumbDeleted.events();
}
auto thumbChanged() {
return _thumbChanged.events();
}
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
@ -974,6 +1158,15 @@ private:
void prepareThumbs();
void updateSizeAnimated(const std::vector<Ui::GroupMediaLayout> &layout);
void updateSize();
void updateFileRows();
int thumbIndex(AlbumThumb *thumb);
AlbumThumb *thumbUnderCursor();
void deleteThumbByIndex(int index);
void changeThumbByIndex(int index);
void thumbButtonsCallback(
not_null<AlbumThumb*> thumb,
ButtonType type);
void paintAlbum(Painter &p) const;
void paintPhotos(Painter &p, QRect clip) const;
@ -1003,6 +1196,9 @@ private:
AlbumThumb *_paintedAbove = nullptr;
QPoint _draggedStartPosition;
rpl::event_stream<int> _thumbDeleted;
rpl::event_stream<int> _thumbChanged;
mutable Ui::Animations::Simple _thumbsHeightAnimation;
mutable Ui::Animations::Simple _shrinkAnimation;
mutable Ui::Animations::Simple _finishDragAnimation;
@ -1019,6 +1215,7 @@ SendFilesBox::AlbumPreview::AlbumPreview(
setMouseTracking(true);
prepareThumbs();
updateSize();
updateFileRows();
}
void SendFilesBox::AlbumPreview::setSendWay(SendFilesWay way) {
@ -1027,9 +1224,18 @@ void SendFilesBox::AlbumPreview::setSendWay(SendFilesWay way) {
_sendWay = way;
}
updateSize();
updateFileRows();
update();
}
void SendFilesBox::AlbumPreview::updateFileRows() {
Expects(_order.size() == _thumbs.size());
const auto isFile = (_sendWay == SendFilesWay::Files);
for (auto i = 0; i < _order.size(); i++) {
_thumbs[i]->updateFileRow(isFile ? _order[i] : -1);
}
}
std::vector<int> SendFilesBox::AlbumPreview::takeOrder() {
auto reordered = std::vector<std::unique_ptr<AlbumThumb>>();
reordered.reserve(_thumbs.size());
@ -1071,7 +1277,10 @@ void SendFilesBox::AlbumPreview::prepareThumbs() {
for (auto i = 0; i != count; ++i) {
_thumbs.push_back(std::make_unique<AlbumThumb>(
_list.files[i],
layout[i]));
layout[i],
this,
[=] { changeThumbByIndex(thumbIndex(thumbUnderCursor())); },
[=] { deleteThumbByIndex(thumbIndex(thumbUnderCursor())); }));
}
_thumbsHeight = countLayoutHeight(layout);
_photosHeight = ranges::accumulate(ranges::view::all(
@ -1094,9 +1303,27 @@ int SendFilesBox::AlbumPreview::contentTop() const {
AlbumThumb *SendFilesBox::AlbumPreview::findThumb(QPoint position) const {
position -= QPoint(contentLeft(), contentTop());
const auto i = ranges::find_if(_thumbs, [&](const auto &thumb) {
return thumb->containsPoint(position);
});
auto top = 0;
const auto isPhotosWay = (_sendWay == SendFilesWay::Photos);
const auto skip = isPhotosWay
? st::sendMediaPreviewPhotoSkip
: st::sendMediaFileThumbSkip;
auto find = [&](const auto &thumb) {
if (_sendWay == SendFilesWay::Album) {
return thumb->containsPoint(position);
} else if (isPhotosWay || _sendWay == SendFilesWay::Files) {
const auto bottom = top + (isPhotosWay
? thumb->photoHeight()
: st::sendMediaFileThumbSize);
const auto isUnderTop = (position.y() > top);
top = bottom + skip;
return isUnderTop && (position.y() < bottom);
}
return false;
};
const auto i = ranges::find_if(_thumbs, std::move(find));
return (i == _thumbs.end()) ? nullptr : i->get();
}
@ -1277,6 +1504,56 @@ void SendFilesBox::AlbumPreview::paintFiles(Painter &p, QRect clip) const {
}
}
int SendFilesBox::AlbumPreview::thumbIndex(AlbumThumb *thumb) {
if (!thumb) {
return -1;
}
const auto thumbIt = ranges::find_if(_thumbs, [&](auto &t) {
return t.get() == thumb;
});
Expects(thumbIt != _thumbs.end());
return std::distance(_thumbs.begin(), thumbIt);
}
AlbumThumb *SendFilesBox::AlbumPreview::thumbUnderCursor() {
return findThumb(mapFromGlobal(QCursor::pos()));
}
void SendFilesBox::AlbumPreview::deleteThumbByIndex(int index) {
if (index < 0) {
return;
}
const auto orderIt = ranges::find(_order, index);
Expects(orderIt != _order.end());
_order.erase(orderIt);
ranges::for_each(_order, [=](auto &i) {
if (i > index) {
i--;
}
});
_thumbDeleted.fire(std::move(index));
}
void SendFilesBox::AlbumPreview::changeThumbByIndex(int index) {
if (index < 0) {
return;
}
_thumbChanged.fire(std::move(index));
}
void SendFilesBox::AlbumPreview::thumbButtonsCallback(
not_null<AlbumThumb*> thumb,
ButtonType type) {
const auto index = thumbIndex(thumb);
switch (type) {
case ButtonType::None: return;
case ButtonType::Edit: changeThumbByIndex(index); break;
case ButtonType::Delete: deleteThumbByIndex(index); break;
}
}
void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
if (_finishDragAnimation.animating()) {
return;
@ -1284,6 +1561,10 @@ void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
const auto position = e->pos();
cancelDrag();
if (const auto thumb = findThumb(position)) {
if (thumb->buttonsContainPoint(e->pos())) {
thumbButtonsCallback(thumb, thumb->buttonTypeFromPoint(e->pos()));
return;
}
_paintedAbove = _suggestedThumb = _draggedThumb = thumb;
_draggedStartPosition = position;
_shrinkAnimation.start([=] { update(); }, 0., 1., kShrinkDuration);
@ -1291,19 +1572,26 @@ void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
}
void SendFilesBox::AlbumPreview::mouseMoveEvent(QMouseEvent *e) {
if (_sendWay != SendFilesWay::Album) {
if (_sendWay == SendFilesWay::Files) {
applyCursor(style::cur_default);
return;
}
if (_draggedThumb) {
const auto isAlbum = (_sendWay == SendFilesWay::Album);
if (isAlbum && _draggedThumb) {
const auto position = e->pos();
_draggedThumb->moveInAlbum(position - _draggedStartPosition);
updateSuggestedDrag(_draggedThumb->center());
update();
} else {
const auto cursor = findThumb(e->pos())
const auto thumb = findThumb(e->pos());
const auto regularCursor = isAlbum
? style::cur_sizeall
: style::cur_default;
const auto cursor = thumb
? (thumb->buttonsContainPoint(e->pos())
? style::cur_pointer
: regularCursor)
: style::cur_default;
applyCursor(cursor);
}
}
@ -1405,7 +1693,8 @@ void SendFilesBox::initPreview(rpl::producer<int> desiredPreviewHeight) {
) | rpl::start_with_next([=](int height) {
setDimensions(
st::boxWideWidth,
std::min(st::sendMediaPreviewHeightMax, height));
std::min(st::sendMediaPreviewHeightMax, height),
true);
}, lifetime());
if (_preview) {
@ -1414,7 +1703,7 @@ void SendFilesBox::initPreview(rpl::producer<int> desiredPreviewHeight) {
}
void SendFilesBox::prepareSingleFilePreview() {
Expects(_list.files.size() == 1);
Expects(IsSingleItem(_list));
const auto &file = _list.files[0];
const auto media = SingleMediaPreview::Create(this, _controller, file);
@ -1442,11 +1731,74 @@ void SendFilesBox::prepareAlbumPreview() {
this,
_list,
_sendWay->value()));
addThumbButtonHandlers(wrap);
_preview = wrap;
_albumPreview->show();
setupShadows(wrap, _albumPreview);
initPreview(_albumPreview->desiredHeightValue());
crl::on_main([=] {
wrap->scrollToY(_lastScrollTop);
_lastScrollTop = 0;
});
}
void SendFilesBox::addThumbButtonHandlers(not_null<Ui::ScrollArea*> wrap) {
_albumPreview->thumbDeleted(
) | rpl::start_with_next([=](auto index) {
_lastScrollTop = wrap->scrollTop();
_list.files.erase(_list.files.begin() + index);
applyAlbumOrder();
if (_preview) {
_preview->deleteLater();
}
_albumPreview = nullptr;
if (IsSingleItem(_list)) {
_list.albumIsPossible = false;
if (_sendWay->value() == SendFilesWay::Album) {
_sendWay->setValue(SendFilesWay::Photos);
}
}
_compressConfirm = _compressConfirmInitial;
refreshAllAfterAlbumChanges();
}, _albumPreview->lifetime());
_albumPreview->thumbChanged(
) | rpl::start_with_next([=](auto index) {
_lastScrollTop = wrap->scrollTop();
const auto callback = [=](FileDialog::OpenResult &&result) {
FileDialogCallback(
std::move(result),
true,
[=] (auto list) {
_list.files[index] = std::move(list.files.front());
applyAlbumOrder();
if (_preview) {
_preview->deleteLater();
}
_albumPreview = nullptr;
refreshAllAfterAlbumChanges();
});
};
FileDialog::GetOpenPath(
this,
tr::lng_choose_file(tr::now),
FileDialog::AlbumFilesFilter(),
crl::guard(this, callback));
}, _albumPreview->lifetime());
}
void SendFilesBox::setupShadows(
@ -1482,7 +1834,7 @@ void SendFilesBox::setupShadows(
void SendFilesBox::prepare() {
_send = addButton(tr::lng_send_button(), [=] { send({}); });
if (_sendType == Api::SendType::Normal) {
SetupSendMenu(
SetupSendMenuAndShortcuts(
_send,
[=] { return _sendMenuType; },
[=] { sendSilent(); },
@ -1497,6 +1849,49 @@ void SendFilesBox::prepare() {
_cancelledCallback();
}
}, lifetime());
const auto title = tr::lng_stickers_featured_add(tr::now) + qsl("...");
_addFileToAlbum = addLeftButton(
rpl::single(title),
App::LambdaDelayed(st::historyAttach.ripple.hideDuration, this, [=] {
openDialogToAddFileToAlbum();
}));
updateLeftButtonVisibility();
}
void SendFilesBox::updateLeftButtonVisibility() {
const auto isAlbum = _list.albumIsPossible
&& (_list.files.size() < Storage::MaxAlbumItems());
if (isAlbum || (IsSingleItem(_list) && IsFirstAlbumItem(_list))) {
_addFileToAlbum->show();
} else {
_addFileToAlbum->hide();
}
}
void SendFilesBox::refreshAllAfterAlbumChanges() {
refreshAlbumMediaCount();
preparePreview();
captionResized();
updateLeftButtonVisibility();
}
void SendFilesBox::openDialogToAddFileToAlbum() {
const auto callback = [=](FileDialog::OpenResult &&result) {
FileDialogCallback(
std::move(result),
true,
[=] (auto list) {
addFiles(std::move(list));
});
};
FileDialog::GetOpenPaths(
this,
tr::lng_choose_file(tr::now),
FileDialog::AlbumFilesFilter(),
crl::guard(this, callback));
}
void SendFilesBox::initSendWay() {
@ -1574,7 +1969,7 @@ void SendFilesBox::refreshAlbumMediaCount() {
}
void SendFilesBox::preparePreview() {
if (_list.files.size() == 1) {
if (IsSingleItem(_list)) {
prepareSingleFilePreview();
} else {
if (_list.albumIsPossible) {
@ -1611,7 +2006,7 @@ void SendFilesBox::setupSendWayControls() {
addRadio(_sendAlbum, SendFilesWay::Album, tr::lng_send_album(tr::now));
}
if (!_list.albumIsPossible || _albumPhotosCount > 0) {
addRadio(_sendPhotos, SendFilesWay::Photos, (_list.files.size() == 1)
addRadio(_sendPhotos, SendFilesWay::Photos, IsSingleItem(_list)
? tr::lng_send_photo(tr::now)
: (_albumVideosCount > 0)
? tr::lng_send_separate_photos_videos(tr::now)
@ -1619,7 +2014,7 @@ void SendFilesBox::setupSendWayControls() {
? tr::lng_send_separate_photos(tr::now)
: tr::lng_send_photos(tr::now, lt_count, _list.files.size())));
}
addRadio(_sendFiles, SendFilesWay::Files, (_list.files.size() == 1)
addRadio(_sendFiles, SendFilesWay::Files, (IsSingleItem(_list))
? tr::lng_send_file(tr::now)
: tr::lng_send_files(tr::now, lt_count, _list.files.size()));
}
@ -1746,16 +2141,9 @@ void SendFilesBox::captionResized() {
update();
}
bool SendFilesBox::canAddUrls(const QList<QUrl> &urls) const {
return !urls.isEmpty() && ranges::find_if(
urls,
[](const QUrl &url) { return !url.isLocalFile(); }
) == urls.end();
}
bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
const auto urls = data->hasUrls() ? data->urls() : QList<QUrl>();
auto filesCount = canAddUrls(urls) ? urls.size() : 0;
auto filesCount = CanAddUrls(urls) ? urls.size() : 0;
if (!filesCount && data->hasImage()) {
++filesCount;
}
@ -1764,8 +2152,7 @@ bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
return false;
} else if (_list.files.size() > 1 && !_albumPreview) {
return false;
} else if (_list.files.front().type
== Storage::PreparedFile::AlbumType::None) {
} else if (!IsFirstAlbumItem(_list)) {
return false;
}
return true;
@ -1774,7 +2161,7 @@ bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
auto list = [&] {
const auto urls = data->hasUrls() ? data->urls() : QList<QUrl>();
auto result = canAddUrls(urls)
auto result = CanAddUrls(urls)
? Storage::PrepareMediaList(urls, st::sendMediaPreviewSize)
: Storage::PreparedList(
Storage::PreparedList::Error::EmptyFile,
@ -1792,35 +2179,35 @@ bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
}
return result;
}();
if (_list.files.size() + list.files.size() > Storage::MaxAlbumItems()) {
return addFiles(std::move(list));
}
bool SendFilesBox::addFiles(Storage::PreparedList list) {
const auto sumFiles = _list.files.size() + list.files.size();
const auto cutToAlbumSize = (sumFiles > Storage::MaxAlbumItems());
if (list.error != Storage::PreparedList::Error::None) {
return false;
} else if (list.error != Storage::PreparedList::Error::None) {
} else if (!IsSingleItem(list) && !list.albumIsPossible) {
return false;
} else if (list.files.size() != 1 && !list.albumIsPossible) {
return false;
} else if (list.files.front().type
== Storage::PreparedFile::AlbumType::None) {
} else if (!IsFirstAlbumItem(list)) {
return false;
} else if (_list.files.size() > 1 && !_albumPreview) {
return false;
} else if (_list.files.front().type
== Storage::PreparedFile::AlbumType::None) {
} else if (!IsFirstAlbumItem(_list)) {
return false;
}
applyAlbumOrder();
delete base::take(_preview);
_albumPreview = nullptr;
if (_list.files.size() == 1
if (IsSingleItem(_list)
&& _sendWay->value() == SendFilesWay::Photos) {
_sendWay->setValue(SendFilesWay::Album);
}
_list.mergeToEnd(std::move(list));
_list.mergeToEnd(std::move(list), cutToAlbumSize);
_compressConfirm = _compressConfirmInitial;
refreshAlbumMediaCount();
preparePreview();
captionResized();
refreshAllAfterAlbumChanges();
return true;
}
@ -1858,7 +2245,9 @@ void SendFilesBox::updateBoxSize() {
}
void SendFilesBox::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
if (e->matches(QKeySequence::Open) && !_addFileToAlbum->isHidden()) {
openDialogToAddFileToAlbum();
} else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
const auto modifiers = e->modifiers();
const auto ctrl = modifiers.testFlag(Qt::ControlModifier)
|| modifiers.testFlag(Qt::MetaModifier);
@ -1937,7 +2326,9 @@ void SendFilesBox::setInnerFocus() {
void SendFilesBox::send(
Api::SendOptions options,
bool ctrlShiftEnter) {
if (_sendType == Api::SendType::Scheduled && !options.scheduled) {
if ((_sendType == Api::SendType::Scheduled
|| _sendType == Api::SendType::ScheduledToUser)
&& !options.scheduled) {
return sendScheduled();
}
@ -1982,9 +2373,12 @@ void SendFilesBox::sendSilent() {
}
void SendFilesBox::sendScheduled() {
const auto type = (_sendType == Api::SendType::ScheduledToUser)
? SendMenuType::ScheduledToUser
: _sendMenuType;
const auto callback = [=](Api::SendOptions options) { send(options); };
Ui::show(
HistoryView::PrepareScheduleBox(this, _sendMenuType, callback),
HistoryView::PrepareScheduleBox(this, type, callback),
Ui::LayerOption::KeepOther);
}

View file

@ -120,9 +120,15 @@ private:
void updateControlsGeometry();
void updateCaptionPlaceholder();
void addThumbButtonHandlers(not_null<Ui::ScrollArea*> wrap);
bool canAddFiles(not_null<const QMimeData*> data) const;
bool canAddUrls(const QList<QUrl> &urls) const;
bool addFiles(not_null<const QMimeData*> data);
bool addFiles(Storage::PreparedList list);
void openDialogToAddFileToAlbum();
void updateLeftButtonVisibility();
void refreshAllAfterAlbumChanges();
const not_null<Window::SessionController*> _controller;
const Api::SendType _sendType = Api::SendType();
@ -163,6 +169,9 @@ private:
int _albumVideosCount = 0;
int _albumPhotosCount = 0;
int _lastScrollTop = 0;
QPointer<Ui::RoundButton> _send;
QPointer<Ui::RoundButton> _addFileToAlbum;
};

View file

@ -413,7 +413,9 @@ void ShareBox::keyPressEvent(QKeyEvent *e) {
SendMenuType ShareBox::sendMenuType() const {
const auto selected = _inner->selected();
return (selected.size() == 1 && selected.front()->isSelf())
return ranges::all_of(selected, HistoryView::CanScheduleUntilOnline)
? SendMenuType::ScheduledToUser
: (selected.size() == 1 && selected.front()->isSelf())
? SendMenuType::Reminder
: SendMenuType::Scheduled;
}
@ -424,7 +426,7 @@ void ShareBox::createButtons() {
const auto send = addButton(tr::lng_share_confirm(), [=] {
submit({});
});
SetupSendMenu(
SetupSendMenuAndShortcuts(
send,
[=] { return sendMenuType(); },
[=] { submitSilent(); },

View file

@ -435,7 +435,9 @@ void EmojiKeywords::LangPack::applyDifference(
LangPackData &&result) {
applyData(std::move(result));
});
crl::async([=, callback = std::move(callback)]() mutable {
crl::async([=,
copy = std::move(copy),
callback = std::move(callback)]() mutable {
ApplyDifference(copy, keywords, version);
WriteLocalCache(id, copy);
crl::on_main([

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qthelp_url.h"
#include "base/event_filter.h"
#include "boxes/abstract_box.h"
#include "core/shortcuts.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/widgets/popup_menu.h"
#include "ui/ui_utility.h"
@ -33,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QGuiApplication>
#include <QtGui/QTextBlock>
#include <QtGui/QClipboard>
#include <QtWidgets/QApplication>
namespace {
@ -666,7 +668,7 @@ void MessageLinksParser::apply(
_list = std::move(parsed);
}
void SetupSendMenu(
void SetupSendMenuAndShortcuts(
not_null<Ui::RpWidget*> button,
Fn<SendMenuType()> type,
Fn<void()> silent,
@ -688,9 +690,9 @@ void SetupSendMenu(
}
if (schedule && now != SendMenuType::SilentOnly) {
(*menu)->addAction(
(now == SendMenuType::Scheduled
? tr::lng_schedule_message(tr::now)
: tr::lng_reminder_message(tr::now)),
(now == SendMenuType::Reminder
? tr::lng_reminder_message(tr::now)
: tr::lng_schedule_message(tr::now)),
schedule);
}
(*menu)->popup(QCursor::pos());
@ -702,4 +704,46 @@ void SetupSendMenu(
}
return base::EventFilterResult::Continue;
});
Shortcuts::Requests(
) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
using Command = Shortcuts::Command;
const auto now = type();
if (now == SendMenuType::Disabled
|| (!silent && now == SendMenuType::SilentOnly)) {
return;
}
(silent
&& (now != SendMenuType::Reminder)
&& request->check(Command::SendSilentMessage)
&& request->handle([=] {
silent();
return true;
}))
||
(schedule
&& (now != SendMenuType::SilentOnly)
&& request->check(Command::ScheduleMessage)
&& request->handle([=] {
schedule();
return true;
}))
||
(request->check(Command::JustSendMessage) && request->handle([=] {
const auto post = [&](QEvent::Type type) {
QApplication::postEvent(
button,
new QMouseEvent(
type,
QPointF(0, 0),
Qt::LeftButton,
Qt::LeftButton,
Qt::NoModifier));
};
post(QEvent::MouseButtonPress);
post(QEvent::MouseButtonRelease);
return true;
}));
}, button->lifetime());
}

View file

@ -99,10 +99,11 @@ enum class SendMenuType {
Disabled,
SilentOnly,
Scheduled,
ScheduledToUser, // For "Send when online".
Reminder,
};
void SetupSendMenu(
void SetupSendMenuAndShortcuts(
not_null<Ui::RpWidget*> button,
Fn<SendMenuType()> type,
Fn<void()> silent,

View file

@ -45,7 +45,6 @@ enum {
StickerMaxSize = 2048, // 2048x2048 is a max image size for sticker
MaxZoomLevel = 7, // x8
ZoomToScreenLevel = 1024, // just constant
PreloadHeightsCount = 3, // when 3 screens to scroll left make a preload request

View file

@ -295,6 +295,10 @@ QString AllFilesFilter() {
#endif // Q_OS_WIN
}
QString AlbumFilesFilter() {
return qsl("Image and Video Files (*.png *.jpg *.mp4 *.jpeg)");
}
namespace internal {
void InitLastPathDefault() {

View file

@ -82,6 +82,7 @@ void GetFolder(
Fn<void()> failed = Fn<void()>());
QString AllFilesFilter();
QString AlbumFilesFilter();
namespace internal {

View file

@ -248,10 +248,8 @@ void Launcher::init() {
QApplication::setApplicationName(qsl("KotatogramDesktop"));
#ifdef TDESKTOP_LAUNCHER_FILENAME
QApplication::setDesktopFileName(qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_FILENAME)));
#elif defined(Q_OS_LINUX) && QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
QApplication::setDesktopFileName(qsl("kotatogramdesktop.desktop"));
#if defined(Q_OS_LINUX) && QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
QApplication::setDesktopFileName(qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_BASENAME)) + ".desktop");
#endif
#ifndef OS_MAC_OLD

View file

@ -102,4 +102,9 @@ MimeType MimeTypeForData(const QByteArray &data) {
return MimeType(QMimeDatabase().mimeTypeForData(data));
}
bool IsMimeSticker(const QString &mime) {
return mime == qsl("image/webp")
|| mime == qsl("application/x-tgsticker");
}
} // namespace Core

View file

@ -39,4 +39,6 @@ MimeType MimeTypeForName(const QString &mime);
MimeType MimeTypeForFile(const QFileInfo &file);
MimeType MimeTypeForData(const QByteArray &data);
bool IsMimeSticker(const QString &mime);
} // namespace Core

View file

@ -98,12 +98,7 @@ int Sandbox::start() {
const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());
char h[33] = { 0 };
hashMd5Hex(d.constData(), d.size(), h);
#ifndef OS_MAC_STORE
_localServerName = psServerPrefix() + h + '-' + cGUIDStr();
#else // OS_MAC_STORE
h[4] = 0; // use only first 4 chars
_localServerName = psServerPrefix() + h;
#endif // OS_MAC_STORE
_localServerName = Platform::SingleInstanceLocalServerName(h);
connect(
&_localSocket,

View file

@ -77,6 +77,12 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ qsl("show_archive") , Command::ShowArchive },
// Shortcuts that have no default values.
{ qsl("message") , Command::JustSendMessage },
{ qsl("message_silently") , Command::SendSilentMessage },
{ qsl("message_scheduled"), Command::ScheduleMessage },
//
{ qsl("save_draft") , Command::SaveDraft },
};

View file

@ -37,6 +37,10 @@ enum class Command {
ShowArchive,
JustSendMessage,
SendSilentMessage,
ScheduleMessage,
SupportReloadTemplates,
SupportToggleMuted,
SupportScrollToCurrent,

View file

@ -15,8 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#define TDESKTOP_ALPHA_VERSION (0ULL)
#endif // TDESKTOP_ALLOW_CLOSED_ALPHA
constexpr auto AppVersion = 1009004;
constexpr auto AppVersionStr = "1.9.4";
constexpr auto AppVersion = 1009007;
constexpr auto AppVersionStr = "1.9.7";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
constexpr auto AppKotatoVersion = 1001003;

View file

@ -134,7 +134,6 @@ void CloudThemes::applyUpdate(const MTPTheme &theme) {
return;
}
applyFromDocument(cloud);
}, [&](const MTPDthemeDocumentNotModified &data) {
});
scheduleReload();
}
@ -160,8 +159,6 @@ void CloudThemes::resolve(
void CloudThemes::showPreview(const MTPTheme &data) {
data.match([&](const MTPDtheme &data) {
showPreview(CloudTheme::Parse(_session, data));
}, [&](const MTPDthemeDocumentNotModified &data) {
LOG(("API Error: Unexpected themeDocumentNotModified."));
});
}
@ -269,8 +266,6 @@ void CloudThemes::parseThemes(const QVector<MTPTheme> &list) {
for (const auto &theme : list) {
theme.match([&](const MTPDtheme &data) {
_list.push_back(CloudTheme::Parse(_session, data));
}, [&](const MTPDthemeDocumentNotModified &data) {
LOG(("API Error: Unexpected themeDocumentNotModified."));
});
}
checkCurrentTheme();

View file

@ -50,7 +50,6 @@ struct FileReferenceAccumulator {
if (const auto document = data.vdocument()) {
push(*document);
}
}, [&](const MTPDthemeDocumentNotModified &data) {
});
}
void push(const MTPWebPageAttribute &data) {

View file

@ -1302,6 +1302,9 @@ TextForMimeData MediaPoll::clipboardText() const {
}
QString MediaPoll::errorTextForForward(not_null<PeerData*> peer) const {
if (_poll->publicVotes() && peer->isChannel() && !peer->isMegagroup()) {
return tr::lng_restricted_send_public_polls(tr::now);
}
return Data::RestrictionError(
peer,
ChatRestriction::f_send_polls

View file

@ -754,6 +754,17 @@ int PeerData::slowmodeSecondsLeft() const {
return 0;
}
bool PeerData::canSendPolls() const {
if (const auto user = asUser()) {
return user->isBot();
} else if (const auto chat = asChat()) {
return chat->canSendPolls();
} else if (const auto channel = asChannel()) {
return channel->canSendPolls();
}
return false;
}
namespace Data {
std::vector<ChatRestrictions> ListOfRestrictions() {

View file

@ -191,6 +191,7 @@ public:
[[nodiscard]] bool canRevokeFullHistory() const;
[[nodiscard]] bool slowmodeApplied() const;
[[nodiscard]] int slowmodeSecondsLeft() const;
[[nodiscard]] bool canSendPolls() const;
[[nodiscard]] UserData *asUser();
[[nodiscard]] const UserData *asUser() const;
@ -366,7 +367,7 @@ private:
static constexpr auto kUnknownPhotoId = PhotoId(0xFFFFFFFFFFFFFFFFULL);
not_null<Data::Session*> _owner;
const not_null<Data::Session*> _owner;
ImagePtr _userpic;
PhotoId _userpicPhotoId = kUnknownPhotoId;

View file

@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_poll.h"
#include "apiwrap.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace {
@ -34,14 +36,19 @@ PollAnswer *AnswerByOption(
} // namespace
PollData::PollData(PollId id) : id(id) {
PollData::PollData(not_null<Data::Session*> owner, PollId id)
: id(id)
, _owner(owner) {
}
bool PollData::applyChanges(const MTPDpoll &poll) {
Expects(poll.vid().v == id);
const auto newQuestion = qs(poll.vquestion());
const auto newClosed = poll.is_closed();
const auto newFlags = (poll.is_closed() ? Flag::Closed : Flag(0))
| (poll.is_public_voters() ? Flag::PublicVotes : Flag(0))
| (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0))
| (poll.is_quiz() ? Flag::Quiz : Flag(0));
auto newAnswers = ranges::view::all(
poll.vanswers().v
) | ranges::view::transform([](const MTPPollAnswer &data) {
@ -56,14 +63,14 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
) | ranges::to_vector;
const auto changed1 = (question != newQuestion)
|| (closed != newClosed);
|| (_flags != newFlags);
const auto changed2 = (answers != newAnswers);
if (!changed1 && !changed2) {
return false;
}
if (changed1) {
question = newQuestion;
closed = newClosed;
_flags = newFlags;
}
if (changed2) {
std::swap(answers, newAnswers);
@ -71,6 +78,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
if (const auto current = answerByOption(old.option)) {
current->votes = old.votes;
current->chosen = old.chosen;
current->correct = old.correct;
}
}
}
@ -92,6 +100,29 @@ bool PollData::applyResults(const MTPPollResults &results) {
}
}
}
if (const auto recent = results.vrecent_voters()) {
const auto recentChanged = !ranges::equal(
recentVoters,
recent->v,
ranges::equal_to(),
&UserData::id,
&MTPint::v);
if (recentChanged) {
changed = true;
recentVoters = ranges::view::all(
recent->v
) | ranges::view::transform([&](MTPint userId) {
const auto user = _owner->user(userId.v);
return (user->loadedStatus != PeerData::NotLoaded)
? user.get()
: nullptr;
}) | ranges::view::filter([](UserData *user) {
return user != nullptr;
}) | ranges::view::transform([](UserData *user) {
return not_null<UserData*>(user);
}) | ranges::to_vector;
}
}
if (!changed) {
return false;
}
@ -104,11 +135,11 @@ bool PollData::applyResults(const MTPPollResults &results) {
void PollData::checkResultsReload(not_null<HistoryItem*> item, crl::time now) {
if (lastResultsUpdate && lastResultsUpdate + kShortPollTimeout > now) {
return;
} else if (closed) {
} else if (closed()) {
return;
}
lastResultsUpdate = now;
Auth().api().reloadPollResults(item);
_owner->session().api().reloadPollResults(item);
}
PollAnswer *PollData::answerByOption(const QByteArray &option) {
@ -137,18 +168,50 @@ bool PollData::applyResultToAnswers(
answer->chosen = voters.is_chosen();
changed = true;
}
if (answer->correct != voters.is_correct()) {
answer->correct = voters.is_correct();
changed = true;
}
} else if (const auto existing = answerByOption(option)) {
answer->chosen = existing->chosen;
answer->correct = existing->correct;
}
return changed;
});
}
void PollData::setFlags(Flags flags) {
if (_flags != flags) {
_flags = flags;
++version;
}
}
PollData::Flags PollData::flags() const {
return _flags;
}
bool PollData::voted() const {
return ranges::find(answers, true, &PollAnswer::chosen) != end(answers);
}
MTPPoll PollDataToMTP(not_null<const PollData*> poll) {
bool PollData::closed() const {
return (_flags & Flag::Closed);
}
bool PollData::publicVotes() const {
return (_flags & Flag::PublicVotes);
}
bool PollData::multiChoice() const {
return (_flags & Flag::MultiChoice);
}
bool PollData::quiz() const {
return (_flags & Flag::Quiz);
}
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
const auto convert = [](const PollAnswer &answer) {
return MTP_pollAnswer(
MTP_string(answer.text),
@ -160,9 +223,14 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll) {
poll->answers,
ranges::back_inserter(answers),
convert);
using Flag = MTPDpoll::Flag;
const auto flags = ((poll->closed() || close) ? Flag::f_closed : Flag(0))
| (poll->multiChoice() ? Flag::f_multiple_choice : Flag(0))
| (poll->publicVotes() ? Flag::f_public_voters : Flag(0))
| (poll->quiz() ? Flag::f_quiz : Flag(0));
return MTP_poll(
MTP_long(poll->id),
MTP_flags(MTPDpoll::Flag::f_closed),
MTP_flags(flags),
MTP_string(poll->question),
MTP_vector<MTPPollAnswer>(answers));
}

View file

@ -7,11 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data {
class Session;
} // namespace Data
struct PollAnswer {
QString text;
QByteArray option;
int votes = 0;
bool chosen = false;
bool correct = false;
};
inline bool operator==(const PollAnswer &a, const PollAnswer &b) {
@ -24,23 +29,39 @@ inline bool operator!=(const PollAnswer &a, const PollAnswer &b) {
}
struct PollData {
explicit PollData(PollId id);
PollData(not_null<Data::Session*> owner, PollId id);
enum class Flag {
Closed = 0x01,
PublicVotes = 0x02,
MultiChoice = 0x04,
Quiz = 0x08,
};
friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;
bool applyChanges(const MTPDpoll &poll);
bool applyResults(const MTPPollResults &results);
void checkResultsReload(not_null<HistoryItem*> item, crl::time now);
PollAnswer *answerByOption(const QByteArray &option);
const PollAnswer *answerByOption(const QByteArray &option) const;
[[nodiscard]] PollAnswer *answerByOption(const QByteArray &option);
[[nodiscard]] const PollAnswer *answerByOption(
const QByteArray &option) const;
bool voted() const;
void setFlags(Flags flags);
[[nodiscard]] Flags flags() const;
[[nodiscard]] bool voted() const;
[[nodiscard]] bool closed() const;
[[nodiscard]] bool publicVotes() const;
[[nodiscard]] bool multiChoice() const;
[[nodiscard]] bool quiz() const;
PollId id = 0;
QString question;
std::vector<PollAnswer> answers;
std::vector<not_null<UserData*>> recentVoters;
int totalVoters = 0;
bool closed = false;
QByteArray sendingVote;
std::vector<QByteArray> sendingVotes;
crl::time lastResultsUpdate = 0;
int version = 0;
@ -52,6 +73,9 @@ private:
const MTPPollAnswerVoters &result,
bool isMinResults);
not_null<Data::Session*> _owner;
Flags _flags = Flags();
};
MTPPoll PollDataToMTP(not_null<const PollData*> poll);
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close = false);

View file

@ -43,6 +43,8 @@ public:
[[nodiscard]] rpl::producer<> updates(not_null<History*> history);
[[nodiscard]] Data::MessagesSlice list(not_null<History*> history);
static constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct List {

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwidget.h"
#include "api/api_text_entities.h"
#include "core/application.h"
#include "core/mime_type.h" // Core::IsMimeSticker
#include "core/crash_reports.h" // CrashReports::SetAnnotation
#include "ui/image/image.h"
#include "ui/image/image_source.h" // Images::LocalFileSource
@ -2390,8 +2391,7 @@ not_null<DocumentData*> Session::processDocument(
case mtpc_document: {
const auto &fields = data.c_document();
const auto mime = qs(fields.vmime_type());
const auto format = (mime == qstr("image/webp")
|| mime == qstr("application/x-tgsticker"))
const auto format = Core::IsMimeSticker(mime)
? "WEBP"
: "JPG";
return document(
@ -2895,7 +2895,7 @@ void Session::gameApplyFields(
not_null<PollData*> Session::poll(PollId id) {
auto i = _polls.find(id);
if (i == _polls.cend()) {
i = _polls.emplace(id, std::make_unique<PollData>(id)).first;
i = _polls.emplace(id, std::make_unique<PollData>(this, id)).first;
}
return i->second.get();
}

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h"
#include "media/clip/media_clip_reader.h"
#include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
#include "history/history_item_components.h"
#include "base/platform/base_platform_info.h"
#include "data/data_peer.h"
@ -115,6 +116,19 @@ void activateBotCommand(
}));
} break;
case ButtonType::RequestPoll: {
hideSingleUseKeyboard(msg);
auto chosen = PollData::Flags();
auto disabled = PollData::Flags();
if (!button->data.isEmpty()) {
disabled |= PollData::Flag::Quiz;
if (button->data[0]) {
chosen |= PollData::Flag::Quiz;
}
}
Window::PeerMenuCreatePoll(msg->history()->peer, chosen, disabled);
} break;
case ButtonType::SwitchInlineSame:
case ButtonType::SwitchInline: {
if (auto m = App::main()) {

View file

@ -577,6 +577,11 @@ bool InnerWidget::elementIntersectsRange(
void InnerWidget::elementStartStickerLoop(not_null<const Element*> view) {
}
void InnerWidget::elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) {
}
void InnerWidget::saveState(not_null<SectionMemento*> memento) {
memento->setFilter(std::move(_filter));
memento->setAdmins(std::move(_admins));

View file

@ -101,6 +101,9 @@ public:
int till) override;
void elementStartStickerLoop(
not_null<const HistoryView::Element*> view) override;
void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) override;
~InnerWidget();

View file

@ -149,6 +149,8 @@ historyReceivedIcon: icon {{ "history_received", historyOutIconFg, point(2px, 4p
historyReceivedSelectedIcon: icon {{ "history_received", historyOutIconFgSelected, point(2px, 4px) }};
historyReceivedInvertedIcon: icon {{ "history_received", historyIconFgInverted, point(2px, 4px) }};
historyScheduledUntilOnlineStateSpace: 17px;
historyViewsSpace: 11px;
historyViewsWidth: 20px;
historyViewsTop: -15px;
@ -539,9 +541,9 @@ historyPollQuestionStyle: TextStyle(defaultTextStyle) {
}
historyPollAnswerStyle: defaultTextStyle;
historyPollQuestionTop: 7px;
historyPollSubtitleSkip: 2px;
historyPollSubtitleSkip: 4px;
historyPollAnswerPadding: margins(31px, 10px, 0px, 10px);
historyPollAnswersSkip: 3px;
historyPollAnswersSkip: 2px;
historyPollPercentFont: semiboldFont;
historyPollPercentSkip: 6px;
historyPollPercentTop: 0px;
@ -575,6 +577,17 @@ historyPollRippleOut: RippleAnimation(defaultRippleAnimation) {
color: msgWaveformOutInactive;
}
historyPollRippleOpacity: 0.3;
historyPollRecentVotersSkip: 4px;
historyPollRecentVoterSize: 18px;
historyPollRecentVoterSkip: 13px;
historyPollBottomButtonSkip: 15px;
historyPollBottomButtonTop: 4px;
historyPollChoiceRight: icon {{ "poll_choice_right", activeButtonFg }};
historyPollChoiceWrong: icon {{ "poll_choice_wrong", activeButtonFg }};
historyPollOutChosen: icon {{ "poll_select_check", historyFileOutIconFg }};
historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgSelected }};
historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }};
historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }};
boxAttachEmoji: IconButton(historyAttachEmoji) {
width: 30px;

View file

@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/inactive_press.h"
#include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
#include "window/window_controller.h"
#include "boxes/confirm_box.h"
#include "boxes/report_box.h"
#include "boxes/sticker_set_box.h"
@ -1719,8 +1720,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
if (const auto media = item->media()) {
if (const auto poll = media->poll()) {
if (!poll->closed) {
if (poll->voted()) {
if (!poll->closed()) {
if (poll->voted() && !poll->quiz()) {
_menu->addAction(tr::lng_polls_retract(tr::now), [=] {
session().api().sendPollVotes(itemId, {});
});
@ -2418,6 +2419,12 @@ void HistoryInner::elementStartStickerLoop(
_animatedStickersPlayed.emplace(view->data());
}
void HistoryInner::elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) {
_controller->showPollResults(poll, context);
}
auto HistoryInner::getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState {
auto result = HistoryView::TopBarWidget::SelectedState {};
@ -3278,6 +3285,13 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
Instance->elementStartStickerLoop(view);
}
}
void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) override {
if (Instance) {
Instance->elementShowPollResults(poll, context);
}
}
};

View file

@ -81,6 +81,9 @@ public:
int from,
int till) const;
void elementStartStickerLoop(not_null<const Element*> view);
void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context);
void updateBotInfo(bool recount = true);

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "core/crash_reports.h"
#include "base/unixtime.h"
#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
#include "data/data_session.h"
#include "data/data_messages.h"
#include "data/data_media_types.h"
@ -862,6 +863,21 @@ QDateTime ItemDateTime(not_null<const HistoryItem*> item) {
return base::unixtime::parse(item->date());
}
QString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline) {
const auto dateText = langDayOfMonthFull(ItemDateTime(item).date());
return !item->isScheduled()
? dateText
: isUntilOnline
? tr::lng_scheduled_date_until_online(tr::now)
: tr::lng_scheduled_date(tr::now, lt_date, dateText);
}
bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {
return item->isScheduled()
&& (item->date() ==
Data::ScheduledMessages::kScheduledUntilOnlineTimestamp);
}
ClickHandlerPtr goToMessageClickHandler(
not_null<HistoryItem*> item,
FullMsgId returnToId) {

View file

@ -374,6 +374,8 @@ private:
};
QDateTime ItemDateTime(not_null<const HistoryItem*> item);
QString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline);
bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item);
ClickHandlerPtr goToMessageClickHandler(
not_null<PeerData*> peer,

View file

@ -899,6 +899,21 @@ void HistoryMessageReplyMarkup::createFromButtonRows(
}, [&](const MTPDinputKeyboardButtonUrlAuth &data) {
LOG(("API Error: inputKeyboardButtonUrlAuth received."));
// Should not get those for the users.
}, [&](const MTPDkeyboardButtonRequestPoll &data) {
const auto quiz = [&] {
if (!data.vquiz()) {
return QByteArray();
}
return data.vquiz()->match([&](const MTPDboolTrue&) {
return QByteArray(1, 1);
}, [&](const MTPDboolFalse&) {
return QByteArray(1, 0);
});
}();
row.emplace_back(
Type::RequestPoll,
qs(data.vtext()),
quiz);
});
}
if (!row.empty()) {

View file

@ -165,6 +165,7 @@ struct HistoryMessageMarkupButton {
Callback,
RequestPhone,
RequestLocation,
RequestPoll,
SwitchInline,
SwitchInlineSame,
Game,

View file

@ -314,7 +314,7 @@ HistoryWidget::HistoryWidget(
_fieldBarCancel->addClickHandler([=] { cancelFieldAreaState(); });
_send->addClickHandler([=] { sendButtonClicked(); });
SetupSendMenu(
SetupSendMenuAndShortcuts(
_send,
[=] { return sendButtonMenuType(); },
[=] { sendSilent(); },
@ -3066,6 +3066,8 @@ SendMenuType HistoryWidget::sendMenuType() const {
? SendMenuType::Disabled
: _peer->isSelf()
? SendMenuType::Reminder
: HistoryView::CanScheduleUntilOnline(_peer)
? SendMenuType::ScheduledToUser
: SendMenuType::Scheduled;
}
@ -6900,7 +6902,13 @@ void HistoryWidget::drawPinnedBar(Painter &p) {
}
p.setPen(st::historyReplyNameFg);
p.setFont(st::msgServiceNameFont);
p.drawText(left, top + st::msgServiceNameFont->ascent, (media && media->poll()) ? tr::lng_pinned_poll(tr::now) : tr::lng_pinned_message(tr::now));
const auto poll = media ? media->poll() : nullptr;
const auto pinnedHeader = !poll
? tr::lng_pinned_message(tr::now)
: poll->quiz()
? tr::lng_pinned_quiz(tr::now)
: tr::lng_pinned_poll(tr::now);
p.drawText(left, top + st::msgServiceNameFont->ascent, pinnedHeader);
p.setPen(st::historyComposeAreaFg);
p.setTextPalette(st::historyComposeAreaPalette);

View file

@ -94,6 +94,11 @@ void SimpleElementDelegate::elementStartStickerLoop(
not_null<const Element*> view) {
}
void SimpleElementDelegate::elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) {
}
TextSelection UnshiftItemSelection(
TextSelection selection,
uint16 byLength) {
@ -180,8 +185,8 @@ void UnreadBar::paint(Painter &p, int y, int w) const {
}
void DateBadge::init(const QDateTime &date) {
text = langDayOfMonthFull(date.date());
void DateBadge::init(const QString &date) {
text = date;
width = st::msgServiceFont->width(text);
}
@ -202,7 +207,8 @@ Element::Element(
not_null<HistoryItem*> data)
: _delegate(delegate)
, _data(data)
, _dateTime(ItemDateTime(data))
, _isScheduledUntilOnline(IsItemScheduledUntilOnline(data))
, _dateTime(_isScheduledUntilOnline ? QDateTime() : ItemDateTime(data))
, _context(delegate->elementContext()) {
history()->owner().registerItemView(this);
refreshMedia();
@ -504,7 +510,7 @@ void Element::setDisplayDate(bool displayDate) {
const auto item = data();
if (displayDate && !Has<DateBadge>()) {
AddComponents(DateBadge::Bit());
Get<DateBadge>()->init(dateTime());
Get<DateBadge>()->init(ItemDateText(item, _isScheduledUntilOnline));
setPendingResize();
} else if (!displayDate && Has<DateBadge>()) {
RemoveComponents(DateBadge::Bit());

View file

@ -51,6 +51,9 @@ public:
int from,
int till) = 0;
virtual void elementStartStickerLoop(not_null<const Element*> view) = 0;
virtual void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) = 0;
};
@ -71,6 +74,9 @@ public:
int from,
int till) override;
void elementStartStickerLoop(not_null<const Element*> view) override;
void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) override;
};
@ -116,7 +122,7 @@ struct UnreadBar : public RuntimeComponent<UnreadBar, Element> {
// Any HistoryView::Element can have this Component for
// displaying the day mark above the message.
struct DateBadge : public RuntimeComponent<DateBadge, Element> {
void init(const QDateTime &date);
void init(const QString &date);
int height() const;
void paint(Painter &p, int y, int w) const;
@ -305,6 +311,7 @@ private:
const not_null<ElementDelegate*> _delegate;
const not_null<HistoryItem*> _data;
std::unique_ptr<Media> _media;
bool _isScheduledUntilOnline = false;
const QDateTime _dateTime;
int _y = 0;

View file

@ -1153,6 +1153,11 @@ bool ListWidget::elementIntersectsRange(
void ListWidget::elementStartStickerLoop(not_null<const Element*> view) {
}
void ListWidget::elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) {
}
void ListWidget::saveState(not_null<ListMemento*> memento) {
memento->setAroundPosition(_aroundPosition);
auto state = countScrollState();
@ -1386,7 +1391,7 @@ void ListWidget::paintEvent(QPaintEvent *e) {
} else {
ServiceMessagePainter::paintDate(
p,
view->dateTime(),
ItemDateText(view->data(), IsItemScheduledUntilOnline(view->data())),
dateY,
width);
}

View file

@ -197,6 +197,9 @@ public:
int from,
int till) override;
void elementStartStickerLoop(not_null<const Element*> view) override;
void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) override;
~ListWidget();

View file

@ -1269,7 +1269,12 @@ int Message::infoWidth() const {
result += st::historySendStateSpace;
}
}
if (hasOutLayout()) {
// When message is scheduled until online, time is not displayed,
// so message should have less space.
if (!item->_timeWidth) {
result += st::historyScheduledUntilOnlineStateSpace;
} else if (hasOutLayout()) {
result += st::historySendStateSpace;
}
return result;

View file

@ -8,17 +8,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_schedule_box.h"
#include "api/api_common.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "boxes/calendar_box.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/padding_wrap.h"
#include "chat_helpers/message_field.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_history.h"
#include <QGuiApplication>
namespace HistoryView {
namespace {
@ -535,20 +542,41 @@ void TimeInput::startBorderAnimation() {
}
}
void FillSendUntilOnlineMenu(
not_null<Ui::IconButton*> button,
Fn<void()> callback) {
const auto menu = std::make_shared<base::unique_qptr<Ui::PopupMenu>>();
button->setClickedCallback([=] {
*menu = base::make_unique_q<Ui::PopupMenu>(button);
(*menu)->addAction(
tr::lng_scheduled_send_until_online(tr::now),
std::move(callback));
(*menu)->popup(QCursor::pos());
return true;
});
}
} // namespace
TimeId DefaultScheduleTime() {
return base::unixtime::now() + 600;
}
bool CanScheduleUntilOnline(not_null<PeerData*> peer) {
return !peer->isSelf()
&& peer->isUser()
&& !peer->asUser()->isBot()
&& (peer->asUser()->onlineTill > 0);
}
void ScheduleBox(
not_null<Ui::GenericBox*> box,
SendMenuType type,
FnMut<void(Api::SendOptions)> done,
TimeId time) {
box->setTitle((type == SendMenuType::Scheduled)
? tr::lng_schedule_title()
: tr::lng_remind_title());
box->setTitle((type == SendMenuType::Reminder)
? tr::lng_remind_title()
: tr::lng_schedule_title());
box->setWidth(st::boxWideWidth);
const auto date = Ui::CreateChild<rpl::variable<QDate>>(
@ -637,10 +665,15 @@ void ScheduleBox(
}
return result;
};
const auto save = [=](bool silent) {
const auto save = [=](bool silent, bool untilOnline = false) {
// Pro tip: Hold Ctrl key to send a silent scheduled message!
auto ctrl =
(QGuiApplication::keyboardModifiers() == Qt::ControlModifier);
auto result = Api::SendOptions();
result.silent = silent;
result.scheduled = collect();
result.silent = silent || ctrl;
result.scheduled = untilOnline
? Data::ScheduledMessages::kScheduledUntilOnlineTimestamp
: collect();
if (!result.scheduled) {
return;
}
@ -658,12 +691,20 @@ void ScheduleBox(
const auto submit = box->addButton(tr::lng_settings_save(), [=] {
save(false);
});
SetupSendMenu(
SetupSendMenuAndShortcuts(
submit.data(),
[=] { return SendMenuType::SilentOnly; },
[=] { save(true); },
nullptr);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
if (type == SendMenuType::ScheduledToUser) {
const auto sendUntilOnline = box->addTopButton(st::infoTopBarMenu);
FillSendUntilOnlineMenu(
sendUntilOnline.data(),
[=] { save(false, true); });
}
}
} // namespace HistoryView

View file

@ -18,6 +18,7 @@ enum class SendMenuType;
namespace HistoryView {
[[nodiscard]] TimeId DefaultScheduleTime();
[[nodiscard]] bool CanScheduleUntilOnline(not_null<PeerData*> peer);
void ScheduleBox(
not_null<Ui::GenericBox*> box,
SendMenuType type,

View file

@ -240,7 +240,9 @@ bool ScheduledWidget::confirmSendingFiles(
text,
boxCompressConfirm,
_history->peer->slowmodeApplied() ? SendLimit::One : SendLimit::Many,
Api::SendType::Scheduled,
CanScheduleUntilOnline(_history->peer)
? Api::SendType::ScheduledToUser
: Api::SendType::Scheduled,
SendMenuType::Disabled);
//_field->setTextWithTags({});
@ -545,6 +547,8 @@ void ScheduledWidget::sendInlineResult(
SendMenuType ScheduledWidget::sendMenuType() const {
return _history->peer->isSelf()
? SendMenuType::Reminder
: HistoryView::CanScheduleUntilOnline(_history->peer)
? SendMenuType::ScheduledToUser
: SendMenuType::Scheduled;
}

View file

@ -199,6 +199,10 @@ void ServiceMessagePainter::paintDate(Painter &p, const QDateTime &date, int y,
paintPreparedDate(p, dateText, dateTextWidth, y, w);
}
void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int y, int w) {
paintPreparedDate(p, dateText, st::msgServiceFont->width(dateText), y, w);
}
void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w) {
paintPreparedDate(p, dateText, dateTextWidth, y, w);
}

View file

@ -63,6 +63,7 @@ struct PaintContext {
class ServiceMessagePainter {
public:
static void paintDate(Painter &p, const QDateTime &date, int y, int w);
static void paintDate(Painter &p, const QString &dateText, int y, int w);
static void paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w);
static void paintBubble(Painter &p, int x, int y, int w, int h);

View file

@ -30,6 +30,35 @@ namespace {
constexpr auto kAudioVoiceMsgUpdateView = crl::time(100);
[[nodiscard]] QString CleanTagSymbols(const QString &value) {
auto result = QString();
const auto begin = value.begin(), end = value.end();
auto from = begin;
for (auto ch = begin; ch != end; ++ch) {
if (ch->isHighSurrogate()
&& (ch + 1) != end
&& (ch + 1)->isLowSurrogate()
&& QChar::surrogateToUcs4(
ch->unicode(),
(ch + 1)->unicode()) >= 0xe0000) {
if (ch > from) {
if (result.isEmpty()) {
result.reserve(value.size());
}
result.append(from, ch - from);
}
++ch;
from = ch + 1;
}
}
if (from == begin) {
return value;
} else if (end > from) {
result.append(from, end - from);
}
return result;
}
} // namespace
Document::Document(
@ -104,7 +133,8 @@ void Document::createComponents(bool caption) {
}
void Document::fillNamedFromData(HistoryDocumentNamed *named) {
const auto nameString = named->_name = _data->composeNameString();
const auto nameString = named->_name = CleanTagSymbols(
_data->composeNameString());
named->_namew = st::semiboldFont->width(nameString);
}

View file

@ -263,7 +263,7 @@ bool Gif::downloadInCorner() const {
&& (_data->loading() || !autoplayEnabled())
&& _data->canBeStreamed()
&& !_data->inappPlaybackFailed()
&& IsServerMsgId(_parent->data()->id);
&& !_parent->data()->isSending();
}
bool Gif::autoplayEnabled() const {
@ -431,19 +431,22 @@ void Gif::draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms
} else {
p.drawPixmap(rthumb.topLeft(), normal->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners));
}
} else if (const auto blurred = _data->thumbnailInline()) {
p.drawPixmap(rthumb.topLeft(), blurred->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners));
} else if (!isRound) {
const auto roundTop = (roundCorners & RectPart::TopLeft);
const auto roundBottom = (roundCorners & RectPart::BottomLeft);
const auto margin = inWebPage
? st::buttonRadius
: st::historyMessageRadius;
const auto parts = roundCorners
| RectPart::NoTopBottom
| (roundTop ? RectPart::Top : RectPart::None)
| (roundBottom ? RectPart::Bottom : RectPart::None);
App::roundRect(p, rthumb.marginsAdded({ 0, roundTop ? 0 : margin, 0, roundBottom ? 0 : margin }), st::imageBg, roundRadius, parts);
} else {
_data->loadThumbnail(_realParent->fullId());
if (const auto blurred = _data->thumbnailInline()) {
p.drawPixmap(rthumb.topLeft(), blurred->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners));
} else if (!isRound) {
const auto roundTop = (roundCorners & RectPart::TopLeft);
const auto roundBottom = (roundCorners & RectPart::BottomLeft);
const auto margin = inWebPage
? st::buttonRadius
: st::historyMessageRadius;
const auto parts = roundCorners
| RectPart::NoTopBottom
| (roundTop ? RectPart::Top : RectPart::None)
| (roundBottom ? RectPart::Bottom : RectPart::None);
App::roundRect(p, rthumb.marginsAdded({ 0, roundTop ? 0 : margin, 0, roundBottom ? 0 : margin }), st::imageBg, roundRadius, parts);
}
}
}
}
@ -807,7 +810,7 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
if (QRect(usex + paintx, painty, usew, painth).contains(point)) {
result.link = _data->uploading()
? _cancell
: !IsServerMsgId(_realParent->id)
: _realParent->isSending()
? nullptr
: (_data->loaded() || _data->canBePlayed())
? _openl
@ -1098,7 +1101,7 @@ TextState Gif::getStateGrouped(
}
return TextState(_parent, _data->uploading()
? _cancell
: !IsServerMsgId(_realParent->id)
: _realParent->isSending()
? nullptr
: (_data->loaded() || _data->canBePlayed())
? _openl
@ -1279,6 +1282,7 @@ void Gif::parentTextUpdated() {
}
void Gif::refreshParentId(not_null<HistoryItem*> realParent) {
File::refreshParentId(realParent);
if (_parent->media() == this) {
refreshCaption();
}

View file

@ -14,11 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h"
#include "calls/calls_instance.h"
#include "ui/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/effects/ripple_animation.h"
#include "data/data_media_types.h"
#include "data/data_poll.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "layout.h"
#include "main/main_session.h"
@ -29,6 +31,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace HistoryView {
namespace {
constexpr auto kShowRecentVotersCount = 3;
struct PercentCounterItem {
int index = 0;
int percent = 0;
@ -112,6 +116,8 @@ struct Poll::AnswerAnimation {
anim::value percent;
anim::value filling;
anim::value opacity;
bool chosen = false;
bool correct = false;
};
struct Poll::AnswersAnimation {
@ -132,7 +138,7 @@ struct Poll::SendingAnimation {
struct Poll::Answer {
Answer();
void fillText(const PollAnswer &original);
void fillData(not_null<PollData*> poll, const PollAnswer &original);
Ui::Text::String text;
QByteArray option;
@ -142,7 +148,10 @@ struct Poll::Answer {
float64 filling = 0.;
QString votesPercentString;
bool chosen = false;
bool correct = false;
bool selected = false;
ClickHandlerPtr handler;
Ui::Animations::Simple selectedAnimation;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
};
@ -159,7 +168,11 @@ Poll::SendingAnimation::SendingAnimation(
Poll::Answer::Answer() : text(st::msgMinWidth / 2) {
}
void Poll::Answer::fillText(const PollAnswer &original) {
void Poll::Answer::fillData(
not_null<PollData*> poll,
const PollAnswer &original) {
chosen = original.chosen;
correct = poll->quiz() ? original.correct : chosen;
if (!text.isEmpty() && text.toString() == original.text) {
return;
}
@ -174,7 +187,15 @@ Poll::Poll(
not_null<PollData*> poll)
: Media(parent)
, _poll(poll)
, _question(st::msgMinWidth / 2) {
, _question(st::msgMinWidth / 2)
, _showResultsLink(
std::make_shared<LambdaClickHandler>(crl::guard(
this,
[=] { showResults(); })))
, _sendVotesLink(
std::make_shared<LambdaClickHandler>(crl::guard(
this,
[=] { sendMultiOptions(); }))) {
history()->owner().registerPollView(_poll, _parent);
}
@ -202,13 +223,17 @@ QSize Poll::countOptimalSize() {
+ st::historyPollAnswerPadding.bottom();
}), 0);
const auto bottomButtonHeight = inlineFooter()
? 0
: st::historyPollBottomButtonSkip;
auto minHeight = st::historyPollQuestionTop
+ _question.minHeight()
+ st::historyPollSubtitleSkip
+ st::msgDateFont->height
+ st::historyPollAnswersSkip
+ answersHeight
+ st::msgPadding.bottom()
+ st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height
+ st::msgPadding.bottom();
if (!isBubbleTop()) {
@ -218,13 +243,28 @@ QSize Poll::countOptimalSize() {
}
bool Poll::showVotes() const {
return _voted || _closed;
return _voted || (_flags & PollData::Flag::Closed);
}
bool Poll::canVote() const {
return !showVotes() && IsServerMsgId(_parent->data()->id);
}
bool Poll::canSendVotes() const {
return canVote() && _hasSelected;
}
bool Poll::showVotersCount() const {
return showVotes()
? (!_totalVotes || !(_flags & PollData::Flag::PublicVotes))
: !(_flags & PollData::Flag::MultiChoice);
}
bool Poll::inlineFooter() const {
return !(_flags
& (PollData::Flag::PublicVotes | PollData::Flag::MultiChoice));
}
int Poll::countAnswerTop(
const Answer &answer,
int innerWidth) const {
@ -278,6 +318,9 @@ QSize Poll::countCurrentSize(int newWidth) {
return countAnswerHeight(answer, innerWidth);
}), 0);
const auto bottomButtonHeight = inlineFooter()
? 0
: st::historyPollBottomButtonSkip;
auto newHeight = st::historyPollQuestionTop
+ _question.countHeight(innerWidth)
+ st::historyPollSubtitleSkip
@ -285,6 +328,7 @@ QSize Poll::countCurrentSize(int newWidth) {
+ st::historyPollAnswersSkip
+ answersHeight
+ st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height
+ st::msgPadding.bottom();
if (!isBubbleTop()) {
@ -309,13 +353,22 @@ void Poll::updateTexts() {
_poll->question,
options);
}
if (_closed != _poll->closed || _subtitle.isEmpty()) {
_closed = _poll->closed;
if (_flags != _poll->flags() || _subtitle.isEmpty()) {
using Flag = PollData::Flag;
_flags = _poll->flags();
_subtitle.setText(
st::msgDateTextStyle,
_closed ? tr::lng_polls_closed(tr::now) : tr::lng_polls_anonymous(tr::now));
((_flags & Flag::Closed)
? tr::lng_polls_closed(tr::now)
: (_flags & Flag::Quiz)
? ((_flags & Flag::PublicVotes)
? tr::lng_polls_public_quiz(tr::now)
: tr::lng_polls_anonymous_quiz(tr::now))
: ((_flags & Flag::PublicVotes)
? tr::lng_polls_public(tr::now)
: tr::lng_polls_anonymous(tr::now))));
}
updateRecentVoters();
updateAnswers();
updateVotes();
@ -324,6 +377,16 @@ void Poll::updateTexts() {
}
}
void Poll::updateRecentVoters() {
auto &&sliced = ranges::view::all(
_poll->recentVoters
) | ranges::view::take(kShowRecentVotersCount);
const auto changed = !ranges::equal(_recentVoters, sliced);
if (changed) {
_recentVoters = sliced | ranges::to_vector;
}
}
void Poll::updateAnswers() {
const auto changed = !ranges::equal(
_answers,
@ -334,16 +397,16 @@ void Poll::updateAnswers() {
if (!changed) {
auto &&answers = ranges::view::zip(_answers, _poll->answers);
for (auto &&[answer, original] : answers) {
answer.fillText(original);
answer.fillData(_poll, original);
}
return;
}
_answers = ranges::view::all(
_poll->answers
) | ranges::view::transform([](const PollAnswer &answer) {
) | ranges::view::transform([&](const PollAnswer &answer) {
auto result = Answer();
result.option = answer.option;
result.fillText(answer);
result.fillData(_poll, answer);
return result;
}) | ranges::to_vector;
@ -355,12 +418,66 @@ void Poll::updateAnswers() {
}
ClickHandlerPtr Poll::createAnswerClickHandler(
const Answer &answer) const {
const Answer &answer) {
const auto option = answer.option;
const auto itemId = _parent->data()->fullId();
return std::make_shared<LambdaClickHandler>([=] {
history()->session().api().sendPollVotes(itemId, { option });
});
if (_flags & PollData::Flag::MultiChoice) {
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleMultiOption(option);
}));
}
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
history()->session().api().sendPollVotes(
_parent->data()->fullId(),
{ option });
}));
}
void Poll::toggleMultiOption(const QByteArray &option) {
const auto i = ranges::find(
_answers,
option,
&Answer::option);
if (i != end(_answers)) {
const auto selected = i->selected;
i->selected = !selected;
i->selectedAnimation.start(
[=] { history()->owner().requestViewRepaint(_parent); },
selected ? 1. : 0.,
selected ? 0. : 1.,
st::defaultCheck.duration);
if (selected) {
const auto j = ranges::find(
_answers,
true,
&Answer::selected);
_hasSelected = (j != end(_answers));
} else {
_hasSelected = true;
}
history()->owner().requestViewRepaint(_parent);
}
}
void Poll::sendMultiOptions() {
auto chosen = _answers | ranges::view::filter(
&Answer::selected
) | ranges::view::transform(
&Answer::option
) | ranges::to_vector;
if (!chosen.empty()) {
for (auto &answer : _answers) {
answer.selected = false;
}
history()->session().api().sendPollVotes(
_parent->data()->fullId(),
std::move(chosen));
}
}
void Poll::showResults() {
_parent->delegate()->elementShowPollResults(
_poll,
_parent->data()->fullId());
}
void Poll::updateVotes() {
@ -370,21 +487,23 @@ void Poll::updateVotes() {
}
void Poll::checkSendingAnimation() const {
const auto &sending = _poll->sendingVote;
if (sending.isEmpty() == !_sendingAnimation) {
const auto &sending = _poll->sendingVotes;
const auto sendingRadial = (sending.size() == 1)
&& !(_flags & PollData::Flag::MultiChoice);
if (sendingRadial == (_sendingAnimation != nullptr)) {
if (_sendingAnimation) {
_sendingAnimation->option = sending;
_sendingAnimation->option = sending.front();
}
return;
}
if (sending.isEmpty()) {
if (!sendingRadial) {
if (!_answersAnimation) {
_sendingAnimation = nullptr;
}
return;
}
_sendingAnimation = std::make_unique<SendingAnimation>(
sending,
sending.front(),
[=] { radialAnimationCallback(); });
_sendingAnimation->animation.start();
}
@ -394,9 +513,17 @@ void Poll::updateTotalVotes() {
return;
}
_totalVotes = _poll->totalVoters;
const auto quiz = _poll->quiz();
const auto string = !_totalVotes
? tr::lng_polls_votes_none(tr::now)
: tr::lng_polls_votes_count(tr::now, lt_count_short, _totalVotes);
? (quiz
? tr::lng_polls_answers_none
: tr::lng_polls_votes_none)(tr::now)
: (quiz
? tr::lng_polls_answers_count
: tr::lng_polls_votes_count)(
tr::now,
lt_count_short,
_totalVotes);
_totalVotesLabel.setText(st::msgDateTextStyle, string);
}
@ -485,6 +612,7 @@ void Poll::draw(Painter &p, const QRect &r, TextSelection selection, crl::time m
p.setPen(regular);
_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());
paintRecentVoters(p, padding.left() + _subtitle.maxWidth(), tshift, selection);
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
const auto progress = _answersAnimation
@ -517,23 +645,80 @@ void Poll::draw(Painter &p, const QRect &r, TextSelection selection, crl::time m
selection);
tshift += height;
}
if (!_totalVotesLabel.isEmpty()) {
if (!inlineFooter()) {
paintBottom(p, padding.left(), tshift, paintw, selection);
} else if (!_totalVotesLabel.isEmpty()) {
tshift += st::msgPadding.bottom();
paintInlineFooter(p, padding.left(), tshift, paintw, selection);
}
}
void Poll::paintInlineFooter(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const {
const auto selected = (selection == FullSelection);
const auto outbg = _parent->hasOutLayout();
const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
p.setPen(regular);
_totalVotesLabel.drawLeftElided(
p,
left,
top,
std::min(
_totalVotesLabel.maxWidth(),
paintw - _parent->infoWidth()),
width());
}
void Poll::paintBottom(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const {
const auto stringtop = top + st::msgPadding.bottom() + st::historyPollBottomButtonTop;
const auto selected = (selection == FullSelection);
const auto outbg = _parent->hasOutLayout();
const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
if (showVotersCount()) {
p.setPen(regular);
_totalVotesLabel.drawLeftElided(
p,
padding.left(),
tshift,
std::min(
_totalVotesLabel.maxWidth(),
paintw - _parent->infoWidth()),
width());
_totalVotesLabel.draw(p, left, stringtop, paintw, style::al_top);
} else {
const auto link = showVotes()
? _showResultsLink
: canSendVotes()
? _sendVotesLink
: nullptr;
if (_linkRipple) {
const auto linkHeight = bottomButtonHeight();
p.setOpacity(st::historyPollRippleOpacity);
_linkRipple->paint(p, left - st::msgPadding.left(), height() - linkHeight, width());
if (_linkRipple->empty()) {
_linkRipple.reset();
}
p.setOpacity(1.);
}
p.setFont(st::semiboldFont);
if (!link) {
p.setPen(regular);
} else {
p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg));
}
const auto string = showVotes()
? tr::lng_polls_view_results(tr::now, Ui::Text::Upper)
: tr::lng_polls_submit_votes(tr::now, Ui::Text::Upper);
const auto stringw = st::semiboldFont->width(string);
p.drawTextLeft(left + (paintw - stringw) / 2, stringtop, width(), string, stringw);
}
}
void Poll::resetAnswersAnimation() const {
_answersAnimation = nullptr;
if (_poll->sendingVote.isEmpty()) {
if (_poll->sendingVotes.size() != 1
|| (_flags & PollData::Flag::MultiChoice)) {
_sendingAnimation = nullptr;
}
}
@ -544,6 +729,36 @@ void Poll::radialAnimationCallback() const {
}
}
void Poll::paintRecentVoters(
Painter &p,
int left,
int top,
TextSelection selection) const {
const auto count = int(_recentVoters.size());
if (!count) {
return;
}
auto x = left
+ st::historyPollRecentVotersSkip
+ (count - 1) * st::historyPollRecentVoterSkip;
auto y = top;
const auto size = st::historyPollRecentVoterSize;
const auto outbg = _parent->hasOutLayout();
const auto selected = (selection == FullSelection);
auto pen = (selected
? (outbg ? st::msgOutBgSelected : st::msgInBgSelected)
: (outbg ? st::msgOutBg : st::msgInBg))->p;
pen.setWidth(st::lineWidth);
for (const auto &recent : _recentVoters) {
recent->paintUserpic(p, x, y, size);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
PainterHighQualityEnabler hq(p);
p.drawEllipse(x, y, size, size);
x -= st::historyPollRecentVoterSkip;
}
}
int Poll::paintAnswer(
Painter &p,
const Answer &answer,
@ -592,6 +807,8 @@ int Poll::paintAnswer(
p.setOpacity(sqrt(opacity));
paintFilling(
p,
animation->chosen,
animation->correct,
animation->filling.current(),
left,
top,
@ -613,6 +830,8 @@ int Poll::paintAnswer(
selection);
paintFilling(
p,
answer.chosen,
answer.correct,
answer.filling,
left,
top,
@ -644,9 +863,13 @@ void Poll::paintRadio(
const auto over = ClickHandler::showAsActive(answer.handler);
const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
p.setBrush(Qt::NoBrush);
const auto checkmark = answer.selectedAnimation.value(answer.selected ? 1. : 0.);
const auto o = p.opacity();
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
if (checkmark < 1.) {
p.setBrush(Qt::NoBrush);
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
}
const auto rect = QRectF(left, top, st.diameter, st.diameter).marginsRemoved(QMarginsF(st.thickness / 2., st.thickness / 2., st.thickness / 2., st.thickness / 2.));
if (_sendingAnimation && _sendingAnimation->option == answer.option) {
@ -665,10 +888,24 @@ void Poll::paintRadio(
state.arcLength);
}
} else {
auto pen = regular->p;
pen.setWidth(st.thickness);
p.setPen(pen);
p.drawEllipse(rect);
if (checkmark < 1.) {
auto pen = regular->p;
pen.setWidth(st.thickness);
p.setPen(pen);
p.drawEllipse(rect);
}
if (checkmark > 0.) {
const auto removeFull = (st.diameter / 2 - st.thickness);
const auto removeNow = removeFull * (1. - checkmark);
const auto color = outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg);
auto pen = color->p;
pen.setWidth(st.thickness);
p.setPen(pen);
p.setBrush(color);
p.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow }));
const auto &icon = outbg ? (selected ? st::historyPollOutChosenSelected : st::historyPollOutChosen) : (selected ? st::historyPollInChosenSelected : st::historyPollInChosen);
icon.paint(p, left + (st.diameter - icon.width()) / 2, top + (st.diameter - icon.height()) / 2, width());
}
}
p.setOpacity(o);
@ -696,6 +933,8 @@ void Poll::paintPercent(
void Poll::paintFilling(
Painter &p,
bool chosen,
bool correct,
float64 filling,
int left,
int top,
@ -712,15 +951,36 @@ void Poll::paintFilling(
top += st::historyPollAnswerPadding.top();
const auto bar = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(bar);
const auto thickness = st::historyPollFillingHeight;
const auto max = awidth - st::historyPollFillingRight;
const auto size = anim::interpolate(st::historyPollFillingMin, max, filling);
const auto radius = st::historyPollFillingRadius;
const auto ftop = bottom - st::historyPollFillingBottom - st::historyPollFillingHeight;
p.drawRoundedRect(aleft, ftop, size, st::historyPollFillingHeight, radius, radius);
const auto ftop = bottom - st::historyPollFillingBottom - thickness;
if (chosen && !correct) {
p.setBrush(st::boxTextFgError);
} else {
const auto bar = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
p.setBrush(bar);
}
auto barleft = aleft;
auto barwidth = size;
if (chosen || correct) {
const auto &icon = (chosen && !correct)
? st::historyPollChoiceWrong
: st::historyPollChoiceRight;
const auto cleft = aleft - st::historyPollPercentSkip - icon.width();
const auto ctop = ftop - (icon.height() - thickness) / 2;
p.drawEllipse(cleft, ctop, icon.width(), icon.height());
icon.paint(p, cleft, ctop, width);
//barleft += icon.width() - radius;
//barwidth -= icon.width() - radius;
}
if (barwidth > 0) {
p.drawRoundedRect(barleft, ftop, barwidth, thickness, radius, radius);
}
}
bool Poll::answerVotesChanged() const {
@ -748,6 +1008,8 @@ void Poll::saveStateInAnimation() const {
result.percent = show ? float64(answer.votesPercent) : 0.;
result.filling = show ? answer.filling : 0.;
result.opacity = show ? 1. : 0.;
result.chosen = answer.chosen;
result.correct = answer.correct;
return result;
};
ranges::transform(
@ -761,7 +1023,7 @@ bool Poll::checkAnimationStart() const {
// Skip initial changes.
return false;
}
const auto result = (showVotes() != (_poll->voted() || _poll->closed))
const auto result = (showVotes() != (_poll->voted() || _poll->closed()))
|| answerVotesChanged();
if (result) {
saveStateInAnimation();
@ -780,6 +1042,8 @@ void Poll::startAnswersAnimation() const {
data.percent.start(show ? float64(answer.votesPercent) : 0.);
data.filling.start(show ? answer.filling : 0.);
data.opacity.start(show ? 1. : 0.);
data.chosen = data.chosen || answer.chosen;
data.correct = data.correct || answer.correct;
}
_answersAnimation->progress.start(
[=] { history()->owner().requestViewRepaint(_parent); },
@ -790,7 +1054,7 @@ void Poll::startAnswersAnimation() const {
TextState Poll::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
if (!_poll->sendingVote.isEmpty()) {
if (!_poll->sendingVotes.empty()) {
return result;
}
@ -819,15 +1083,39 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
result.customTooltip = true;
using Flag = Ui::Text::StateRequest::Flag;
if (request.flags & Flag::LookupCustomTooltip) {
const auto quiz = _poll->quiz();
result.customTooltipText = answer.votes
? tr::lng_polls_votes_count(tr::now, lt_count_decimal, answer.votes)
: tr::lng_polls_votes_none(tr::now);
? (quiz
? tr::lng_polls_answers_count
: tr::lng_polls_votes_count)(
tr::now,
lt_count_decimal,
answer.votes)
: (quiz
? tr::lng_polls_answers_none
: tr::lng_polls_votes_none)(tr::now);
}
}
return result;
}
tshift += height;
}
if (!showVotersCount()) {
const auto link = showVotes()
? _showResultsLink
: canSendVotes()
? _sendVotesLink
: nullptr;
if (link) {
const auto linkHeight = bottomButtonHeight();
const auto linkTop = height() - linkHeight;
if (QRect(0, linkTop, width(), linkHeight).contains(point)) {
_lastLinkPoint = point;
result.link = link;
return result;
}
}
}
return result;
}
@ -842,6 +1130,8 @@ void Poll::clickHandlerPressedChanged(
&Answer::handler);
if (i != end(_answers)) {
toggleRipple(*i, pressed);
} else if (handler == _sendVotesLink || handler == _showResultsLink) {
toggleLinkRipple(pressed);
}
}
@ -864,10 +1154,55 @@ void Poll::toggleRipple(Answer &answer, bool pressed) {
}
const auto top = countAnswerTop(answer, innerWidth);
answer.ripple->add(_lastLinkPoint - QPoint(0, top));
} else {
if (answer.ripple) {
answer.ripple->lastStop();
} else if (answer.ripple) {
answer.ripple->lastStop();
}
}
int Poll::bottomButtonHeight() const {
const auto skip = st::historyPollChoiceRight.height()
- st::historyPollFillingBottom
- st::historyPollFillingHeight
- (st::historyPollChoiceRight.height() - st::historyPollFillingHeight) / 2;
return st::historyPollTotalVotesSkip
- skip
+ st::historyPollBottomButtonSkip
+ st::msgDateFont->height
+ st::msgPadding.bottom();
}
void Poll::toggleLinkRipple(bool pressed) {
if (pressed) {
const auto linkWidth = width();
const auto linkHeight = bottomButtonHeight();
if (!_linkRipple) {
const auto drawMask = [&](QPainter &p) {
const auto radius = st::historyMessageRadius;
p.drawRoundedRect(
0,
0,
linkWidth,
linkHeight,
radius,
radius);
p.fillRect(0, 0, linkWidth, radius * 2, Qt::white);
};
auto mask = isBubbleBottom()
? Ui::RippleAnimation::maskByDrawer(
QSize(linkWidth, linkHeight),
false,
drawMask)
: Ui::RippleAnimation::rectMask({ linkWidth, linkHeight });
_linkRipple = std::make_unique<Ui::RippleAnimation>(
(_parent->hasOutLayout()
? st::historyPollRippleOut
: st::historyPollRippleIn),
std::move(mask),
[=] { history()->owner().requestViewRepaint(_parent); });
}
_linkRipple->add(_lastLinkPoint - QPoint(0, height() - linkHeight));
} else if (_linkRipple) {
_linkRipple->lastStop();
}
}

View file

@ -8,12 +8,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "history/view/media/history_view_media.h"
#include "data/data_poll.h"
#include "base/weak_ptr.h"
struct PollAnswer;
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace HistoryView {
class Poll : public Media {
class Poll : public Media, public base::has_weak_ptr {
public:
Poll(
not_null<Element*> parent,
@ -53,6 +57,7 @@ private:
[[nodiscard]] bool showVotes() const;
[[nodiscard]] bool canVote() const;
[[nodiscard]] bool canSendVotes() const;
[[nodiscard]] int countAnswerTop(
const Answer &answer,
@ -61,11 +66,14 @@ private:
const Answer &answer,
int innerWidth) const;
[[nodiscard]] ClickHandlerPtr createAnswerClickHandler(
const Answer &answer) const;
const Answer &answer);
void updateTexts();
void updateRecentVoters();
void updateAnswers();
void updateVotes();
void updateTotalVotes();
bool showVotersCount() const;
bool inlineFooter() const;
void updateAnswerVotes();
void updateAnswerVotesFromOriginal(
Answer &answer,
@ -74,6 +82,11 @@ private:
int maxVotes);
void checkSendingAnimation() const;
void paintRecentVoters(
Painter &p,
int left,
int top,
TextSelection selection) const;
int paintAnswer(
Painter &p,
const Answer &answer,
@ -99,12 +112,26 @@ private:
TextSelection selection) const;
void paintFilling(
Painter &p,
bool chosen,
bool correct,
float64 filling,
int left,
int top,
int width,
int height,
TextSelection selection) const;
void paintInlineFooter(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const;
void paintBottom(
Painter &p,
int left,
int top,
int paintw,
TextSelection selection) const;
bool checkAnimationStart() const;
bool answerVotesChanged() const;
@ -114,17 +141,30 @@ private:
void radialAnimationCallback() const;
void toggleRipple(Answer &answer, bool pressed);
void toggleLinkRipple(bool pressed);
void toggleMultiOption(const QByteArray &option);
void sendMultiOptions();
void showResults();
not_null<PollData*> _poll;
[[nodiscard]] int bottomButtonHeight() const;
const not_null<PollData*> _poll;
int _pollVersion = 0;
int _totalVotes = 0;
bool _voted = false;
bool _closed = false;
PollData::Flags _flags = PollData::Flags();
Ui::Text::String _question;
Ui::Text::String _subtitle;
std::vector<not_null<UserData*>> _recentVoters;
QImage _recentVotersImage;
std::vector<Answer> _answers;
Ui::Text::String _totalVotesLabel;
ClickHandlerPtr _showResultsLink;
ClickHandlerPtr _sendVotesLink;
mutable std::unique_ptr<Ui::RippleAnimation> _linkRipple;
bool _hasSelected = false;
mutable std::unique_ptr<AnswersAnimation> _answersAnimation;
mutable std::unique_ptr<SendingAnimation> _sendingAnimation;

View file

@ -266,6 +266,8 @@ Key ContentMemento::key() const {
return Key(Auth().data().peer(peerId));
//} else if (const auto feed = this->feed()) { // #feed
// return Key(feed);
} else if (const auto poll = this->poll()) {
return Key(poll, pollContextId());
} else {
return Settings::Tag{ settingsSelf() };
}

View file

@ -123,6 +123,10 @@ public:
//explicit ContentMemento(not_null<Data::Feed*> feed) : _feed(feed) { // #feed
//}
explicit ContentMemento(Settings::Tag settings);
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
: _poll(poll)
, _pollContextId(contextId) {
}
virtual object_ptr<ContentWidget> createWidget(
QWidget *parent,
@ -141,6 +145,12 @@ public:
UserData *settingsSelf() const {
return _settingsSelf;
}
PollData *poll() const {
return _poll;
}
FullMsgId pollContextId() const {
return _pollContextId;
}
Key key() const;
virtual Section section() const = 0;
@ -177,6 +187,9 @@ private:
const PeerId _migratedPeerId = 0;
//Data::Feed * const _feed = nullptr; // #feed
UserData * const _settingsSelf = nullptr;
PollData * const _poll = nullptr;
const FullMsgId _pollContextId;
int _scrollTop = 0;
QString _searchFieldQuery;
bool _searchEnabledByContent = false;

View file

@ -19,6 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
#include "data/data_media_types.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
@ -46,6 +48,10 @@ Key::Key(not_null<PeerData*> peer) : _value(peer) {
Key::Key(Settings::Tag settings) : _value(settings) {
}
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
: _value(PollKey{ poll, contextId }) {
}
PeerData *Key::peer() const {
if (const auto peer = base::get_if<not_null<PeerData*>>(&_value)) {
return *peer;
@ -67,6 +73,20 @@ UserData *Key::settingsSelf() const {
return nullptr;
}
PollData *Key::poll() const {
if (const auto data = base::get_if<PollKey>(&_value)) {
return data->poll;
}
return nullptr;
}
FullMsgId Key::pollContextId() const {
if (const auto data = base::get_if<PollKey>(&_value)) {
return data->contextId;
}
return FullMsgId();
}
rpl::producer<SparseIdsMergedSlice> AbstractController::mediaSource(
SparseIdsMergedSlice::UniversalMsgId aroundId,
int limitBefore,
@ -106,6 +126,15 @@ PeerId AbstractController::migratedPeerId() const {
return PeerId(0);
}
PollData *AbstractController::poll() const {
if (const auto item = session().data().message(pollContextId())) {
if (const auto media = item->media()) {
return media->poll();
}
}
return nullptr;
}
void AbstractController::showSection(
Window::SectionMemento &&memento,
const Window::SectionShow &params) {

View file

@ -33,16 +33,24 @@ public:
Key(not_null<PeerData*> peer);
//Key(not_null<Data::Feed*> feed); // #feed
Key(Settings::Tag settings);
Key(not_null<PollData*> poll, FullMsgId contextId);
PeerData *peer() const;
//Data::Feed *feed() const; // #feed
UserData *settingsSelf() const;
PollData *poll() const;
FullMsgId pollContextId() const;
private:
struct PollKey {
not_null<PollData*> poll;
FullMsgId contextId;
};
base::variant<
not_null<PeerData*>,
//not_null<Data::Feed*>, // #feed
Settings::Tag> _value;
Settings::Tag,
PollKey> _value;
};
@ -60,6 +68,7 @@ public:
Members,
//Channels, // #feed
Settings,
PollResults,
};
using SettingsType = ::Settings::Type;
using MediaType = Storage::SharedMediaType;
@ -113,6 +122,10 @@ public:
UserData *settingsSelf() const {
return key().settingsSelf();
}
PollData *poll() const;
FullMsgId pollContextId() const {
return key().pollContextId();
}
virtual void setSearchEnabledByContent(bool enabled) {
}

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/common_groups/info_common_groups_widget.h"
//#include "info/feed/info_feed_profile_widget.h" // #feed
#include "info/settings/info_settings_widget.h"
#include "info/polls/info_polls_results_widget.h"
#include "info/info_section_widget.h"
#include "info/info_layer_widget.h"
#include "info/info_controller.h"
@ -42,6 +43,10 @@ Memento::Memento(Settings::Tag settings, Section section)
: Memento(DefaultStack(settings, section)) {
}
Memento::Memento(not_null<PollData*> poll, FullMsgId contextId)
: Memento(DefaultStack(poll, contextId)) {
}
Memento::Memento(std::vector<std::unique_ptr<ContentMemento>> stack)
: _stack(std::move(stack)) {
}
@ -72,6 +77,14 @@ std::vector<std::unique_ptr<ContentMemento>> Memento::DefaultStack(
return result;
}
std::vector<std::unique_ptr<ContentMemento>> Memento::DefaultStack(
not_null<PollData*> poll,
FullMsgId contextId) {
auto result = std::vector<std::unique_ptr<ContentMemento>>();
result.push_back(std::make_unique<Polls::Memento>(poll, contextId));
return result;
}
Section Memento::DefaultSection(not_null<PeerData*> peer) {
if (peer->isSelf()) {
return Section(Section::MediaType::Photo);

View file

@ -36,6 +36,7 @@ public:
Memento(PeerId peerId, Section section);
//Memento(not_null<Data::Feed*> feed, Section section); // #feed
Memento(Settings::Tag settings, Section section);
Memento(not_null<PollData*> poll, FullMsgId contextId);
explicit Memento(std::vector<std::unique_ptr<ContentMemento>> stack);
object_ptr<Window::SectionWidget> createWidget(
@ -76,10 +77,13 @@ private:
static std::vector<std::unique_ptr<ContentMemento>> DefaultStack(
Settings::Tag settings,
Section section);
static std::vector<std::unique_ptr<ContentMemento>> DefaultStack(
not_null<PollData*> poll,
FullMsgId contextId);
//static std::unique_ptr<ContentMemento> DefaultContent( // #feed
// not_null<Data::Feed*> feed,
// Section section);
static std::unique_ptr<ContentMemento> DefaultContent(
PeerId peerId,
Section section);

View file

@ -635,6 +635,11 @@ rpl::producer<QString> TitleValue(
return tr::ktg_settings_kotato();
}
Unexpected("Bad settings type in Info::TitleValue()");
case Section::Type::PollResults:
return key.poll()->quiz()
? tr::lng_polls_quiz_results_title()
: tr::lng_polls_poll_results_title();
}
Unexpected("Bad section type in Info::TitleValue()");
}

View file

@ -217,7 +217,7 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const {
return Dialogs::RowDescriptor(peer->owner().history(peer), FullMsgId());
//} else if (const auto feed = key().feed()) { // #feed
// return Dialogs::RowDescriptor(feed, FullMsgId());
} else if (key().settingsSelf()) {
} else if (key().settingsSelf() || key().poll()) {
return Dialogs::RowDescriptor();
}
Unexpected("Owner in WrapWidget::activeChat().");

View file

@ -0,0 +1,725 @@
/*
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 "info/polls/info_polls_results_inner_widget.h"
#include "info/polls/info_polls_results_widget.h"
#include "info/info_controller.h"
#include "lang/lang_keys.h"
#include "data/data_poll.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/text/text_utilities.h"
#include "boxes/peer_list_box.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "apiwrap.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
namespace Info {
namespace Polls {
namespace {
constexpr auto kFirstPage = 15;
constexpr auto kPerPage = 50;
constexpr auto kLeavePreloaded = 5;
class PeerListDummy final : public Ui::RpWidget {
public:
PeerListDummy(QWidget *parent, int count, const style::PeerList &st);
protected:
void paintEvent(QPaintEvent *e) override;
private:
const style::PeerList &_st;
int _count = 0;
std::vector<Ui::Animations::Simple> _animations;
};
class ListDelegate final : public PeerListContentDelegate {
public:
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
bool peerListIsRowSelected(not_null<PeerData*> peer) override;
int peerListSelectedRowsCount() override;
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
void peerListScrollToTop() override;
void peerListAddSelectedRowInBunch(
not_null<PeerData*> peer) override;
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
};
PeerListDummy::PeerListDummy(
QWidget *parent,
int count,
const style::PeerList &st)
: _st(st)
, _count(count) {
resize(width(), _count * _st.item.height);
}
void PeerListDummy::paintEvent(QPaintEvent *e) {
QPainter p(this);
PainterHighQualityEnabler hq(p);
const auto fill = e->rect();
const auto bottom = fill.top() + fill.height();
const auto from = floorclamp(fill.top(), _st.item.height, 0, _count);
const auto till = ceilclamp(bottom, _st.item.height, 0, _count);
p.translate(0, _st.item.height * from);
p.setPen(Qt::NoPen);
for (auto i = from; i != till; ++i) {
p.setBrush(st::windowBgOver);
p.drawEllipse(
_st.item.photoPosition.x(),
_st.item.photoPosition.y(),
_st.item.photoSize,
_st.item.photoSize);
const auto small = int(1.5 * _st.item.photoSize);
const auto large = 2 * small;
const auto second = (i % 2) ? large : small;
const auto height = _st.item.nameStyle.font->height / 2;
const auto radius = height / 2;
const auto left = _st.item.namePosition.x();
const auto top = _st.item.namePosition.y()
+ (_st.item.nameStyle.font->height - height) / 2;
const auto skip = _st.item.namePosition.x()
- _st.item.photoPosition.x()
- _st.item.photoSize;
const auto next = left + small + skip;
p.drawRoundedRect(left, top, small, height, radius, radius);
p.drawRoundedRect(next, top, second, height, radius, radius);
p.translate(0, _st.item.height);
}
}
void ListDelegate::peerListSetTitle(rpl::producer<QString> title) {
}
void ListDelegate::peerListSetAdditionalTitle(rpl::producer<QString> title) {
}
bool ListDelegate::peerListIsRowSelected(not_null<PeerData*> peer) {
return false;
}
int ListDelegate::peerListSelectedRowsCount() {
return 0;
}
auto ListDelegate::peerListCollectSelectedRows()
-> std::vector<not_null<PeerData*>> {
return {};
}
void ListDelegate::peerListScrollToTop() {
}
void ListDelegate::peerListAddSelectedRowInBunch(not_null<PeerData*> peer) {
Unexpected("Item selection in Info::Profile::Members.");
}
void ListDelegate::peerListFinishSelectedRowsBunch() {
}
void ListDelegate::peerListSetDescription(
object_ptr<Ui::FlatLabel> description) {
description.destroy();
}
} // namespace
class ListController final : public PeerListController {
public:
ListController(
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
QByteArray option);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
void allowLoadMore();
void collapse();
[[nodiscard]] auto showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>>;
[[nodiscard]] rpl::producer<int> scrollToRequests() const;
[[nodiscard]] rpl::producer<int> count() const;
[[nodiscard]] rpl::producer<int> fullCount() const;
[[nodiscard]] rpl::producer<int> loadMoreCount() const;
std::unique_ptr<PeerListState> saveState() const override;
void restoreState(std::unique_ptr<PeerListState> state) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
void scrollTo(int y);
private:
struct SavedState : SavedStateBase {
QString offset;
QString loadForOffset;
int leftToLoad = 0;
int fullCount = 0;
std::vector<not_null<UserData*>> preloaded;
bool wasLoading = false;
};
bool appendRow(not_null<UserData*> user);
std::unique_ptr<PeerListRow> createRow(not_null<UserData*> user) const;
void addPreloaded();
bool addPreloadedPage();
void preloadedAdded();
const not_null<Main::Session*> _session;
const not_null<PollData*> _poll;
const FullMsgId _context;
const QByteArray _option;
MTP::Sender _api;
QString _offset;
mtpRequestId _loadRequestId = 0;
QString _loadForOffset;
std::vector<not_null<UserData*>> _preloaded;
rpl::variable<int> _count = 0;
rpl::variable<int> _fullCount;
rpl::variable<int> _leftToLoad;
rpl::event_stream<not_null<PeerData*>> _showPeerInfoRequests;
rpl::event_stream<int> _scrollToRequests;
};
ListController::ListController(
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
QByteArray option)
: _session(session)
, _poll(poll)
, _context(context)
, _option(option)
, _api(_session->api().instance()) {
const auto i = ranges::find(poll->answers, option, &PollAnswer::option);
Assert(i != poll->answers.end());
_fullCount = i->votes;
_leftToLoad = i->votes;
}
Main::Session &ListController::session() const {
return *_session;
}
void ListController::prepare() {
delegate()->peerListRefreshRows();
}
void ListController::loadMoreRows() {
if (_loadRequestId
|| !_leftToLoad.current()
|| (!_offset.isEmpty() && _loadForOffset != _offset)
|| !_preloaded.empty()) {
return;
}
const auto item = session().data().message(_context);
if (!item || !IsServerMsgId(item->id)) {
_leftToLoad = 0;
return;
}
using Flag = MTPmessages_GetPollVotes::Flag;
const auto flags = Flag::f_option
| (_offset.isEmpty() ? Flag(0) : Flag::f_offset);
const auto limit = _offset.isEmpty() ? kFirstPage : kPerPage;
_loadRequestId = _api.request(MTPmessages_GetPollVotes(
MTP_flags(flags),
item->history()->peer->input,
MTP_int(item->id),
MTP_bytes(_option),
MTP_string(_offset),
MTP_int(limit)
)).done([=](const MTPmessages_VotesList &result) {
const auto count = result.match([&](
const MTPDmessages_votesList &data) {
_offset = data.vnext_offset().value_or_empty();
auto &owner = session().data();
owner.processUsers(data.vusers());
auto add = limit - kLeavePreloaded;
for (const auto &vote : data.vvotes().v) {
vote.match([&](const auto &data) {
const auto user = owner.user(data.vuser_id().v);
if (user->loadedStatus != PeerData::NotLoaded) {
if (add) {
appendRow(user);
--add;
} else {
_preloaded.push_back(user);
}
}
});
}
return data.vcount().v;
});
if (_offset.isEmpty()) {
addPreloaded();
_fullCount = delegate()->peerListFullRowsCount();
_leftToLoad = 0;
} else {
_count = delegate()->peerListFullRowsCount();
_fullCount = count;
_leftToLoad = count - delegate()->peerListFullRowsCount();
delegate()->peerListRefreshRows();
}
_loadRequestId = 0;
}).fail([=](const RPCError &error) {
_loadRequestId = 0;
}).send();
}
void ListController::allowLoadMore() {
if (!addPreloadedPage()) {
_loadForOffset = _offset;
addPreloaded();
loadMoreRows();
}
}
void ListController::collapse() {
const auto count = delegate()->peerListFullRowsCount();
if (count <= kFirstPage) {
return;
}
const auto remove = count - (kFirstPage - kLeavePreloaded);
ranges::action::reverse(_preloaded);
_preloaded.reserve(_preloaded.size() + remove);
for (auto i = 0; i != remove; ++i) {
const auto row = delegate()->peerListRowAt(count - i - 1);
_preloaded.push_back(row->peer()->asUser());
delegate()->peerListRemoveRow(row);
}
ranges::action::reverse(_preloaded);
delegate()->peerListRefreshRows();
const auto now = count - remove;
_count = now;
_leftToLoad = _fullCount.current() - now;
}
void ListController::addPreloaded() {
for (const auto user : base::take(_preloaded)) {
appendRow(user);
}
preloadedAdded();
}
bool ListController::addPreloadedPage() {
if (_preloaded.size() < kPerPage + kLeavePreloaded) {
return false;
}
const auto from = begin(_preloaded);
const auto till = from + kPerPage;
for (auto i = from; i != till; ++i) {
appendRow(*i);
}
_preloaded.erase(from, till);
preloadedAdded();
return true;
}
void ListController::preloadedAdded() {
_count = delegate()->peerListFullRowsCount();
_leftToLoad = _fullCount.current() - _count.current();
delegate()->peerListRefreshRows();
}
auto ListController::showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>> {
return _showPeerInfoRequests.events();
}
rpl::producer<int> ListController::scrollToRequests() const {
return _scrollToRequests.events();
}
rpl::producer<int> ListController::count() const {
return _count.value();
}
rpl::producer<int> ListController::fullCount() const {
return _fullCount.value();
}
rpl::producer<int> ListController::loadMoreCount() const {
const auto initial = (_fullCount.current() <= kFirstPage)
? _fullCount.current()
: (kFirstPage - kLeavePreloaded);
return rpl::combine(
_count.value(),
_leftToLoad.value()
) | rpl::map([=](int count, int leftToLoad) {
return (count > 0) ? leftToLoad : (leftToLoad - initial);
});
}
auto ListController::saveState() const -> std::unique_ptr<PeerListState> {
auto result = PeerListController::saveState();
auto my = std::make_unique<SavedState>();
my->offset = _offset;
my->fullCount = _fullCount.current();
my->leftToLoad = _leftToLoad.current();
my->preloaded = _preloaded;
my->wasLoading = (_loadRequestId != 0);
my->loadForOffset = _loadForOffset;
result->controllerState = std::move(my);
return result;
}
void ListController::restoreState(std::unique_ptr<PeerListState> state) {
auto typeErasedState = state
? state->controllerState.get()
: nullptr;
if (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {
if (const auto requestId = base::take(_loadRequestId)) {
_api.request(requestId).cancel();
}
_offset = my->offset;
_loadForOffset = my->loadForOffset;
_preloaded = std::move(my->preloaded);
_count = int(state->list.size());
_fullCount = my->fullCount;
_leftToLoad = my->leftToLoad;
if (my->wasLoading) {
loadMoreRows();
}
PeerListController::restoreState(std::move(state));
}
}
std::unique_ptr<PeerListRow> ListController::createRestoredRow(
not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return createRow(user);
}
return nullptr;
}
void ListController::rowClicked(not_null<PeerListRow*> row) {
_showPeerInfoRequests.fire(row->peer());
}
bool ListController::appendRow(not_null<UserData*> user) {
if (delegate()->peerListFindRow(user->id)) {
return false;
}
delegate()->peerListAppendRow(createRow(user));
return true;
}
std::unique_ptr<PeerListRow> ListController::createRow(
not_null<UserData*> user) const {
auto row = std::make_unique<PeerListRow>(user);
row->setCustomStatus(QString());
return row;
}
void ListController::scrollTo(int y) {
_scrollToRequests.fire_copy(y);
}
ListController *CreateAnswerRows(
not_null<Ui::VerticalLayout*> container,
rpl::producer<int> visibleTop,
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
const PollAnswer &answer) {
using namespace rpl::mappers;
if (!answer.votes) {
return nullptr;
}
const auto delegate = container->lifetime().make_state<ListDelegate>();
const auto controller = container->lifetime().make_state<ListController>(
session,
poll,
context,
answer.option);
const auto percent = answer.votes * 100 / poll->totalVoters;
const auto phrase = poll->quiz()
? tr::lng_polls_answers_count
: tr::lng_polls_votes_count;
const auto sampleText = phrase(
tr::now,
lt_count_decimal,
answer.votes);
const auto &font = st::boxDividerLabel.style.font;
const auto sampleWidth = font->width(sampleText);
const auto rightSkip = sampleWidth + font->spacew * 4;
const auto headerWrap = container->add(
object_ptr<Ui::RpWidget>(
container));
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxLittleSkip));
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller,
st::infoCommonGroupsList));
delegate->setContent(content);
controller->setDelegate(delegate);
const auto count = (answer.votes <= kFirstPage)
? answer.votes
: (kFirstPage - kLeavePreloaded);
const auto placeholder = container->add(object_ptr<PeerListDummy>(
container,
count,
st::infoCommonGroupsList));
controller->count(
) | rpl::filter(_1 > 0) | rpl::start_with_next([=] {
delete placeholder;
}, placeholder->lifetime());
const auto header = Ui::CreateChild<Ui::DividerLabel>(
container.get(),
object_ptr<Ui::FlatLabel>(
container,
(answer.text
+ QString::fromUtf8(" \xe2\x80\x94 ")
+ QString::number(percent)
+ "%"),
st::boxDividerLabel),
style::margins(
st::pollResultsHeaderPadding.left(),
st::pollResultsHeaderPadding.top(),
st::pollResultsHeaderPadding.right() + rightSkip,
st::pollResultsHeaderPadding.bottom()));
const auto votes = Ui::CreateChild<Ui::FlatLabel>(
header,
phrase(
lt_count_decimal,
controller->fullCount() | rpl::map(_1 + 0.)),
st::pollResultsVotesCount);
const auto collapse = Ui::CreateChild<Ui::LinkButton>(
header,
tr::lng_polls_votes_collapse(tr::now),
st::defaultLinkButton);
collapse->setClickedCallback([=] {
controller->scrollTo(headerWrap->y());
controller->collapse();
});
rpl::combine(
controller->fullCount(),
controller->count()
) | rpl::start_with_next([=](int fullCount, int count) {
const auto many = (fullCount > kFirstPage)
&& (count > kFirstPage - kLeavePreloaded);
collapse->setVisible(many);
votes->setVisible(!many);
}, collapse->lifetime());
headerWrap->widthValue(
) | rpl::start_with_next([=](int width) {
header->resizeToWidth(width);
votes->moveToRight(
st::pollResultsHeaderPadding.right(),
st::pollResultsHeaderPadding.top(),
width);
collapse->moveToRight(
st::pollResultsHeaderPadding.right(),
st::pollResultsHeaderPadding.top(),
width);
}, header->lifetime());
header->heightValue(
) | rpl::start_with_next([=](int height) {
headerWrap->resize(headerWrap->width(), height);
}, header->lifetime());
const auto more = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
object_ptr<Ui::SettingsButton>(
container,
tr::lng_polls_show_more(
lt_count_decimal,
controller->loadMoreCount() | rpl::map(_1 + 0.),
Ui::Text::Upper),
st::pollResultsShowMore)));
more->entity()->setClickedCallback([=] {
controller->allowLoadMore();
});
controller->loadMoreCount(
) | rpl::map(_1 > 0) | rpl::start_with_next([=](bool visible) {
more->toggle(visible, anim::type::instant);
}, more->lifetime());
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxLittleSkip));
rpl::combine(
std::move(visibleTop),
headerWrap->geometryValue(),
more->topValue()
) | rpl::filter([=](int, QRect headerRect, int moreTop) {
return moreTop >= headerRect.y() + headerRect.height();
}) | rpl::start_with_next([=](
int visibleTop,
QRect headerRect,
int moreTop) {
const auto skip = st::pollResultsHeaderPadding.top()
- st::pollResultsHeaderPadding.bottom();
const auto top = std::clamp(
visibleTop - skip,
headerRect.y(),
moreTop - headerRect.height());
header->move(0, top);
}, header->lifetime());
return controller;
}
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PollData*> poll,
FullMsgId contextId)
: RpWidget(parent)
, _controller(controller)
, _poll(poll)
, _contextId(contextId)
, _content(this) {
setupContent();
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_content, visibleTop, visibleBottom);
_visibleTop = visibleTop;
}
void InnerWidget::saveState(not_null<Memento*> memento) {
auto states = base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>>();
for (const auto &[option, controller] : _sections) {
states[option] = controller->saveState();
}
memento->setListStates(std::move(states));
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
auto states = memento->listStates();
for (const auto &[option, controller] : _sections) {
const auto i = states.find(option);
if (i != end(states)) {
controller->restoreState(std::move(i->second));
}
}
}
int InnerWidget::desiredHeight() const {
auto desired = 0;
//auto count = qMax(_user->commonChatsCount(), 1);
//desired += qMax(count, _list->fullRowsCount())
// * st::infoCommonGroupsList.item.height;
return qMax(height(), desired);
}
void InnerWidget::setupContent() {
const auto quiz = _poll->quiz();
_content->add(
object_ptr<Ui::FlatLabel>(
_content,
_poll->question,
st::pollResultsQuestion),
style::margins{
st::boxRowPadding.left(),
0,
st::boxRowPadding.right(),
st::boxMediumSkip });
for (const auto &answer : _poll->answers) {
const auto session = &_controller->parentController()->session();
const auto controller = CreateAnswerRows(
_content,
_visibleTop.value(),
session,
_poll,
_contextId,
answer);
if (!controller) {
continue;
}
controller->showPeerInfoRequests(
) | rpl::start_to_stream(
_showPeerInfoRequests,
lifetime());
controller->scrollToRequests(
) | rpl::start_with_next([=](int y) {
_scrollToRequests.fire({ y, -1 });
}, lifetime());
_sections.emplace(answer.option, controller);
}
widthValue(
) | rpl::start_with_next([=](int newWidth) {
_content->resizeToWidth(newWidth);
}, _content->lifetime());
_content->heightValue(
) | rpl::start_with_next([=](int height) {
resize(width(), height);
}, _content->lifetime());
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
auto InnerWidget::showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>> {
return _showPeerInfoRequests.events();
}
} // namespace Polls
} // namespace Info

View file

@ -0,0 +1,74 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/widgets/scroll_area.h"
#include "base/object_ptr.h"
namespace Ui {
class VerticalLayout;
} // namespace Ui
namespace Info {
class Controller;
namespace Polls {
class Memento;
class ListController;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PollData*> poll,
FullMsgId contextId);
[[nodiscard]] not_null<PollData*> poll() const {
return _poll;
}
[[nodiscard]] FullMsgId contextId() const {
return _contextId;
}
[[nodiscard]] auto scrollToRequests() const
-> rpl::producer<Ui::ScrollToRequest>;
[[nodiscard]] auto showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>>;
[[nodiscard]] int desiredHeight() const;
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
void setupContent();
not_null<Controller*> _controller;
not_null<PollData*> _poll;
FullMsgId _contextId;
object_ptr<Ui::VerticalLayout> _content;
base::flat_map<QByteArray, not_null<ListController*>> _sections;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<not_null<PeerData*>> _showPeerInfoRequests;
rpl::variable<int> _visibleTop = 0;
};
} // namespace Polls
} // namespace Info

View file

@ -0,0 +1,111 @@
/*
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 "info/polls/info_polls_results_widget.h"
#include "info/polls/info_polls_results_inner_widget.h"
#include "boxes/peer_list_box.h"
namespace Info {
namespace Polls {
Memento::Memento(not_null<PollData*> poll, FullMsgId contextId)
: ContentMemento(poll, contextId) {
}
Section Memento::section() const {
return Section(Section::Type::PollResults);
}
void Memento::setListStates(base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>> states) {
_listStates = std::move(states);
}
auto Memento::listStates()
-> base::flat_map<QByteArray, std::unique_ptr<PeerListState>> {
return std::move(_listStates);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Memento::~Memento() = default;
Widget::Widget(QWidget *parent, not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _inner(setInnerWidget(
object_ptr<InnerWidget>(
this,
controller,
controller->poll(),
controller->pollContextId()))) {
_inner->showPeerInfoRequests(
) | rpl::start_with_next([=](not_null<PeerData*> peer) {
controller->showPeerInfo(peer);
}, _inner->lifetime());
_inner->scrollToRequests(
) | rpl::start_with_next([=](const Ui::ScrollToRequest &request) {
scrollTo(request);
}, _inner->lifetime());
controller->setCanSaveChanges(rpl::single(false));
}
not_null<PollData*> Widget::poll() const {
return _inner->poll();
}
FullMsgId Widget::contextId() const {
return _inner->contextId();
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
//if (const auto myMemento = dynamic_cast<Memento*>(memento.get())) {
// Assert(myMemento->self() == self());
// if (_inner->showInternal(myMemento)) {
// return true;
// }
//}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::unique_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_unique<Memento>(poll(), contextId());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
auto scrollTop = memento->scrollTop();
scrollTopRestore(memento->scrollTop());
}
} // namespace Polls
} // namespace Info

View file

@ -0,0 +1,70 @@
/*
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 "info/info_content_widget.h"
#include "info/info_controller.h"
struct PeerListState;
namespace Info {
namespace Polls {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<PollData*> poll, FullMsgId contextId);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
void setListStates(base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>> states);
auto listStates()
-> base::flat_map<QByteArray, std::unique_ptr<PeerListState>>;
private:
base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>> _listStates;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
[[nodiscard]] not_null<PollData*> poll() const;
[[nodiscard]] FullMsgId contextId() const;
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::unique_ptr<ContentMemento> doCreateMemento() override;
not_null<InnerWidget*> _inner;
};
} // namespace Settings
} // namespace Info

View file

@ -20,7 +20,6 @@ namespace Settings {
using Type = Section::SettingsType;
struct Tag;
class InnerWidget;
class Memento final : public ContentMemento {
public:

View file

@ -309,6 +309,7 @@ void File::Context::readNextPacket() {
if (i->second.size() == kMaxQueuedPackets) {
processQueuedPackets(SleepPolicy::Allowed);
}
Assert(i->second.size() < kMaxQueuedPackets);
} else {
// Still trying to read by drain.
Assert(result.is<FFmpeg::AvErrorWrap>());

View file

@ -396,6 +396,8 @@ bool Player::fileProcessPackets(
videoReceivedTill(till);
});
_video->process(base::take(list));
} else {
list.clear(); // Free non-needed packets.
}
}
return fileReadMore();

View file

@ -68,6 +68,7 @@ namespace View {
namespace {
constexpr auto kPreloadCount = 4;
constexpr auto kMaxZoomLevel = 7; // x8
constexpr auto kOverlayLoaderPriority = 2;
@ -897,13 +898,13 @@ bool OverlayWidget::radialAnimationCallback(crl::time now) {
void OverlayWidget::zoomIn() {
int32 newZoom = _zoom;
if (newZoom == ZoomToScreenLevel) {
if (qCeil(_zoomToScreen) <= MaxZoomLevel) {
if (qCeil(_zoomToScreen) <= kMaxZoomLevel) {
newZoom = qCeil(_zoomToScreen);
}
} else {
if (newZoom < _zoomToScreen && (newZoom + 1 > _zoomToScreen || (_zoomToScreen > MaxZoomLevel && newZoom == MaxZoomLevel))) {
if (newZoom < _zoomToScreen && (newZoom + 1 > _zoomToScreen || (_zoomToScreen > kMaxZoomLevel && newZoom == kMaxZoomLevel))) {
newZoom = ZoomToScreenLevel;
} else if (newZoom < MaxZoomLevel) {
} else if (newZoom < kMaxZoomLevel) {
++newZoom;
}
}
@ -913,13 +914,13 @@ void OverlayWidget::zoomIn() {
void OverlayWidget::zoomOut() {
int32 newZoom = _zoom;
if (newZoom == ZoomToScreenLevel) {
if (qFloor(_zoomToScreen) >= -MaxZoomLevel) {
if (qFloor(_zoomToScreen) >= -kMaxZoomLevel) {
newZoom = qFloor(_zoomToScreen);
}
} else {
if (newZoom > _zoomToScreen && (newZoom - 1 < _zoomToScreen || (_zoomToScreen < -MaxZoomLevel && newZoom == -MaxZoomLevel))) {
if (newZoom > _zoomToScreen && (newZoom - 1 < _zoomToScreen || (_zoomToScreen < -kMaxZoomLevel && newZoom == -kMaxZoomLevel))) {
newZoom = ZoomToScreenLevel;
} else if (newZoom > -MaxZoomLevel) {
} else if (newZoom > -kMaxZoomLevel) {
--newZoom;
}
}
@ -929,7 +930,7 @@ void OverlayWidget::zoomOut() {
void OverlayWidget::zoomReset() {
int32 newZoom = _zoom;
if (_zoom == 0) {
if (qFloor(_zoomToScreen) == qCeil(_zoomToScreen) && qRound(_zoomToScreen) >= -MaxZoomLevel && qRound(_zoomToScreen) <= MaxZoomLevel) {
if (qFloor(_zoomToScreen) == qCeil(_zoomToScreen) && qRound(_zoomToScreen) >= -kMaxZoomLevel && qRound(_zoomToScreen) <= kMaxZoomLevel) {
newZoom = qRound(_zoomToScreen);
} else {
newZoom = ZoomToScreenLevel;

View file

@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/linux_desktop_environment.h"
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
#include <QDBusInterface>
#endif
namespace Platform {
namespace DesktopEnvironment {
@ -117,11 +119,11 @@ bool TryQtTrayIcon() {
bool PreferAppIndicatorTrayIcon() {
return IsXFCE() || IsUnity() || IsUbuntu() ||
(IsGnome() && QDBusInterface("org.kde.StatusNotifierWatcher", "/").isValid());
}
bool TryUnityCounter() {
return IsUnity() || IsPantheon() || IsUbuntu() || IsKDE5();
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
(IsGnome() && QDBusInterface("org.kde.StatusNotifierWatcher", "/").isValid());
#else
IsGnome();
#endif
}
} // namespace DesktopEnvironment

View file

@ -67,7 +67,6 @@ inline bool IsAwesome() {
bool TryQtTrayIcon();
bool PreferAppIndicatorTrayIcon();
bool TryUnityCounter();
} // namespace DesktopEnvironment
} // namespace Platform

View file

@ -19,7 +19,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "facades.h"
#include "app.h"
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
#include <QtDBus>
#endif
#include <QtWidgets/QMenu>
#include <QtWidgets/QAction>
@ -43,7 +46,10 @@ bool _trayIconMuted = true;
int32 _trayIconCount = 0;
QImage _trayIconImageBack, _trayIconImage;
QString _desktopFile;
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
QString _dbusPath = "/";
#endif
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
void _trayIconPopup(GtkStatusIcon *status_icon, guint button, guint32 activate_time, gpointer popup_menu) {
@ -331,6 +337,7 @@ void MainWindow::updateIconCounters() {
const auto counter = Core::App().unreadBadge();
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
if (useUnityCount) {
QVariantMap dbusUnityProperties;
if (counter > 0) {
@ -345,6 +352,7 @@ void MainWindow::updateIconCounters() {
signal << dbusUnityProperties;
QDBusConnection::sessionBus().send(signal);
}
#endif
if (noQtTrayIcon) {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
@ -534,14 +542,12 @@ void MainWindow::psCreateTrayIcon() {
void MainWindow::psFirstShow() {
psCreateTrayIcon();
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
if (QDBusInterface("com.canonical.Unity", "/").isValid()) {
auto snapName = QString::fromLatin1(qgetenv("SNAP_NAME"));
if(snapName.isEmpty()) {
std::vector<QString> possibleDesktopFiles = {
#ifdef TDESKTOP_LAUNCHER_FILENAME
MACRO_TO_STRING(TDESKTOP_LAUNCHER_FILENAME),
#endif // TDESKTOP_LAUNCHER_FILENAME
"kotatogramdesktop.desktop",
MACRO_TO_STRING(TDESKTOP_LAUNCHER_BASENAME) ".desktop",
"Kotatogram.desktop"
};
@ -565,6 +571,7 @@ void MainWindow::psFirstShow() {
} else {
LOG(("Not using Unity Launcher count."));
}
#endif
bool showShadows = true;

View file

@ -12,12 +12,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "facades.h"
#include <QtCore/QBuffer>
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusReply>
#include <QtDBus/QDBusMetaType>
#endif
namespace Platform {
namespace Notifications {
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
namespace {
constexpr auto kService = str_const("org.freedesktop.Notifications");
@ -52,19 +57,19 @@ std::vector<QString> GetServerInformation(
return serverInformation;
}
std::vector<QString> GetCapabilities(
QStringList GetCapabilities(
const std::shared_ptr<QDBusInterface> &notificationInterface) {
QDBusReply<QStringList> capabilitiesReply = notificationInterface
->call(qsl("GetCapabilities"));
if (capabilitiesReply.isValid()) {
return capabilitiesReply.value().toVector().toStdVector();
return capabilitiesReply.value();
} else {
LOG(("Native notification error: %1")
.arg(capabilitiesReply.error().message()));
}
return std::vector<QString>();
return {};
}
QVersionNumber ParseSpecificationVersion(
@ -92,9 +97,8 @@ NotificationData::NotificationData(
, _peerId(peerId)
, _msgId(msgId) {
auto capabilities = GetCapabilities(_notificationInterface);
auto capabilitiesEnd = capabilities.end();
if (ranges::find(capabilities, qsl("body-markup")) != capabilitiesEnd) {
if (capabilities.contains(qsl("body-markup"))) {
_body = subtitle.isEmpty()
? msg.toHtmlEscaped()
: qsl("<b>%1</b>\n%2").arg(subtitle.toHtmlEscaped())
@ -105,24 +109,33 @@ NotificationData::NotificationData(
: qsl("%1\n%2").arg(subtitle).arg(msg);
}
if (ranges::find(capabilities, qsl("actions")) != capabilitiesEnd) {
if (capabilities.contains(qsl("actions"))) {
_actions << qsl("default") << QString();
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
_actions << qsl("mail-reply-sender")
<< tr::lng_notification_reply(tr::now);
connect(_notificationInterface.get(),
SIGNAL(ActionInvoked(uint, QString)),
this, SLOT(notificationClicked(uint)));
if (capabilities.contains(qsl("inline-reply"))) {
_actions << qsl("inline-reply")
<< tr::lng_notification_reply(tr::now);
connect(_notificationInterface.get(),
SIGNAL(NotificationReplied(uint,QString)),
this, SLOT(notificationReplied(uint,QString)));
} else {
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
_actions << qsl("mail-reply-sender")
<< tr::lng_notification_reply(tr::now);
}
}
if (ranges::find(capabilities, qsl("action-icons")) != capabilitiesEnd) {
if (capabilities.contains(qsl("action-icons"))) {
_hints["action-icons"] = true;
}
// suppress system sound if telegram sound activated, otherwise use system sound
if (ranges::find(capabilities, qsl("sound")) != capabilitiesEnd) {
if (capabilities.contains(qsl("sound"))) {
if (Global::SoundNotify()) {
_hints["suppress-sound"] = true;
} else {
@ -131,20 +144,14 @@ NotificationData::NotificationData(
}
}
if (ranges::find(capabilities, qsl("x-canonical-append"))
!= capabilitiesEnd) {
if (capabilities.contains(qsl("x-canonical-append"))) {
_hints["x-canonical-append"] = qsl("true");
}
_hints["category"] = qsl("im.received");
#ifdef TDESKTOP_LAUNCHER_FILENAME
_hints["desktop-entry"] =
qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_FILENAME))
.remove(QRegExp(qsl("\\.desktop$"), Qt::CaseInsensitive));
#else
_hints["desktop-entry"] = qsl("kotatogramdesktop");
#endif
qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_BASENAME));
connect(_notificationInterface.get(),
SIGNAL(NotificationClosed(uint, uint)),
@ -238,6 +245,15 @@ void NotificationData::notificationClicked(uint id) {
}
}
void NotificationData::notificationReplied(uint id, const QString &text) {
if (id == _notificationId) {
const auto manager = _manager;
crl::on_main(manager, [=] {
manager->notificationReplied(_peerId, _msgId, { text, {} });
});
}
}
QDBusArgument &operator<<(QDBusArgument &argument,
const NotificationData::ImageData &imageData) {
argument.beginStructure();
@ -265,24 +281,32 @@ const QDBusArgument &operator>>(const QDBusArgument &argument,
argument.endStructure();
return argument;
}
#endif
bool Supported() {
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
static auto Available = QDBusInterface(
str_const_toString(kService),
str_const_toString(kObjectPath),
str_const_toString(kInterface)).isValid();
return Available;
#else
return false;
#endif
}
std::unique_ptr<Window::Notifications::Manager> Create(
Window::Notifications::System *system) {
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
if (Global::NativeNotifications() && Supported()) {
return std::make_unique<Manager>(system);
}
#endif
return nullptr;
}
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
Manager::Private::Private(Manager *manager, Type type)
: _cachedUserpics(type)
, _manager(manager)
@ -303,14 +327,7 @@ Manager::Private::Private(Manager *manager, Type type)
}
if (!capabilities.empty()) {
const auto capabilitiesString = std::accumulate(
capabilities.begin(),
capabilities.end(),
QString{},
[](auto &s, auto &p) {
return s + (p + qstr(", "));
}).chopped(2);
const auto capabilitiesString = capabilities.join(", ");
LOG(("Notification daemon capabilities: %1").arg(capabilitiesString));
}
}
@ -431,6 +448,7 @@ void Manager::doClearAllFast() {
void Manager::doClearFromHistory(not_null<History*> history) {
_private->clearFromHistory(history);
}
#endif
} // namespace Notifications
} // namespace Platform

Some files were not shown because too many files have changed in this diff Show more