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! Thanks for reporting issues of Telegram Desktop!
To make it easier for us to help you please enter detailed information below. To make it easier for us to help you please enter detailed information below.
--> -->
### Steps to reproduce ### Steps to reproduce
1. 1.
2. 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 git clone $GIT/desktop-app/zlib.git
cd zlib cd zlib
CFLAGS="$MIN_MAC $UNGUARDED" LDFLAGS="$MIN_MAC" ./configure CFLAGS="$MIN_MAC $UNGUARDED" LDFLAGS="$MIN_MAC" ./configure --prefix=$PREFIX
make -j$(nproc) make -j$(nproc)
sudo make install sudo make install
@ -429,4 +429,4 @@ jobs:
name: Upload artifact. name: Upload artifact.
with: with:
name: Telegram name: Telegram
path: $REPO_NAME\out\Debug\artifact\ path: ${{ env.REPO_NAME }}/out/Debug/artifact/

View file

@ -23,7 +23,6 @@ jobs:
env: env:
SDK: "10.0.18362.0" SDK: "10.0.18362.0"
VC: "call vcvars32.bat && cd Libraries" VC: "call vcvars32.bat && cd Libraries"
PY2: 'C:\hostedtoolcache\windows\Python\2.7.16\x64'
GIT: "https://github.com" GIT: "https://github.com"
QT: "5_12_5" QT: "5_12_5"
OPENSSL_VER: "1_1_1" OPENSSL_VER: "1_1_1"
@ -186,6 +185,16 @@ jobs:
run: | run: |
cd %LibrariesPath% 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 git clone %GIT%/telegramdesktop/gyp.git
cd gyp cd gyp
SET PATH=%PY2%;%cd%;%PATH% SET PATH=%PY2%;%cd%;%PATH%

3
.gitmodules vendored
View file

@ -28,9 +28,6 @@
[submodule "Telegram/lib_base"] [submodule "Telegram/lib_base"]
path = Telegram/lib_base path = Telegram/lib_base
url = https://github.com/desktop-app/lib_base.git 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"] [submodule "Telegram/codegen"]
path = Telegram/codegen path = Telegram/codegen
url = https://github.com/desktop-app/codegen.git url = https://github.com/desktop-app/codegen.git

View file

@ -224,7 +224,7 @@ buildRange() {
rm -rf * rm -rf *
cd "$EXTERNAL" 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" cd "$EXTERNAL/range-v3"
cp -r * "$RANGE_PATH/" cp -r * "$RANGE_PATH/"

View file

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

View file

@ -524,6 +524,10 @@ PRIVATE
info/media/info_media_widget.h info/media/info_media_widget.h
info/members/info_members_widget.cpp info/members/info_members_widget.cpp
info/members/info_members_widget.h 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.cpp
info/profile/info_profile_actions.h info/profile/info_profile_actions.h
info/profile/info_profile_cover.cpp info/profile/info_profile_cover.cpp
@ -1096,7 +1100,11 @@ elseif (build_osx)
else() else()
set(bundle_identifier "com.tdesktop.Telegram$<$<CONFIG:Debug>:Debug>") set(bundle_identifier "com.tdesktop.Telegram$<$<CONFIG:Debug>:Debug>")
set(bundle_entitlements "Telegram.entitlements") 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() endif()
set_target_properties(Telegram PROPERTIES 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}) set_target_properties(Packer PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
endif() endif()
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_deleted_message" = "Deleted message";
"lng_pinned_message" = "Pinned message"; "lng_pinned_message" = "Pinned message";
"lng_pinned_poll" = "Pinned poll"; "lng_pinned_poll" = "Pinned poll";
"lng_pinned_quiz" = "Pinned quiz";
"lng_pinned_unpin_sure" = "Would you like to unpin this message?"; "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_sure" = "Would you like to pin this message?";
"lng_pinned_pin" = "Pin"; "lng_pinned_pin" = "Pin";
@ -1294,6 +1295,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_scheduled_messages" = "Scheduled Messages"; "lng_scheduled_messages" = "Scheduled Messages";
"lng_reminder_messages" = "Reminders"; "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" = "Send message now?";
"lng_scheduled_send_now_many#one" = "Send {count} message now?"; "lng_scheduled_send_now_many#one" = "Send {count} message now?";
"lng_scheduled_send_now_many#other" = "Send {count} messages 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_album" = "Send as an album";
"lng_send_photo" = "Send as a photo"; "lng_send_photo" = "Send as a photo";
"lng_send_file" = "Send as a file"; "lng_send_file" = "Send as a file";
"lng_send_media_invalid_files" = "Sorry, no valid files found.";
"lng_forward_choose" = "Choose recipient..."; "lng_forward_choose" = "Choose recipient...";
"lng_forward_cant" = "Sorry, no way to forward here :("; "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_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_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_exceptions_list_title" = "Exceptions";
"lng_removed_list_title" = "Removed users"; "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_launch_exe_dont_ask" = "Don't ask me again";
"lng_polls_anonymous" = "Anonymous Poll"; "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_closed" = "Final results";
"lng_polls_votes_count#one" = "{count} vote"; "lng_polls_votes_count#one" = "{count} vote";
"lng_polls_votes_count#other" = "{count} votes"; "lng_polls_votes_count#other" = "{count} votes";
"lng_polls_votes_none" = "No 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_retract" = "Retract vote";
"lng_polls_stop" = "Stop poll"; "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."; "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#one" = "You can add {count} more option.";
"lng_polls_create_limit#other" = "You can add {count} more options."; "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_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_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_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM.";
"lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}."; "lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}.";
"lng_outdated_now" = "So that Telegram Desktop can update to newer versions."; "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; 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; 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; 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; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto; inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
@ -351,6 +351,7 @@ updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector<int> = Update;
updateTheme#8216fba3 theme:Theme = Update; updateTheme#8216fba3 theme:Theme = Update;
updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update;
updateLoginToken#564fe691 = 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; 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; keyboardButtonBuy#afd93fbb text:string = KeyboardButton;
keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = 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; 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; 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; 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; 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; inputTheme#3c5693e9 id:long access_hash:long = InputTheme;
inputThemeSlug#f5890df1 slug:string = 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; 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.themesNotModified#f41eb622 = account.Themes;
account.themes#7f676421 hash:int themes:Vector<Theme> = 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.loginToken#629f1980 expires:int token:bytes = auth.LoginToken;
auth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken; auth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken;
auth.loginTokenSuccess#390d5c5e authorization:auth.Authorization = 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; 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--- ---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; 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.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector<int> = messages.Messages;
messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates; messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates;
messages.deleteScheduledMessages#59ae2b16 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.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; 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; 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.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.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.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile; 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.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
folders.deleteFolder#1c295881 folder_id:int = Updates; folders.deleteFolder#1c295881 folder_id:int = Updates;
wallet.sendLiteRequest#e2c9d33e body:bytes = wallet.LiteResponse; // LAYER 109
wallet.getKeySecretSalt#b57f346 revoke:Bool = wallet.KeySecretSalt;
// LAYER 108

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -514,6 +514,29 @@ editMediaButton: IconButton {
ripple: defaultRippleAnimation; 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; calendarTitleHeight: boxTitleHeight;
calendarPrevious: IconButton { calendarPrevious: IconButton {
width: calendarTitleHeight; width: calendarTitleHeight;
@ -812,15 +835,15 @@ createPollField: InputField(defaultInputField) {
} }
createPollFieldPadding: margins(22px, 5px, 22px, 5px); createPollFieldPadding: margins(22px, 5px, 22px, 5px);
createPollOptionField: InputField(createPollField) { createPollOptionField: InputField(createPollField) {
textMargins: margins(22px, 8px, 40px, 8px); textMargins: margins(22px, 11px, 40px, 11px);
placeholderMargins: margins(2px, 0px, 2px, 0px); placeholderMargins: margins(2px, 0px, 2px, 0px);
heightMax: 64px; heightMax: 68px;
} }
createPollLimitLabel: FlatLabel(defaultFlatLabel) { createPollLimitLabel: FlatLabel(defaultFlatLabel) {
minWidth: 274px; minWidth: 274px;
align: align(topleft); align: align(topleft);
} }
createPollLimitPadding: margins(22px, 10px, 22px, 5px); createPollLimitPadding: margins(22px, 10px, 22px, 16px);
createPollOptionRemove: CrossButton { createPollOptionRemove: CrossButton {
width: 22px; width: 22px;
height: 22px; height: 22px;
@ -841,7 +864,7 @@ createPollOptionRemove: CrossButton {
color: windowBgOver; color: windowBgOver;
} }
} }
createPollOptionRemovePosition: point(10px, 7px); createPollOptionRemovePosition: point(11px, 9px);
createPollWarning: FlatLabel(defaultFlatLabel) { createPollWarning: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg; textFg: windowSubTextFg;
palette: TextPalette(defaultTextPalette) { palette: TextPalette(defaultTextPalette) {
@ -849,6 +872,8 @@ createPollWarning: FlatLabel(defaultFlatLabel) {
} }
} }
createPollWarningPosition: point(16px, 6px); createPollWarningPosition: point(16px, 6px);
createPollCheckboxMargin: margins(23px, 10px, 23px, 10px);
createPollFieldTitlePadding: margins(22px, 7px, 10px, 6px);
callSettingsButton: IconButton { callSettingsButton: IconButton {
width: 50px; width: 50px;
@ -922,6 +947,33 @@ customBadgeField: InputField(defaultInputField) {
heightMin: 32px; 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) { fontsBoxTextStyle: TextStyle(defaultTextStyle) {
font: font(13px); font: font(13px);
linkFont: 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/toast/toast.h"
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h" #include "ui/wrap/slide_wrap.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/input_fields.h" #include "ui/widgets/input_fields.h"
#include "ui/widgets/shadow.h" #include "ui/widgets/shadow.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/toast/toast.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
@ -42,13 +45,16 @@ public:
Options( Options(
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session); not_null<Main::Session*> session,
bool chooseCorrectEnabled);
[[nodiscard]] bool isValid() const; [[nodiscard]] bool isValid() const;
[[nodiscard]] rpl::producer<bool> isValidChanged() const; [[nodiscard]] rpl::producer<bool> isValidChanged() const;
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const; [[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
void focusFirst(); void focusFirst();
void enableChooseCorrect(bool enabled);
[[nodiscard]] rpl::producer<int> usedCount() const; [[nodiscard]] rpl::producer<int> usedCount() const;
[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const; [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
[[nodiscard]] rpl::producer<> backspaceInFront() const; [[nodiscard]] rpl::producer<> backspaceInFront() const;
@ -56,23 +62,31 @@ public:
private: private:
class Option { class Option {
public: public:
static Option Create( Option(
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session, 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 toggleRemoveAlways(bool toggled);
void enableChooseCorrect(
std::shared_ptr<Ui::RadiobuttonGroup> group);
void show(anim::type animated); void show(anim::type animated);
void destroy(FnMut<void()> done); void destroy(FnMut<void()> done);
//[[nodisacrd]] bool hasShadow() const; [[nodisacrd]] bool hasShadow() const;
//void destroyShadow(); void createShadow();
void destroyShadow();
[[nodiscard]] bool isEmpty() const; [[nodiscard]] bool isEmpty() const;
[[nodiscard]] bool isGood() const; [[nodiscard]] bool isGood() const;
[[nodiscard]] bool isTooLong() const; [[nodiscard]] bool isTooLong() const;
[[nodiscard]] bool isCorrect() const;
[[nodiscard]] bool hasFocus() const; [[nodiscard]] bool hasFocus() const;
void setFocus() const; void setFocus() const;
void clearValue(); void clearValue();
@ -86,29 +100,18 @@ private:
[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const; [[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: private:
Option() = default;
void createShadow();
void createRemove(); void createRemove();
void createWarning(); 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::PlainShadow> _shadow;
base::unique_qptr<Ui::CrossButton> _remove; base::unique_qptr<Ui::CrossButton> _remove;
rpl::variable<bool> *_removeAlways = nullptr; rpl::variable<bool> *_removeAlways = nullptr;
@ -116,23 +119,26 @@ private:
}; };
[[nodiscard]] bool full() const; [[nodiscard]] bool full() const;
//[[nodiscard]] bool correctShadows() const; [[nodiscard]] bool correctShadows() const;
//void fixShadows(); void fixShadows();
void removeEmptyTail(); void removeEmptyTail();
void addEmptyOption(); void addEmptyOption();
void checkLastOption(); void checkLastOption();
void validateState(); void validateState();
void fixAfterErase(); void fixAfterErase();
void destroy(Option &&option); void destroy(std::unique_ptr<Option> option);
void removeDestroyed(not_null<Ui::InputField*> field); void removeDestroyed(not_null<Option*> field);
int findField(not_null<Ui::InputField*> field) const; int findField(not_null<Ui::InputField*> field) const;
[[nodiscard]] auto createChooseCorrectGroup()
-> std::shared_ptr<Ui::RadiobuttonGroup>;
not_null<QWidget*> _outer; not_null<QWidget*> _outer;
not_null<Ui::VerticalLayout*> _container; not_null<Ui::VerticalLayout*> _container;
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
int _position = 0; int _position = 0;
std::vector<Option> _list; std::vector<std::unique_ptr<Option>> _list;
std::set<Option, std::less<>> _destroyed; std::vector<std::unique_ptr<Option>> _destroyed;
rpl::variable<bool> _valid = false; rpl::variable<bool> _valid = false;
rpl::variable<int> _usedCount = 0; rpl::variable<int> _usedCount = 0;
rpl::event_stream<not_null<QWidget*>> _scrollToWidget; rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
@ -187,58 +193,85 @@ void FocusAtEnd(not_null<Ui::InputField*> field) {
field->ensureCursorVisible(); field->ensureCursorVisible();
} }
Options::Option Options::Option::Create( Options::Option::Option(
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session, not_null<Main::Session*> session,
int position) { int position,
auto result = Option(); std::shared_ptr<Ui::RadiobuttonGroup> group)
const auto field = container->insert( : _wrap(container->insert(
position, position,
object_ptr<Ui::SlideWrap<Ui::InputField>>( object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
container, container,
object_ptr<Ui::InputField>( object_ptr<Ui::RpWidget>(container))))
container, , _content(_wrap->entity())
st::createPollOptionField, , _field(
Ui::InputField::Mode::NoNewlines, Ui::CreateChild<Ui::InputField>(
tr::lng_polls_create_option_add()))); _content.get(),
InitField(outer, field->entity(), session); st::createPollOptionField,
field->entity()->setMaxLength(kOptionLimit + kErrorLimit); Ui::InputField::Mode::NoNewlines,
result._field.reset(field); tr::lng_polls_create_option_add())) {
InitField(outer, _field, session);
_field->setMaxLength(kOptionLimit + kErrorLimit);
_field->show();
result.createShadow(); _wrap->hide(anim::type::instant);
result.createRemove();
result.createWarning(); _content->widthValue(
return result; ) | 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 { bool Options::Option::hasShadow() const {
// return (_shadow != nullptr); return (_shadow != nullptr);
//} }
void Options::Option::createShadow() { void Options::Option::createShadow() {
Expects(_field != nullptr); Expects(_content != nullptr);
if (_shadow) { if (_shadow) {
return; return;
} }
const auto value = Ui::CreateChild<Ui::PlainShadow>(field().get()); _shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));
value->show(); _shadow->show();
field()->sizeValue( field()->sizeValue(
) | rpl::start_with_next([=](QSize size) { ) | rpl::start_with_next([=](QSize size) {
const auto left = st::createPollFieldPadding.left(); const auto left = st::createPollFieldPadding.left();
value->setGeometry( _shadow->setGeometry(
left, left,
size.height() - st::lineWidth, size.height() - st::lineWidth,
size.width() - left, size.width() - left,
st::lineWidth); st::lineWidth);
}, value->lifetime()); }, _shadow->lifetime());
_shadow.reset(value);
} }
//void Options::Option::destroyShadow() { void Options::Option::destroyShadow() {
// _shadow = nullptr; _shadow = nullptr;
//} }
void Options::Option::createRemove() { void Options::Option::createRemove() {
using namespace rpl::mappers; using namespace rpl::mappers;
@ -313,6 +346,10 @@ bool Options::Option::isTooLong() const {
return (field()->getLastText().size() > kOptionLimit); return (field()->getLastText().size() > kOptionLimit);
} }
bool Options::Option::isCorrect() const {
return isGood() && _correct && _correct->entity()->Checkbox::checked();
}
bool Options::Option::hasFocus() const { bool Options::Option::hasFocus() const {
return field()->hasFocus(); return field()->hasFocus();
} }
@ -333,8 +370,66 @@ void Options::Option::toggleRemoveAlways(bool toggled) {
*_removeAlways = 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 { not_null<Ui::InputField*> Options::Option::field() const {
return _field->entity(); return _field;
} }
void Options::Option::removePlaceholder() const { void Options::Option::removePlaceholder() const {
@ -344,10 +439,12 @@ void Options::Option::removePlaceholder() const {
PollAnswer Options::Option::toPollAnswer(int index) const { PollAnswer Options::Option::toPollAnswer(int index) const {
Expects(index >= 0 && index < kMaxOptionsCount); Expects(index >= 0 && index < kMaxOptionsCount);
return PollAnswer{ auto result = PollAnswer{
field()->getLastText().trimmed(), field()->getLastText().trimmed(),
QByteArray(1, ('0' + index)) QByteArray(1, ('0' + index))
}; };
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
return result;
} }
rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const { rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
@ -357,10 +454,14 @@ rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
Options::Options( Options::Options(
not_null<QWidget*> outer, not_null<QWidget*> outer,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session) not_null<Main::Session*> session,
bool chooseCorrectEnabled)
: _outer(outer) : _outer(outer)
, _container(container) , _container(container)
, _session(session) , _session(session)
, _chooseCorrectGroup(chooseCorrectEnabled
? createChooseCorrectGroup()
: nullptr)
, _position(_container->count()) { , _position(_container->count()) {
checkLastOption(); checkLastOption();
} }
@ -390,19 +491,18 @@ rpl::producer<> Options::backspaceInFront() const {
} }
void Options::Option::show(anim::type animated) { void Options::Option::show(anim::type animated) {
_field->hide(anim::type::instant); _wrap->show(animated);
_field->show(animated);
} }
void Options::Option::destroy(FnMut<void()> done) { void Options::Option::destroy(FnMut<void()> done) {
if (anim::Disabled() || _field->isHidden()) { if (anim::Disabled() || _wrap->isHidden()) {
Ui::PostponeCall(std::move(done)); Ui::PostponeCall(std::move(done));
return; return;
} }
_field->hide(anim::type::normal); _wrap->hide(anim::type::normal);
base::call_delayed( base::call_delayed(
st::slideWrapDuration * 2, st::slideWrapDuration * 2,
_field.get(), _content.get(),
std::move(done)); std::move(done));
} }
@ -410,8 +510,8 @@ std::vector<PollAnswer> Options::toPollAnswers() const {
auto result = std::vector<PollAnswer>(); auto result = std::vector<PollAnswer>();
result.reserve(_list.size()); result.reserve(_list.size());
auto counter = int(0); auto counter = int(0);
const auto makeAnswer = [&](const Option &option) { const auto makeAnswer = [&](const std::unique_ptr<Option> &option) {
return option.toPollAnswer(counter++); return option->toPollAnswer(counter++);
}; };
ranges::copy( ranges::copy(
_list _list
@ -424,29 +524,45 @@ std::vector<PollAnswer> Options::toPollAnswers() const {
void Options::focusFirst() { void Options::focusFirst() {
Expects(!_list.empty()); 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() { void Options::removeEmptyTail() {
// Only one option at the end of options list can be empty. // Only one option at the end of options list can be empty.
@ -465,7 +581,7 @@ void Options::removeEmptyTail() {
return; return;
} }
if (focusLast) { if (focusLast) {
emptyItem->setFocus(); (*emptyItem)->setFocus();
} }
for (auto i = emptyItem + 1; i != end; ++i) { for (auto i = emptyItem + 1; i != end; ++i) {
destroy(std::move(*i)); destroy(std::move(*i));
@ -474,44 +590,46 @@ void Options::removeEmptyTail() {
fixAfterErase(); fixAfterErase();
} }
void Options::destroy(Option &&option) { void Options::destroy(std::unique_ptr<Option> option) {
const auto field = option.field(); const auto value = option.get();
option.destroy([=] { removeDestroyed(field); }); option->destroy([=] { removeDestroyed(value); });
_destroyed.emplace(std::move(option)); _destroyed.push_back(std::move(option));
} }
void Options::fixAfterErase() { void Options::fixAfterErase() {
Expects(!_list.empty()); Expects(!_list.empty());
const auto last = _list.end() - 1; const auto last = _list.end() - 1;
last->setPlaceholder(); (*last)->setPlaceholder();
last->toggleRemoveAlways(false); (*last)->toggleRemoveAlways(false);
if (last != begin(_list)) { if (last != begin(_list)) {
(last - 1)->setPlaceholder(); (*(last - 1))->setPlaceholder();
(last - 1)->toggleRemoveAlways(false); (*(last - 1))->toggleRemoveAlways(false);
} }
fixShadows();
} }
void Options::addEmptyOption() { void Options::addEmptyOption() {
if (full()) { if (full()) {
return; return;
} else if (!_list.empty() && _list.back().isEmpty()) { } else if (!_list.empty() && _list.back()->isEmpty()) {
return; return;
} }
if (_list.size() > 1) { if (_list.size() > 1) {
(_list.end() - 2)->removePlaceholder(); (*(_list.end() - 2))->removePlaceholder();
(_list.end() - 2)->toggleRemoveAlways(true); (*(_list.end() - 2))->toggleRemoveAlways(true);
} }
_list.push_back(Option::Create( _list.push_back(std::make_unique<Option>(
_outer, _outer,
_container, _container,
_session, _session,
_position + _list.size() + _destroyed.size())); _position + _list.size() + _destroyed.size(),
const auto field = _list.back().field(); _chooseCorrectGroup));
const auto field = _list.back()->field();
QObject::connect(field, &Ui::InputField::submitted, [=] { QObject::connect(field, &Ui::InputField::submitted, [=] {
const auto index = findField(field); const auto index = findField(field);
if (_list[index].isGood() && index + 1 < _list.size()) { if (_list[index]->isGood() && index + 1 < _list.size()) {
_list[index + 1].setFocus(); _list[index + 1]->setFocus();
} }
}); });
QObject::connect(field, &Ui::InputField::changed, [=] { QObject::connect(field, &Ui::InputField::changed, [=] {
@ -534,25 +652,25 @@ void Options::addEmptyOption() {
const auto index = findField(field); const auto index = findField(field);
if (index > 0) { if (index > 0) {
_list[index - 1].setFocus(); _list[index - 1]->setFocus();
} else { } else {
_backspaceInFront.fire({}); _backspaceInFront.fire({});
} }
return base::EventFilterResult::Cancel; return base::EventFilterResult::Cancel;
}); });
_list.back().removeClicks( _list.back()->removeClicks(
) | rpl::start_with_next([=] { ) | rpl::take(1) | rpl::start_with_next([=] {
Ui::PostponeCall(crl::guard(field, [=] { Ui::PostponeCall(crl::guard(field, [=] {
Expects(!_list.empty()); Expects(!_list.empty());
const auto item = begin(_list) + findField(field); const auto item = begin(_list) + findField(field);
if (item == _list.end() - 1) { if (item == _list.end() - 1) {
item->clearValue(); (*item)->clearValue();
return; return;
} }
if (item->hasFocus()) { if ((*item)->hasFocus()) {
(item + 1)->setFocus(); (*(item + 1))->setFocus();
} }
destroy(std::move(*item)); destroy(std::move(*item));
_list.erase(item); _list.erase(item);
@ -561,21 +679,28 @@ void Options::addEmptyOption() {
})); }));
}, field->lifetime()); }, field->lifetime());
_list.back().show((_list.size() == 1) _list.back()->show((_list.size() == 1)
? anim::type::instant ? anim::type::instant
: anim::type::normal); : anim::type::normal);
//fixShadows(); fixShadows();
} }
void Options::removeDestroyed(not_null<Ui::InputField*> field) { void Options::removeDestroyed(not_null<Option*> option) {
_destroyed.erase(_destroyed.find(field)); const auto i = ranges::find(
_destroyed,
option.get(),
&std::unique_ptr<Option>::get);
Assert(i != end(_destroyed));
_destroyed.erase(i);
} }
void Options::validateState() { void Options::validateState() {
checkLastOption(); checkLastOption();
_valid = (ranges::count_if(_list, &Option::isGood) > 1) _valid = (ranges::count_if(_list, &Option::isGood) > 1)
&& (ranges::find_if(_list, &Option::isTooLong) == end(_list)); && (ranges::find_if(_list, &Option::isTooLong) == end(_list))
const auto lastEmpty = !_list.empty() && _list.back().isEmpty(); && (!_chooseCorrectGroup
|| ranges::find_if(_list, &Option::isCorrect) != end(_list));
const auto lastEmpty = !_list.empty() && _list.back()->isEmpty();
_usedCount = _list.size() - (lastEmpty ? 1 : 0); _usedCount = _list.size() - (lastEmpty ? 1 : 0);
} }
@ -599,8 +724,12 @@ void Options::checkLastOption() {
CreatePollBox::CreatePollBox( CreatePollBox::CreatePollBox(
QWidget*, QWidget*,
not_null<Main::Session*> session, not_null<Main::Session*> session,
PollData::Flags chosen,
PollData::Flags disabled,
Api::SendType sendType) Api::SendType sendType)
: _session(session) : _session(session)
, _chosen(chosen)
, _disabled(disabled)
, _sendType(sendType) { , _sendType(sendType) {
} }
@ -668,11 +797,17 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
const auto question = setupQuestion(container); const auto question = setupQuestion(container);
AddDivider(container); AddDivider(container);
AddSkip(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>( const auto options = lifetime().make_state<Options>(
getDelegate()->outerContainer(), getDelegate()->outerContainer(),
container, container,
_session); _session,
(_chosen & PollData::Flag::Quiz));
auto limit = options->usedCount() | rpl::after_next([=](int count) { auto limit = options->usedCount() | rpl::after_next([=](int count) {
setCloseByEscape(!count); setCloseByEscape(!count);
setCloseByOutsideClick(!count); setCloseByOutsideClick(!count);
@ -684,11 +819,68 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
container->resizeToWidth(container->widthNoMargins()); container->resizeToWidth(container->widthNoMargins());
}); });
container->add( container->add(
object_ptr<Ui::FlatLabel>( object_ptr<Ui::DividerLabel>(
container, container,
std::move(limit), object_ptr<Ui::FlatLabel>(
st::createPollLimitLabel), container,
st::createPollLimitPadding); 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 isValidQuestion = [=] {
const auto text = question->getLastText().trimmed(); const auto text = question->getLastText().trimmed();
@ -706,9 +898,16 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
}; };
const auto collectResult = [=] { const auto collectResult = [=] {
auto result = PollData(id); using Flag = PollData::Flag;
auto result = PollData(&_session->data(), id);
result.question = question->getLastText().trimmed(); result.question = question->getLastText().trimmed();
result.answers = options->toPollAnswers(); 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; return result;
}; };
const auto send = [=](Api::SendOptions options) { const auto send = [=](Api::SendOptions options) {
@ -743,7 +942,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
tr::lng_polls_create_button(), tr::lng_polls_create_button(),
[=] { send({}); }); [=] { send({}); });
if (_sendType == Api::SendType::Normal) { if (_sendType == Api::SendType::Normal) {
SetupSendMenu( SetupSendMenuAndShortcuts(
submit.data(), submit.data(),
[=] { return SendMenuType::Scheduled; }, [=] { return SendMenuType::Scheduled; },
sendSilent, sendSilent,

View file

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

View file

@ -488,97 +488,30 @@ void EditCaptionBox::updateEditMediaButton() {
void EditCaptionBox::createEditMediaButton() { void EditCaptionBox::createEditMediaButton() {
const auto callback = [=](FileDialog::OpenResult &&result) { const auto callback = [=](FileDialog::OpenResult &&result) {
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { auto showBoxErrorCallback = [](tr::phrase<> t) {
return; Ui::show(Box<InformBox>(t(tr::now)), Ui::LayerOption::KeepOther);
}
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;
}; };
if (!result.remoteContent.isEmpty()) { auto list = Storage::PreparedList::PreparedFileFromFilesDialog(
std::move(result),
_isAlbum,
std::move(showBoxErrorCallback),
st::sendMediaPreviewSize);
auto list = Storage::PrepareMediaFromImage( if (list) {
QImage(), _preparedList = std::move(*list);
std::move(result.remoteContent), updateEditPreview();
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;
} }
updateEditPreview();
}; };
const auto buttonCallback = [=] { const auto buttonCallback = [=] {
const auto filters = _isAlbum const auto filters = _isAlbum
? QStringList(qsl("Image and Video Files (*.png *.jpg *.mp4)")) ? FileDialog::AlbumFilesFilter()
: QStringList(FileDialog::AllFilesFilter()); : FileDialog::AllFilesFilter();
FileDialog::GetOpenPath( FileDialog::GetOpenPath(
this, this,
tr::lng_choose_file(tr::now), tr::lng_choose_file(tr::now),
filters.join(qsl(";;")), filters,
crl::guard(this, callback)); 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/emoji_suggestions_widget.h"
#include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h" #include "chat_helpers/tabbed_selector.h"
#include "confirm_box.h"
#include "history/view/history_view_schedule_box.h" #include "history/view/history_view_schedule_box.h"
#include "core/file_utilities.h" #include "core/file_utilities.h"
#include "core/mime_type.h" #include "core/mime_type.h"
@ -48,8 +49,102 @@ namespace {
constexpr auto kMinPreviewWidth = 20; constexpr auto kMinPreviewWidth = 20;
constexpr auto kShrinkDuration = crl::time(150); constexpr auto kShrinkDuration = crl::time(150);
constexpr auto kDragDuration = crl::time(200); 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 { class SingleMediaPreview : public Ui::RpWidget {
public: public:
@ -123,7 +218,10 @@ class AlbumThumb {
public: public:
AlbumThumb( AlbumThumb(
const Storage::PreparedFile &file, 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 moveToLayout(const Ui::GroupMediaLayout &layout);
void animateLayoutToInitial(); void animateLayoutToInitial();
@ -141,6 +239,8 @@ public:
void paintFile(Painter &p, int left, int top, int outerWidth); void paintFile(Painter &p, int left, int top, int outerWidth);
bool containsPoint(QPoint position) const; bool containsPoint(QPoint position) const;
bool buttonsContainPoint(QPoint position) const;
ButtonType buttonTypeFromPoint(QPoint position) const;
int distanceTo(QPoint position) const; int distanceTo(QPoint position) const;
bool isPointAfter(QPoint position) const; bool isPointAfter(QPoint position) const;
void moveInAlbum(QPoint to); void moveInAlbum(QPoint to);
@ -148,6 +248,8 @@ public:
void suggestMove(float64 delta, Fn<void()> callback); void suggestMove(float64 delta, Fn<void()> callback);
void finishAnimations(); void finishAnimations();
void updateFileRow(int row);
private: private:
QRect countRealGeometry() const; QRect countRealGeometry() const;
QRect countCurrentGeometry(float64 progress) const; QRect countCurrentGeometry(float64 progress) const;
@ -173,11 +275,19 @@ private:
Ui::Animations::Simple _suggestedMoveAnimation; Ui::Animations::Simple _suggestedMoveAnimation;
int _lastShrinkValue = 0; int _lastShrinkValue = 0;
QRect _lastRectOfButtons;
object_ptr<Ui::IconButton> _editMedia = nullptr;
object_ptr<Ui::IconButton> _deleteMedia = nullptr;
}; };
AlbumThumb::AlbumThumb( AlbumThumb::AlbumThumb(
const Storage::PreparedFile &file, const Storage::PreparedFile &file,
const Ui::GroupMediaLayout &layout) const Ui::GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> editCallback,
Fn<void()> deleteCallback)
: _layout(layout) : _layout(layout)
, _fullPreview(file.preview) , _fullPreview(file.preview)
, _shrinkSize(int(std::ceil(st::historyMessageRadius / 1.4))) , _shrinkSize(int(std::ceil(st::historyMessageRadius / 1.4)))
@ -218,7 +328,11 @@ AlbumThumb::AlbumThumb(
const auto availableFileWidth = st::sendMediaPreviewSize const auto availableFileWidth = st::sendMediaPreviewSize
- st::sendMediaFileThumbSkip - st::sendMediaFileThumbSkip
- st::sendMediaFileThumbSize; - st::sendMediaFileThumbSize
// Right buttons.
- st::sendBoxAlbumGroupButtonFile.width * 2
- st::sendBoxAlbumGroupEditInternalSkip
- st::sendBoxAlbumGroupSkipRight;
const auto filepath = file.path; const auto filepath = file.path;
if (filepath.isEmpty()) { if (filepath.isEmpty()) {
_name = filedialogDefaultName( _name = filedialogDefaultName(
@ -245,6 +359,45 @@ AlbumThumb::AlbumThumb(
_nameWidth = st::semiboldFont->width(_name); _nameWidth = st::semiboldFont->width(_name);
} }
_statusWidth = st::normalFont->width(_status); _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() { void AlbumThumb::resetLayoutAnimation() {
@ -337,6 +490,12 @@ void AlbumThumb::paintInAlbum(
} }
st::historyFileThumbPlay.paintInCenter(p, inner); st::historyFileThumbPlay.paintInCenter(p, inner);
} }
_lastRectOfButtons = PaintAlbumThumbButtons(
p,
{ x, y },
geometry.width(),
shrinkProgress);
} }
void AlbumThumb::prepareCache(QSize size, int shrink) { void AlbumThumb::prepareCache(QSize size, int shrink) {
@ -490,6 +649,12 @@ void AlbumThumb::paintPhoto(Painter &p, int left, int top, int outerWidth) {
top, top,
outerWidth, outerWidth,
_photo); _photo);
_lastRectOfButtons = PaintAlbumThumbButtons(
p,
{ left, top },
st::sendMediaPreviewSize,
0);
} }
void AlbumThumb::paintFile(Painter &p, int left, int top, int outerWidth) { 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); 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 { int AlbumThumb::distanceTo(QPoint position) const {
const auto delta = (_layout.geometry.center() - position); const auto delta = (_layout.geometry.center() - position);
return QPoint::dotProduct(delta, delta); return QPoint::dotProduct(delta, delta);
@ -601,14 +779,12 @@ SingleMediaPreview *SingleMediaPreview::Create(
preview.height())) { preview.height())) {
return nullptr; return nullptr;
} }
const auto sticker = (file.information->filemime == kStickerMimeString)
|| (file.information->filemime == kAnimatedStickerMimeString);
return Ui::CreateChild<SingleMediaPreview>( return Ui::CreateChild<SingleMediaPreview>(
parent, parent,
controller, controller,
preview, preview,
animated, animated,
sticker, Core::IsMimeSticker(file.information->filemime),
animationPreview ? file.path : QString()); animationPreview ? file.path : QString());
} }
@ -960,6 +1136,14 @@ public:
void setSendWay(SendFilesWay way); void setSendWay(SendFilesWay way);
std::vector<int> takeOrder(); std::vector<int> takeOrder();
auto thumbDeleted() {
return _thumbDeleted.events();
}
auto thumbChanged() {
return _thumbChanged.events();
}
protected: protected:
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override;
@ -974,6 +1158,15 @@ private:
void prepareThumbs(); void prepareThumbs();
void updateSizeAnimated(const std::vector<Ui::GroupMediaLayout> &layout); void updateSizeAnimated(const std::vector<Ui::GroupMediaLayout> &layout);
void updateSize(); 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 paintAlbum(Painter &p) const;
void paintPhotos(Painter &p, QRect clip) const; void paintPhotos(Painter &p, QRect clip) const;
@ -1003,6 +1196,9 @@ private:
AlbumThumb *_paintedAbove = nullptr; AlbumThumb *_paintedAbove = nullptr;
QPoint _draggedStartPosition; QPoint _draggedStartPosition;
rpl::event_stream<int> _thumbDeleted;
rpl::event_stream<int> _thumbChanged;
mutable Ui::Animations::Simple _thumbsHeightAnimation; mutable Ui::Animations::Simple _thumbsHeightAnimation;
mutable Ui::Animations::Simple _shrinkAnimation; mutable Ui::Animations::Simple _shrinkAnimation;
mutable Ui::Animations::Simple _finishDragAnimation; mutable Ui::Animations::Simple _finishDragAnimation;
@ -1019,6 +1215,7 @@ SendFilesBox::AlbumPreview::AlbumPreview(
setMouseTracking(true); setMouseTracking(true);
prepareThumbs(); prepareThumbs();
updateSize(); updateSize();
updateFileRows();
} }
void SendFilesBox::AlbumPreview::setSendWay(SendFilesWay way) { void SendFilesBox::AlbumPreview::setSendWay(SendFilesWay way) {
@ -1027,9 +1224,18 @@ void SendFilesBox::AlbumPreview::setSendWay(SendFilesWay way) {
_sendWay = way; _sendWay = way;
} }
updateSize(); updateSize();
updateFileRows();
update(); 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() { std::vector<int> SendFilesBox::AlbumPreview::takeOrder() {
auto reordered = std::vector<std::unique_ptr<AlbumThumb>>(); auto reordered = std::vector<std::unique_ptr<AlbumThumb>>();
reordered.reserve(_thumbs.size()); reordered.reserve(_thumbs.size());
@ -1071,7 +1277,10 @@ void SendFilesBox::AlbumPreview::prepareThumbs() {
for (auto i = 0; i != count; ++i) { for (auto i = 0; i != count; ++i) {
_thumbs.push_back(std::make_unique<AlbumThumb>( _thumbs.push_back(std::make_unique<AlbumThumb>(
_list.files[i], _list.files[i],
layout[i])); layout[i],
this,
[=] { changeThumbByIndex(thumbIndex(thumbUnderCursor())); },
[=] { deleteThumbByIndex(thumbIndex(thumbUnderCursor())); }));
} }
_thumbsHeight = countLayoutHeight(layout); _thumbsHeight = countLayoutHeight(layout);
_photosHeight = ranges::accumulate(ranges::view::all( _photosHeight = ranges::accumulate(ranges::view::all(
@ -1094,9 +1303,27 @@ int SendFilesBox::AlbumPreview::contentTop() const {
AlbumThumb *SendFilesBox::AlbumPreview::findThumb(QPoint position) const { AlbumThumb *SendFilesBox::AlbumPreview::findThumb(QPoint position) const {
position -= QPoint(contentLeft(), contentTop()); 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(); 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) { void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
if (_finishDragAnimation.animating()) { if (_finishDragAnimation.animating()) {
return; return;
@ -1284,6 +1561,10 @@ void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
const auto position = e->pos(); const auto position = e->pos();
cancelDrag(); cancelDrag();
if (const auto thumb = findThumb(position)) { if (const auto thumb = findThumb(position)) {
if (thumb->buttonsContainPoint(e->pos())) {
thumbButtonsCallback(thumb, thumb->buttonTypeFromPoint(e->pos()));
return;
}
_paintedAbove = _suggestedThumb = _draggedThumb = thumb; _paintedAbove = _suggestedThumb = _draggedThumb = thumb;
_draggedStartPosition = position; _draggedStartPosition = position;
_shrinkAnimation.start([=] { update(); }, 0., 1., kShrinkDuration); _shrinkAnimation.start([=] { update(); }, 0., 1., kShrinkDuration);
@ -1291,19 +1572,26 @@ void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
} }
void SendFilesBox::AlbumPreview::mouseMoveEvent(QMouseEvent *e) { void SendFilesBox::AlbumPreview::mouseMoveEvent(QMouseEvent *e) {
if (_sendWay != SendFilesWay::Album) { if (_sendWay == SendFilesWay::Files) {
applyCursor(style::cur_default); applyCursor(style::cur_default);
return; return;
} }
if (_draggedThumb) { const auto isAlbum = (_sendWay == SendFilesWay::Album);
if (isAlbum && _draggedThumb) {
const auto position = e->pos(); const auto position = e->pos();
_draggedThumb->moveInAlbum(position - _draggedStartPosition); _draggedThumb->moveInAlbum(position - _draggedStartPosition);
updateSuggestedDrag(_draggedThumb->center()); updateSuggestedDrag(_draggedThumb->center());
update(); update();
} else { } else {
const auto cursor = findThumb(e->pos()) const auto thumb = findThumb(e->pos());
const auto regularCursor = isAlbum
? style::cur_sizeall ? style::cur_sizeall
: style::cur_default; : style::cur_default;
const auto cursor = thumb
? (thumb->buttonsContainPoint(e->pos())
? style::cur_pointer
: regularCursor)
: style::cur_default;
applyCursor(cursor); applyCursor(cursor);
} }
} }
@ -1405,7 +1693,8 @@ void SendFilesBox::initPreview(rpl::producer<int> desiredPreviewHeight) {
) | rpl::start_with_next([=](int height) { ) | rpl::start_with_next([=](int height) {
setDimensions( setDimensions(
st::boxWideWidth, st::boxWideWidth,
std::min(st::sendMediaPreviewHeightMax, height)); std::min(st::sendMediaPreviewHeightMax, height),
true);
}, lifetime()); }, lifetime());
if (_preview) { if (_preview) {
@ -1414,7 +1703,7 @@ void SendFilesBox::initPreview(rpl::producer<int> desiredPreviewHeight) {
} }
void SendFilesBox::prepareSingleFilePreview() { void SendFilesBox::prepareSingleFilePreview() {
Expects(_list.files.size() == 1); Expects(IsSingleItem(_list));
const auto &file = _list.files[0]; const auto &file = _list.files[0];
const auto media = SingleMediaPreview::Create(this, _controller, file); const auto media = SingleMediaPreview::Create(this, _controller, file);
@ -1442,11 +1731,74 @@ void SendFilesBox::prepareAlbumPreview() {
this, this,
_list, _list,
_sendWay->value())); _sendWay->value()));
addThumbButtonHandlers(wrap);
_preview = wrap; _preview = wrap;
_albumPreview->show(); _albumPreview->show();
setupShadows(wrap, _albumPreview); setupShadows(wrap, _albumPreview);
initPreview(_albumPreview->desiredHeightValue()); 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( void SendFilesBox::setupShadows(
@ -1482,7 +1834,7 @@ void SendFilesBox::setupShadows(
void SendFilesBox::prepare() { void SendFilesBox::prepare() {
_send = addButton(tr::lng_send_button(), [=] { send({}); }); _send = addButton(tr::lng_send_button(), [=] { send({}); });
if (_sendType == Api::SendType::Normal) { if (_sendType == Api::SendType::Normal) {
SetupSendMenu( SetupSendMenuAndShortcuts(
_send, _send,
[=] { return _sendMenuType; }, [=] { return _sendMenuType; },
[=] { sendSilent(); }, [=] { sendSilent(); },
@ -1497,6 +1849,49 @@ void SendFilesBox::prepare() {
_cancelledCallback(); _cancelledCallback();
} }
}, lifetime()); }, 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() { void SendFilesBox::initSendWay() {
@ -1574,7 +1969,7 @@ void SendFilesBox::refreshAlbumMediaCount() {
} }
void SendFilesBox::preparePreview() { void SendFilesBox::preparePreview() {
if (_list.files.size() == 1) { if (IsSingleItem(_list)) {
prepareSingleFilePreview(); prepareSingleFilePreview();
} else { } else {
if (_list.albumIsPossible) { if (_list.albumIsPossible) {
@ -1611,7 +2006,7 @@ void SendFilesBox::setupSendWayControls() {
addRadio(_sendAlbum, SendFilesWay::Album, tr::lng_send_album(tr::now)); addRadio(_sendAlbum, SendFilesWay::Album, tr::lng_send_album(tr::now));
} }
if (!_list.albumIsPossible || _albumPhotosCount > 0) { 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) ? tr::lng_send_photo(tr::now)
: (_albumVideosCount > 0) : (_albumVideosCount > 0)
? tr::lng_send_separate_photos_videos(tr::now) ? 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_separate_photos(tr::now)
: tr::lng_send_photos(tr::now, lt_count, _list.files.size()))); : 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_file(tr::now)
: tr::lng_send_files(tr::now, lt_count, _list.files.size())); : tr::lng_send_files(tr::now, lt_count, _list.files.size()));
} }
@ -1746,16 +2141,9 @@ void SendFilesBox::captionResized() {
update(); 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 { bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
const auto urls = data->hasUrls() ? data->urls() : QList<QUrl>(); 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()) { if (!filesCount && data->hasImage()) {
++filesCount; ++filesCount;
} }
@ -1764,8 +2152,7 @@ bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
return false; return false;
} else if (_list.files.size() > 1 && !_albumPreview) { } else if (_list.files.size() > 1 && !_albumPreview) {
return false; return false;
} else if (_list.files.front().type } else if (!IsFirstAlbumItem(_list)) {
== Storage::PreparedFile::AlbumType::None) {
return false; return false;
} }
return true; return true;
@ -1774,7 +2161,7 @@ bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
bool SendFilesBox::addFiles(not_null<const QMimeData*> data) { bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
auto list = [&] { auto list = [&] {
const auto urls = data->hasUrls() ? data->urls() : QList<QUrl>(); const auto urls = data->hasUrls() ? data->urls() : QList<QUrl>();
auto result = canAddUrls(urls) auto result = CanAddUrls(urls)
? Storage::PrepareMediaList(urls, st::sendMediaPreviewSize) ? Storage::PrepareMediaList(urls, st::sendMediaPreviewSize)
: Storage::PreparedList( : Storage::PreparedList(
Storage::PreparedList::Error::EmptyFile, Storage::PreparedList::Error::EmptyFile,
@ -1792,35 +2179,35 @@ bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
} }
return result; 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; return false;
} else if (list.error != Storage::PreparedList::Error::None) { } else if (!IsSingleItem(list) && !list.albumIsPossible) {
return false; return false;
} else if (list.files.size() != 1 && !list.albumIsPossible) { } else if (!IsFirstAlbumItem(list)) {
return false;
} else if (list.files.front().type
== Storage::PreparedFile::AlbumType::None) {
return false; return false;
} else if (_list.files.size() > 1 && !_albumPreview) { } else if (_list.files.size() > 1 && !_albumPreview) {
return false; return false;
} else if (_list.files.front().type } else if (!IsFirstAlbumItem(_list)) {
== Storage::PreparedFile::AlbumType::None) {
return false; return false;
} }
applyAlbumOrder(); applyAlbumOrder();
delete base::take(_preview); delete base::take(_preview);
_albumPreview = nullptr; _albumPreview = nullptr;
if (_list.files.size() == 1 if (IsSingleItem(_list)
&& _sendWay->value() == SendFilesWay::Photos) { && _sendWay->value() == SendFilesWay::Photos) {
_sendWay->setValue(SendFilesWay::Album); _sendWay->setValue(SendFilesWay::Album);
} }
_list.mergeToEnd(std::move(list)); _list.mergeToEnd(std::move(list), cutToAlbumSize);
_compressConfirm = _compressConfirmInitial; _compressConfirm = _compressConfirmInitial;
refreshAlbumMediaCount(); refreshAllAfterAlbumChanges();
preparePreview();
captionResized();
return true; return true;
} }
@ -1858,7 +2245,9 @@ void SendFilesBox::updateBoxSize() {
} }
void SendFilesBox::keyPressEvent(QKeyEvent *e) { 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 modifiers = e->modifiers();
const auto ctrl = modifiers.testFlag(Qt::ControlModifier) const auto ctrl = modifiers.testFlag(Qt::ControlModifier)
|| modifiers.testFlag(Qt::MetaModifier); || modifiers.testFlag(Qt::MetaModifier);
@ -1937,7 +2326,9 @@ void SendFilesBox::setInnerFocus() {
void SendFilesBox::send( void SendFilesBox::send(
Api::SendOptions options, Api::SendOptions options,
bool ctrlShiftEnter) { bool ctrlShiftEnter) {
if (_sendType == Api::SendType::Scheduled && !options.scheduled) { if ((_sendType == Api::SendType::Scheduled
|| _sendType == Api::SendType::ScheduledToUser)
&& !options.scheduled) {
return sendScheduled(); return sendScheduled();
} }
@ -1982,9 +2373,12 @@ void SendFilesBox::sendSilent() {
} }
void SendFilesBox::sendScheduled() { void SendFilesBox::sendScheduled() {
const auto type = (_sendType == Api::SendType::ScheduledToUser)
? SendMenuType::ScheduledToUser
: _sendMenuType;
const auto callback = [=](Api::SendOptions options) { send(options); }; const auto callback = [=](Api::SendOptions options) { send(options); };
Ui::show( Ui::show(
HistoryView::PrepareScheduleBox(this, _sendMenuType, callback), HistoryView::PrepareScheduleBox(this, type, callback),
Ui::LayerOption::KeepOther); Ui::LayerOption::KeepOther);
} }

View file

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

View file

@ -413,7 +413,9 @@ void ShareBox::keyPressEvent(QKeyEvent *e) {
SendMenuType ShareBox::sendMenuType() const { SendMenuType ShareBox::sendMenuType() const {
const auto selected = _inner->selected(); 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::Reminder
: SendMenuType::Scheduled; : SendMenuType::Scheduled;
} }
@ -424,7 +426,7 @@ void ShareBox::createButtons() {
const auto send = addButton(tr::lng_share_confirm(), [=] { const auto send = addButton(tr::lng_share_confirm(), [=] {
submit({}); submit({});
}); });
SetupSendMenu( SetupSendMenuAndShortcuts(
send, send,
[=] { return sendMenuType(); }, [=] { return sendMenuType(); },
[=] { submitSilent(); }, [=] { submitSilent(); },

View file

@ -435,7 +435,9 @@ void EmojiKeywords::LangPack::applyDifference(
LangPackData &&result) { LangPackData &&result) {
applyData(std::move(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); ApplyDifference(copy, keywords, version);
WriteLocalCache(id, copy); WriteLocalCache(id, copy);
crl::on_main([ crl::on_main([

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qthelp_url.h" #include "base/qthelp_url.h"
#include "base/event_filter.h" #include "base/event_filter.h"
#include "boxes/abstract_box.h" #include "boxes/abstract_box.h"
#include "core/shortcuts.h"
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
@ -33,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QGuiApplication> #include <QtGui/QGuiApplication>
#include <QtGui/QTextBlock> #include <QtGui/QTextBlock>
#include <QtGui/QClipboard> #include <QtGui/QClipboard>
#include <QtWidgets/QApplication>
namespace { namespace {
@ -666,7 +668,7 @@ void MessageLinksParser::apply(
_list = std::move(parsed); _list = std::move(parsed);
} }
void SetupSendMenu( void SetupSendMenuAndShortcuts(
not_null<Ui::RpWidget*> button, not_null<Ui::RpWidget*> button,
Fn<SendMenuType()> type, Fn<SendMenuType()> type,
Fn<void()> silent, Fn<void()> silent,
@ -688,9 +690,9 @@ void SetupSendMenu(
} }
if (schedule && now != SendMenuType::SilentOnly) { if (schedule && now != SendMenuType::SilentOnly) {
(*menu)->addAction( (*menu)->addAction(
(now == SendMenuType::Scheduled (now == SendMenuType::Reminder
? tr::lng_schedule_message(tr::now) ? tr::lng_reminder_message(tr::now)
: tr::lng_reminder_message(tr::now)), : tr::lng_schedule_message(tr::now)),
schedule); schedule);
} }
(*menu)->popup(QCursor::pos()); (*menu)->popup(QCursor::pos());
@ -702,4 +704,46 @@ void SetupSendMenu(
} }
return base::EventFilterResult::Continue; 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, Disabled,
SilentOnly, SilentOnly,
Scheduled, Scheduled,
ScheduledToUser, // For "Send when online".
Reminder, Reminder,
}; };
void SetupSendMenu( void SetupSendMenuAndShortcuts(
not_null<Ui::RpWidget*> button, not_null<Ui::RpWidget*> button,
Fn<SendMenuType()> type, Fn<SendMenuType()> type,
Fn<void()> silent, Fn<void()> silent,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -77,6 +77,12 @@ const auto CommandByName = base::flat_map<QString, Command>{
{ qsl("show_archive") , Command::ShowArchive }, { 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 }, { qsl("save_draft") , Command::SaveDraft },
}; };

View file

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

View file

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

View file

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

View file

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

View file

@ -1302,6 +1302,9 @@ TextForMimeData MediaPoll::clipboardText() const {
} }
QString MediaPoll::errorTextForForward(not_null<PeerData*> peer) 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( return Data::RestrictionError(
peer, peer,
ChatRestriction::f_send_polls ChatRestriction::f_send_polls

View file

@ -754,6 +754,17 @@ int PeerData::slowmodeSecondsLeft() const {
return 0; 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 { namespace Data {
std::vector<ChatRestrictions> ListOfRestrictions() { std::vector<ChatRestrictions> ListOfRestrictions() {

View file

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

View file

@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_poll.h" #include "data/data_poll.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "main/main_session.h" #include "main/main_session.h"
namespace { namespace {
@ -34,14 +36,19 @@ PollAnswer *AnswerByOption(
} // namespace } // 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) { bool PollData::applyChanges(const MTPDpoll &poll) {
Expects(poll.vid().v == id); Expects(poll.vid().v == id);
const auto newQuestion = qs(poll.vquestion()); 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( auto newAnswers = ranges::view::all(
poll.vanswers().v poll.vanswers().v
) | ranges::view::transform([](const MTPPollAnswer &data) { ) | ranges::view::transform([](const MTPPollAnswer &data) {
@ -56,14 +63,14 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
) | ranges::to_vector; ) | ranges::to_vector;
const auto changed1 = (question != newQuestion) const auto changed1 = (question != newQuestion)
|| (closed != newClosed); || (_flags != newFlags);
const auto changed2 = (answers != newAnswers); const auto changed2 = (answers != newAnswers);
if (!changed1 && !changed2) { if (!changed1 && !changed2) {
return false; return false;
} }
if (changed1) { if (changed1) {
question = newQuestion; question = newQuestion;
closed = newClosed; _flags = newFlags;
} }
if (changed2) { if (changed2) {
std::swap(answers, newAnswers); std::swap(answers, newAnswers);
@ -71,6 +78,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
if (const auto current = answerByOption(old.option)) { if (const auto current = answerByOption(old.option)) {
current->votes = old.votes; current->votes = old.votes;
current->chosen = old.chosen; 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) { if (!changed) {
return false; return false;
} }
@ -104,11 +135,11 @@ bool PollData::applyResults(const MTPPollResults &results) {
void PollData::checkResultsReload(not_null<HistoryItem*> item, crl::time now) { void PollData::checkResultsReload(not_null<HistoryItem*> item, crl::time now) {
if (lastResultsUpdate && lastResultsUpdate + kShortPollTimeout > now) { if (lastResultsUpdate && lastResultsUpdate + kShortPollTimeout > now) {
return; return;
} else if (closed) { } else if (closed()) {
return; return;
} }
lastResultsUpdate = now; lastResultsUpdate = now;
Auth().api().reloadPollResults(item); _owner->session().api().reloadPollResults(item);
} }
PollAnswer *PollData::answerByOption(const QByteArray &option) { PollAnswer *PollData::answerByOption(const QByteArray &option) {
@ -137,18 +168,50 @@ bool PollData::applyResultToAnswers(
answer->chosen = voters.is_chosen(); answer->chosen = voters.is_chosen();
changed = true; changed = true;
} }
if (answer->correct != voters.is_correct()) {
answer->correct = voters.is_correct();
changed = true;
}
} else if (const auto existing = answerByOption(option)) { } else if (const auto existing = answerByOption(option)) {
answer->chosen = existing->chosen; answer->chosen = existing->chosen;
answer->correct = existing->correct;
} }
return changed; return changed;
}); });
} }
void PollData::setFlags(Flags flags) {
if (_flags != flags) {
_flags = flags;
++version;
}
}
PollData::Flags PollData::flags() const {
return _flags;
}
bool PollData::voted() const { bool PollData::voted() const {
return ranges::find(answers, true, &PollAnswer::chosen) != end(answers); 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) { const auto convert = [](const PollAnswer &answer) {
return MTP_pollAnswer( return MTP_pollAnswer(
MTP_string(answer.text), MTP_string(answer.text),
@ -160,9 +223,14 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll) {
poll->answers, poll->answers,
ranges::back_inserter(answers), ranges::back_inserter(answers),
convert); 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( return MTP_poll(
MTP_long(poll->id), MTP_long(poll->id),
MTP_flags(MTPDpoll::Flag::f_closed), MTP_flags(flags),
MTP_string(poll->question), MTP_string(poll->question),
MTP_vector<MTPPollAnswer>(answers)); MTP_vector<MTPPollAnswer>(answers));
} }

View file

@ -7,11 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
namespace Data {
class Session;
} // namespace Data
struct PollAnswer { struct PollAnswer {
QString text; QString text;
QByteArray option; QByteArray option;
int votes = 0; int votes = 0;
bool chosen = false; bool chosen = false;
bool correct = false;
}; };
inline bool operator==(const PollAnswer &a, const PollAnswer &b) { inline bool operator==(const PollAnswer &a, const PollAnswer &b) {
@ -24,23 +29,39 @@ inline bool operator!=(const PollAnswer &a, const PollAnswer &b) {
} }
struct PollData { 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 applyChanges(const MTPDpoll &poll);
bool applyResults(const MTPPollResults &results); bool applyResults(const MTPPollResults &results);
void checkResultsReload(not_null<HistoryItem*> item, crl::time now); void checkResultsReload(not_null<HistoryItem*> item, crl::time now);
PollAnswer *answerByOption(const QByteArray &option); [[nodiscard]] PollAnswer *answerByOption(const QByteArray &option);
const PollAnswer *answerByOption(const QByteArray &option) const; [[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; PollId id = 0;
QString question; QString question;
std::vector<PollAnswer> answers; std::vector<PollAnswer> answers;
std::vector<not_null<UserData*>> recentVoters;
int totalVoters = 0; int totalVoters = 0;
bool closed = false; std::vector<QByteArray> sendingVotes;
QByteArray sendingVote;
crl::time lastResultsUpdate = 0; crl::time lastResultsUpdate = 0;
int version = 0; int version = 0;
@ -52,6 +73,9 @@ private:
const MTPPollAnswerVoters &result, const MTPPollAnswerVoters &result,
bool isMinResults); 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]] rpl::producer<> updates(not_null<History*> history);
[[nodiscard]] Data::MessagesSlice list(not_null<History*> history); [[nodiscard]] Data::MessagesSlice list(not_null<History*> history);
static constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
private: private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>; using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct List { struct List {

View file

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

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h" #include "core/application.h"
#include "media/clip/media_clip_reader.h" #include "media/clip/media_clip_reader.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
#include "data/data_peer.h" #include "data/data_peer.h"
@ -115,6 +116,19 @@ void activateBotCommand(
})); }));
} break; } 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::SwitchInlineSame:
case ButtonType::SwitchInline: { case ButtonType::SwitchInline: {
if (auto m = App::main()) { if (auto m = App::main()) {

View file

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

View file

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

View file

@ -149,6 +149,8 @@ historyReceivedIcon: icon {{ "history_received", historyOutIconFg, point(2px, 4p
historyReceivedSelectedIcon: icon {{ "history_received", historyOutIconFgSelected, point(2px, 4px) }}; historyReceivedSelectedIcon: icon {{ "history_received", historyOutIconFgSelected, point(2px, 4px) }};
historyReceivedInvertedIcon: icon {{ "history_received", historyIconFgInverted, point(2px, 4px) }}; historyReceivedInvertedIcon: icon {{ "history_received", historyIconFgInverted, point(2px, 4px) }};
historyScheduledUntilOnlineStateSpace: 17px;
historyViewsSpace: 11px; historyViewsSpace: 11px;
historyViewsWidth: 20px; historyViewsWidth: 20px;
historyViewsTop: -15px; historyViewsTop: -15px;
@ -539,9 +541,9 @@ historyPollQuestionStyle: TextStyle(defaultTextStyle) {
} }
historyPollAnswerStyle: defaultTextStyle; historyPollAnswerStyle: defaultTextStyle;
historyPollQuestionTop: 7px; historyPollQuestionTop: 7px;
historyPollSubtitleSkip: 2px; historyPollSubtitleSkip: 4px;
historyPollAnswerPadding: margins(31px, 10px, 0px, 10px); historyPollAnswerPadding: margins(31px, 10px, 0px, 10px);
historyPollAnswersSkip: 3px; historyPollAnswersSkip: 2px;
historyPollPercentFont: semiboldFont; historyPollPercentFont: semiboldFont;
historyPollPercentSkip: 6px; historyPollPercentSkip: 6px;
historyPollPercentTop: 0px; historyPollPercentTop: 0px;
@ -575,6 +577,17 @@ historyPollRippleOut: RippleAnimation(defaultRippleAnimation) {
color: msgWaveformOutInactive; color: msgWaveformOutInactive;
} }
historyPollRippleOpacity: 0.3; 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) { boxAttachEmoji: IconButton(historyAttachEmoji) {
width: 30px; width: 30px;

View file

@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/inactive_press.h" #include "ui/inactive_press.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "window/window_peer_menu.h" #include "window/window_peer_menu.h"
#include "window/window_controller.h"
#include "boxes/confirm_box.h" #include "boxes/confirm_box.h"
#include "boxes/report_box.h" #include "boxes/report_box.h"
#include "boxes/sticker_set_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 media = item->media()) {
if (const auto poll = media->poll()) { if (const auto poll = media->poll()) {
if (!poll->closed) { if (!poll->closed()) {
if (poll->voted()) { if (poll->voted() && !poll->quiz()) {
_menu->addAction(tr::lng_polls_retract(tr::now), [=] { _menu->addAction(tr::lng_polls_retract(tr::now), [=] {
session().api().sendPollVotes(itemId, {}); session().api().sendPollVotes(itemId, {});
}); });
@ -2418,6 +2419,12 @@ void HistoryInner::elementStartStickerLoop(
_animatedStickersPlayed.emplace(view->data()); _animatedStickersPlayed.emplace(view->data());
} }
void HistoryInner::elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context) {
_controller->showPollResults(poll, context);
}
auto HistoryInner::getSelectionState() const auto HistoryInner::getSelectionState() const
-> HistoryView::TopBarWidget::SelectedState { -> HistoryView::TopBarWidget::SelectedState {
auto result = HistoryView::TopBarWidget::SelectedState {}; auto result = HistoryView::TopBarWidget::SelectedState {};
@ -3278,6 +3285,13 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
Instance->elementStartStickerLoop(view); 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 from,
int till) const; int till) const;
void elementStartStickerLoop(not_null<const Element*> view); void elementStartStickerLoop(not_null<const Element*> view);
void elementShowPollResults(
not_null<PollData*> poll,
FullMsgId context);
void updateBotInfo(bool recount = true); 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 "window/window_session_controller.h"
#include "core/crash_reports.h" #include "core/crash_reports.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_messages.h" #include "data/data_messages.h"
#include "data/data_media_types.h" #include "data/data_media_types.h"
@ -862,6 +863,21 @@ QDateTime ItemDateTime(not_null<const HistoryItem*> item) {
return base::unixtime::parse(item->date()); 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( ClickHandlerPtr goToMessageClickHandler(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
FullMsgId returnToId) { FullMsgId returnToId) {

View file

@ -374,6 +374,8 @@ private:
}; };
QDateTime ItemDateTime(not_null<const HistoryItem*> item); 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( ClickHandlerPtr goToMessageClickHandler(
not_null<PeerData*> peer, not_null<PeerData*> peer,

View file

@ -899,6 +899,21 @@ void HistoryMessageReplyMarkup::createFromButtonRows(
}, [&](const MTPDinputKeyboardButtonUrlAuth &data) { }, [&](const MTPDinputKeyboardButtonUrlAuth &data) {
LOG(("API Error: inputKeyboardButtonUrlAuth received.")); LOG(("API Error: inputKeyboardButtonUrlAuth received."));
// Should not get those for the users. // 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()) { if (!row.empty()) {

View file

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

View file

@ -314,7 +314,7 @@ HistoryWidget::HistoryWidget(
_fieldBarCancel->addClickHandler([=] { cancelFieldAreaState(); }); _fieldBarCancel->addClickHandler([=] { cancelFieldAreaState(); });
_send->addClickHandler([=] { sendButtonClicked(); }); _send->addClickHandler([=] { sendButtonClicked(); });
SetupSendMenu( SetupSendMenuAndShortcuts(
_send, _send,
[=] { return sendButtonMenuType(); }, [=] { return sendButtonMenuType(); },
[=] { sendSilent(); }, [=] { sendSilent(); },
@ -3066,6 +3066,8 @@ SendMenuType HistoryWidget::sendMenuType() const {
? SendMenuType::Disabled ? SendMenuType::Disabled
: _peer->isSelf() : _peer->isSelf()
? SendMenuType::Reminder ? SendMenuType::Reminder
: HistoryView::CanScheduleUntilOnline(_peer)
? SendMenuType::ScheduledToUser
: SendMenuType::Scheduled; : SendMenuType::Scheduled;
} }
@ -6900,7 +6902,13 @@ void HistoryWidget::drawPinnedBar(Painter &p) {
} }
p.setPen(st::historyReplyNameFg); p.setPen(st::historyReplyNameFg);
p.setFont(st::msgServiceNameFont); 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.setPen(st::historyComposeAreaFg);
p.setTextPalette(st::historyComposeAreaPalette); p.setTextPalette(st::historyComposeAreaPalette);

View file

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

View file

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

View file

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

View file

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

View file

@ -1269,7 +1269,12 @@ int Message::infoWidth() const {
result += st::historySendStateSpace; 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; result += st::historySendStateSpace;
} }
return result; 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 "history/view/history_view_schedule_box.h"
#include "api/api_common.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 "lang/lang_keys.h"
#include "base/unixtime.h" #include "base/unixtime.h"
#include "boxes/calendar_box.h" #include "boxes/calendar_box.h"
#include "ui/widgets/input_fields.h" #include "ui/widgets/input_fields.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/padding_wrap.h" #include "ui/wrap/padding_wrap.h"
#include "chat_helpers/message_field.h" #include "chat_helpers/message_field.h"
#include "styles/style_info.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_history.h" #include "styles/style_history.h"
#include <QGuiApplication>
namespace HistoryView { namespace HistoryView {
namespace { 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 } // namespace
TimeId DefaultScheduleTime() { TimeId DefaultScheduleTime() {
return base::unixtime::now() + 600; 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( void ScheduleBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
SendMenuType type, SendMenuType type,
FnMut<void(Api::SendOptions)> done, FnMut<void(Api::SendOptions)> done,
TimeId time) { TimeId time) {
box->setTitle((type == SendMenuType::Scheduled) box->setTitle((type == SendMenuType::Reminder)
? tr::lng_schedule_title() ? tr::lng_remind_title()
: tr::lng_remind_title()); : tr::lng_schedule_title());
box->setWidth(st::boxWideWidth); box->setWidth(st::boxWideWidth);
const auto date = Ui::CreateChild<rpl::variable<QDate>>( const auto date = Ui::CreateChild<rpl::variable<QDate>>(
@ -637,10 +665,15 @@ void ScheduleBox(
} }
return result; 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(); auto result = Api::SendOptions();
result.silent = silent; result.silent = silent || ctrl;
result.scheduled = collect(); result.scheduled = untilOnline
? Data::ScheduledMessages::kScheduledUntilOnlineTimestamp
: collect();
if (!result.scheduled) { if (!result.scheduled) {
return; return;
} }
@ -658,12 +691,20 @@ void ScheduleBox(
const auto submit = box->addButton(tr::lng_settings_save(), [=] { const auto submit = box->addButton(tr::lng_settings_save(), [=] {
save(false); save(false);
}); });
SetupSendMenu( SetupSendMenuAndShortcuts(
submit.data(), submit.data(),
[=] { return SendMenuType::SilentOnly; }, [=] { return SendMenuType::SilentOnly; },
[=] { save(true); }, [=] { save(true); },
nullptr); nullptr);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); 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 } // namespace HistoryView

View file

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

View file

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

View file

@ -199,6 +199,10 @@ void ServiceMessagePainter::paintDate(Painter &p, const QDateTime &date, int y,
paintPreparedDate(p, dateText, dateTextWidth, y, w); 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) { void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w) {
paintPreparedDate(p, dateText, dateTextWidth, y, w); paintPreparedDate(p, dateText, dateTextWidth, y, w);
} }

View file

@ -63,6 +63,7 @@ struct PaintContext {
class ServiceMessagePainter { class ServiceMessagePainter {
public: public:
static void paintDate(Painter &p, const QDateTime &date, int y, int w); 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 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); 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); 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 } // namespace
Document::Document( Document::Document(
@ -104,7 +133,8 @@ void Document::createComponents(bool caption) {
} }
void Document::fillNamedFromData(HistoryDocumentNamed *named) { 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); named->_namew = st::semiboldFont->width(nameString);
} }

View file

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

View file

@ -14,11 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "calls/calls_instance.h" #include "calls/calls_instance.h"
#include "ui/text_options.h" #include "ui/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/effects/animations.h" #include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h" #include "ui/effects/radial_animation.h"
#include "ui/effects/ripple_animation.h" #include "ui/effects/ripple_animation.h"
#include "data/data_media_types.h" #include "data/data_media_types.h"
#include "data/data_poll.h" #include "data/data_poll.h"
#include "data/data_user.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "layout.h" #include "layout.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -29,6 +31,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace HistoryView { namespace HistoryView {
namespace { namespace {
constexpr auto kShowRecentVotersCount = 3;
struct PercentCounterItem { struct PercentCounterItem {
int index = 0; int index = 0;
int percent = 0; int percent = 0;
@ -112,6 +116,8 @@ struct Poll::AnswerAnimation {
anim::value percent; anim::value percent;
anim::value filling; anim::value filling;
anim::value opacity; anim::value opacity;
bool chosen = false;
bool correct = false;
}; };
struct Poll::AnswersAnimation { struct Poll::AnswersAnimation {
@ -132,7 +138,7 @@ struct Poll::SendingAnimation {
struct Poll::Answer { struct Poll::Answer {
Answer(); Answer();
void fillText(const PollAnswer &original); void fillData(not_null<PollData*> poll, const PollAnswer &original);
Ui::Text::String text; Ui::Text::String text;
QByteArray option; QByteArray option;
@ -142,7 +148,10 @@ struct Poll::Answer {
float64 filling = 0.; float64 filling = 0.;
QString votesPercentString; QString votesPercentString;
bool chosen = false; bool chosen = false;
bool correct = false;
bool selected = false;
ClickHandlerPtr handler; ClickHandlerPtr handler;
Ui::Animations::Simple selectedAnimation;
mutable std::unique_ptr<Ui::RippleAnimation> ripple; mutable std::unique_ptr<Ui::RippleAnimation> ripple;
}; };
@ -159,7 +168,11 @@ Poll::SendingAnimation::SendingAnimation(
Poll::Answer::Answer() : text(st::msgMinWidth / 2) { 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) { if (!text.isEmpty() && text.toString() == original.text) {
return; return;
} }
@ -174,7 +187,15 @@ Poll::Poll(
not_null<PollData*> poll) not_null<PollData*> poll)
: Media(parent) : Media(parent)
, _poll(poll) , _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); history()->owner().registerPollView(_poll, _parent);
} }
@ -202,13 +223,17 @@ QSize Poll::countOptimalSize() {
+ st::historyPollAnswerPadding.bottom(); + st::historyPollAnswerPadding.bottom();
}), 0); }), 0);
const auto bottomButtonHeight = inlineFooter()
? 0
: st::historyPollBottomButtonSkip;
auto minHeight = st::historyPollQuestionTop auto minHeight = st::historyPollQuestionTop
+ _question.minHeight() + _question.minHeight()
+ st::historyPollSubtitleSkip + st::historyPollSubtitleSkip
+ st::msgDateFont->height + st::msgDateFont->height
+ st::historyPollAnswersSkip + st::historyPollAnswersSkip
+ answersHeight + answersHeight
+ st::msgPadding.bottom() + st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height + st::msgDateFont->height
+ st::msgPadding.bottom(); + st::msgPadding.bottom();
if (!isBubbleTop()) { if (!isBubbleTop()) {
@ -218,13 +243,28 @@ QSize Poll::countOptimalSize() {
} }
bool Poll::showVotes() const { bool Poll::showVotes() const {
return _voted || _closed; return _voted || (_flags & PollData::Flag::Closed);
} }
bool Poll::canVote() const { bool Poll::canVote() const {
return !showVotes() && IsServerMsgId(_parent->data()->id); 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( int Poll::countAnswerTop(
const Answer &answer, const Answer &answer,
int innerWidth) const { int innerWidth) const {
@ -278,6 +318,9 @@ QSize Poll::countCurrentSize(int newWidth) {
return countAnswerHeight(answer, innerWidth); return countAnswerHeight(answer, innerWidth);
}), 0); }), 0);
const auto bottomButtonHeight = inlineFooter()
? 0
: st::historyPollBottomButtonSkip;
auto newHeight = st::historyPollQuestionTop auto newHeight = st::historyPollQuestionTop
+ _question.countHeight(innerWidth) + _question.countHeight(innerWidth)
+ st::historyPollSubtitleSkip + st::historyPollSubtitleSkip
@ -285,6 +328,7 @@ QSize Poll::countCurrentSize(int newWidth) {
+ st::historyPollAnswersSkip + st::historyPollAnswersSkip
+ answersHeight + answersHeight
+ st::historyPollTotalVotesSkip + st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height + st::msgDateFont->height
+ st::msgPadding.bottom(); + st::msgPadding.bottom();
if (!isBubbleTop()) { if (!isBubbleTop()) {
@ -309,13 +353,22 @@ void Poll::updateTexts() {
_poll->question, _poll->question,
options); options);
} }
if (_closed != _poll->closed || _subtitle.isEmpty()) { if (_flags != _poll->flags() || _subtitle.isEmpty()) {
_closed = _poll->closed; using Flag = PollData::Flag;
_flags = _poll->flags();
_subtitle.setText( _subtitle.setText(
st::msgDateTextStyle, 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(); updateAnswers();
updateVotes(); 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() { void Poll::updateAnswers() {
const auto changed = !ranges::equal( const auto changed = !ranges::equal(
_answers, _answers,
@ -334,16 +397,16 @@ void Poll::updateAnswers() {
if (!changed) { if (!changed) {
auto &&answers = ranges::view::zip(_answers, _poll->answers); auto &&answers = ranges::view::zip(_answers, _poll->answers);
for (auto &&[answer, original] : answers) { for (auto &&[answer, original] : answers) {
answer.fillText(original); answer.fillData(_poll, original);
} }
return; return;
} }
_answers = ranges::view::all( _answers = ranges::view::all(
_poll->answers _poll->answers
) | ranges::view::transform([](const PollAnswer &answer) { ) | ranges::view::transform([&](const PollAnswer &answer) {
auto result = Answer(); auto result = Answer();
result.option = answer.option; result.option = answer.option;
result.fillText(answer); result.fillData(_poll, answer);
return result; return result;
}) | ranges::to_vector; }) | ranges::to_vector;
@ -355,12 +418,66 @@ void Poll::updateAnswers() {
} }
ClickHandlerPtr Poll::createAnswerClickHandler( ClickHandlerPtr Poll::createAnswerClickHandler(
const Answer &answer) const { const Answer &answer) {
const auto option = answer.option; const auto option = answer.option;
const auto itemId = _parent->data()->fullId(); if (_flags & PollData::Flag::MultiChoice) {
return std::make_shared<LambdaClickHandler>([=] { return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
history()->session().api().sendPollVotes(itemId, { option }); 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() { void Poll::updateVotes() {
@ -370,21 +487,23 @@ void Poll::updateVotes() {
} }
void Poll::checkSendingAnimation() const { void Poll::checkSendingAnimation() const {
const auto &sending = _poll->sendingVote; const auto &sending = _poll->sendingVotes;
if (sending.isEmpty() == !_sendingAnimation) { const auto sendingRadial = (sending.size() == 1)
&& !(_flags & PollData::Flag::MultiChoice);
if (sendingRadial == (_sendingAnimation != nullptr)) {
if (_sendingAnimation) { if (_sendingAnimation) {
_sendingAnimation->option = sending; _sendingAnimation->option = sending.front();
} }
return; return;
} }
if (sending.isEmpty()) { if (!sendingRadial) {
if (!_answersAnimation) { if (!_answersAnimation) {
_sendingAnimation = nullptr; _sendingAnimation = nullptr;
} }
return; return;
} }
_sendingAnimation = std::make_unique<SendingAnimation>( _sendingAnimation = std::make_unique<SendingAnimation>(
sending, sending.front(),
[=] { radialAnimationCallback(); }); [=] { radialAnimationCallback(); });
_sendingAnimation->animation.start(); _sendingAnimation->animation.start();
} }
@ -394,9 +513,17 @@ void Poll::updateTotalVotes() {
return; return;
} }
_totalVotes = _poll->totalVoters; _totalVotes = _poll->totalVoters;
const auto quiz = _poll->quiz();
const auto string = !_totalVotes const auto string = !_totalVotes
? tr::lng_polls_votes_none(tr::now) ? (quiz
: tr::lng_polls_votes_count(tr::now, lt_count_short, _totalVotes); ? 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); _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); p.setPen(regular);
_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width()); _subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());
paintRecentVoters(p, padding.left() + _subtitle.maxWidth(), tshift, selection);
tshift += st::msgDateFont->height + st::historyPollAnswersSkip; tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
const auto progress = _answersAnimation const auto progress = _answersAnimation
@ -517,23 +645,80 @@ void Poll::draw(Painter &p, const QRect &r, TextSelection selection, crl::time m
selection); selection);
tshift += height; tshift += height;
} }
if (!_totalVotesLabel.isEmpty()) { if (!inlineFooter()) {
paintBottom(p, padding.left(), tshift, paintw, selection);
} else if (!_totalVotesLabel.isEmpty()) {
tshift += st::msgPadding.bottom(); 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); p.setPen(regular);
_totalVotesLabel.drawLeftElided( _totalVotesLabel.draw(p, left, stringtop, paintw, style::al_top);
p, } else {
padding.left(), const auto link = showVotes()
tshift, ? _showResultsLink
std::min( : canSendVotes()
_totalVotesLabel.maxWidth(), ? _sendVotesLink
paintw - _parent->infoWidth()), : nullptr;
width()); 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 { void Poll::resetAnswersAnimation() const {
_answersAnimation = nullptr; _answersAnimation = nullptr;
if (_poll->sendingVote.isEmpty()) { if (_poll->sendingVotes.size() != 1
|| (_flags & PollData::Flag::MultiChoice)) {
_sendingAnimation = nullptr; _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( int Poll::paintAnswer(
Painter &p, Painter &p,
const Answer &answer, const Answer &answer,
@ -592,6 +807,8 @@ int Poll::paintAnswer(
p.setOpacity(sqrt(opacity)); p.setOpacity(sqrt(opacity));
paintFilling( paintFilling(
p, p,
animation->chosen,
animation->correct,
animation->filling.current(), animation->filling.current(),
left, left,
top, top,
@ -613,6 +830,8 @@ int Poll::paintAnswer(
selection); selection);
paintFilling( paintFilling(
p, p,
answer.chosen,
answer.correct,
answer.filling, answer.filling,
left, left,
top, top,
@ -644,9 +863,13 @@ void Poll::paintRadio(
const auto over = ClickHandler::showAsActive(answer.handler); const auto over = ClickHandler::showAsActive(answer.handler);
const auto &regular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg); 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(); 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.)); 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) { if (_sendingAnimation && _sendingAnimation->option == answer.option) {
@ -665,10 +888,24 @@ void Poll::paintRadio(
state.arcLength); state.arcLength);
} }
} else { } else {
auto pen = regular->p; if (checkmark < 1.) {
pen.setWidth(st.thickness); auto pen = regular->p;
p.setPen(pen); pen.setWidth(st.thickness);
p.drawEllipse(rect); 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); p.setOpacity(o);
@ -696,6 +933,8 @@ void Poll::paintPercent(
void Poll::paintFilling( void Poll::paintFilling(
Painter &p, Painter &p,
bool chosen,
bool correct,
float64 filling, float64 filling,
int left, int left,
int top, int top,
@ -712,15 +951,36 @@ void Poll::paintFilling(
top += st::historyPollAnswerPadding.top(); top += st::historyPollAnswerPadding.top();
const auto bar = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
PainterHighQualityEnabler hq(p); PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.setBrush(bar); const auto thickness = st::historyPollFillingHeight;
const auto max = awidth - st::historyPollFillingRight; const auto max = awidth - st::historyPollFillingRight;
const auto size = anim::interpolate(st::historyPollFillingMin, max, filling); const auto size = anim::interpolate(st::historyPollFillingMin, max, filling);
const auto radius = st::historyPollFillingRadius; const auto radius = st::historyPollFillingRadius;
const auto ftop = bottom - st::historyPollFillingBottom - st::historyPollFillingHeight; const auto ftop = bottom - st::historyPollFillingBottom - thickness;
p.drawRoundedRect(aleft, ftop, size, st::historyPollFillingHeight, radius, radius);
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 { bool Poll::answerVotesChanged() const {
@ -748,6 +1008,8 @@ void Poll::saveStateInAnimation() const {
result.percent = show ? float64(answer.votesPercent) : 0.; result.percent = show ? float64(answer.votesPercent) : 0.;
result.filling = show ? answer.filling : 0.; result.filling = show ? answer.filling : 0.;
result.opacity = show ? 1. : 0.; result.opacity = show ? 1. : 0.;
result.chosen = answer.chosen;
result.correct = answer.correct;
return result; return result;
}; };
ranges::transform( ranges::transform(
@ -761,7 +1023,7 @@ bool Poll::checkAnimationStart() const {
// Skip initial changes. // Skip initial changes.
return false; return false;
} }
const auto result = (showVotes() != (_poll->voted() || _poll->closed)) const auto result = (showVotes() != (_poll->voted() || _poll->closed()))
|| answerVotesChanged(); || answerVotesChanged();
if (result) { if (result) {
saveStateInAnimation(); saveStateInAnimation();
@ -780,6 +1042,8 @@ void Poll::startAnswersAnimation() const {
data.percent.start(show ? float64(answer.votesPercent) : 0.); data.percent.start(show ? float64(answer.votesPercent) : 0.);
data.filling.start(show ? answer.filling : 0.); data.filling.start(show ? answer.filling : 0.);
data.opacity.start(show ? 1. : 0.); data.opacity.start(show ? 1. : 0.);
data.chosen = data.chosen || answer.chosen;
data.correct = data.correct || answer.correct;
} }
_answersAnimation->progress.start( _answersAnimation->progress.start(
[=] { history()->owner().requestViewRepaint(_parent); }, [=] { history()->owner().requestViewRepaint(_parent); },
@ -790,7 +1054,7 @@ void Poll::startAnswersAnimation() const {
TextState Poll::textState(QPoint point, StateRequest request) const { TextState Poll::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent); auto result = TextState(_parent);
if (!_poll->sendingVote.isEmpty()) { if (!_poll->sendingVotes.empty()) {
return result; return result;
} }
@ -819,15 +1083,39 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
result.customTooltip = true; result.customTooltip = true;
using Flag = Ui::Text::StateRequest::Flag; using Flag = Ui::Text::StateRequest::Flag;
if (request.flags & Flag::LookupCustomTooltip) { if (request.flags & Flag::LookupCustomTooltip) {
const auto quiz = _poll->quiz();
result.customTooltipText = answer.votes result.customTooltipText = answer.votes
? tr::lng_polls_votes_count(tr::now, lt_count_decimal, answer.votes) ? (quiz
: tr::lng_polls_votes_none(tr::now); ? 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; return result;
} }
tshift += height; 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; return result;
} }
@ -842,6 +1130,8 @@ void Poll::clickHandlerPressedChanged(
&Answer::handler); &Answer::handler);
if (i != end(_answers)) { if (i != end(_answers)) {
toggleRipple(*i, pressed); 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); const auto top = countAnswerTop(answer, innerWidth);
answer.ripple->add(_lastLinkPoint - QPoint(0, top)); answer.ripple->add(_lastLinkPoint - QPoint(0, top));
} else { } else if (answer.ripple) {
if (answer.ripple) { answer.ripple->lastStop();
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 #pragma once
#include "history/view/media/history_view_media.h" #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 { namespace HistoryView {
class Poll : public Media { class Poll : public Media, public base::has_weak_ptr {
public: public:
Poll( Poll(
not_null<Element*> parent, not_null<Element*> parent,
@ -53,6 +57,7 @@ private:
[[nodiscard]] bool showVotes() const; [[nodiscard]] bool showVotes() const;
[[nodiscard]] bool canVote() const; [[nodiscard]] bool canVote() const;
[[nodiscard]] bool canSendVotes() const;
[[nodiscard]] int countAnswerTop( [[nodiscard]] int countAnswerTop(
const Answer &answer, const Answer &answer,
@ -61,11 +66,14 @@ private:
const Answer &answer, const Answer &answer,
int innerWidth) const; int innerWidth) const;
[[nodiscard]] ClickHandlerPtr createAnswerClickHandler( [[nodiscard]] ClickHandlerPtr createAnswerClickHandler(
const Answer &answer) const; const Answer &answer);
void updateTexts(); void updateTexts();
void updateRecentVoters();
void updateAnswers(); void updateAnswers();
void updateVotes(); void updateVotes();
void updateTotalVotes(); void updateTotalVotes();
bool showVotersCount() const;
bool inlineFooter() const;
void updateAnswerVotes(); void updateAnswerVotes();
void updateAnswerVotesFromOriginal( void updateAnswerVotesFromOriginal(
Answer &answer, Answer &answer,
@ -74,6 +82,11 @@ private:
int maxVotes); int maxVotes);
void checkSendingAnimation() const; void checkSendingAnimation() const;
void paintRecentVoters(
Painter &p,
int left,
int top,
TextSelection selection) const;
int paintAnswer( int paintAnswer(
Painter &p, Painter &p,
const Answer &answer, const Answer &answer,
@ -99,12 +112,26 @@ private:
TextSelection selection) const; TextSelection selection) const;
void paintFilling( void paintFilling(
Painter &p, Painter &p,
bool chosen,
bool correct,
float64 filling, float64 filling,
int left, int left,
int top, int top,
int width, int width,
int height, int height,
TextSelection selection) const; 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 checkAnimationStart() const;
bool answerVotesChanged() const; bool answerVotesChanged() const;
@ -114,17 +141,30 @@ private:
void radialAnimationCallback() const; void radialAnimationCallback() const;
void toggleRipple(Answer &answer, bool pressed); 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 _pollVersion = 0;
int _totalVotes = 0; int _totalVotes = 0;
bool _voted = false; bool _voted = false;
bool _closed = false; PollData::Flags _flags = PollData::Flags();
Ui::Text::String _question; Ui::Text::String _question;
Ui::Text::String _subtitle; Ui::Text::String _subtitle;
std::vector<not_null<UserData*>> _recentVoters;
QImage _recentVotersImage;
std::vector<Answer> _answers; std::vector<Answer> _answers;
Ui::Text::String _totalVotesLabel; 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<AnswersAnimation> _answersAnimation;
mutable std::unique_ptr<SendingAnimation> _sendingAnimation; mutable std::unique_ptr<SendingAnimation> _sendingAnimation;

View file

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

View file

@ -123,6 +123,10 @@ public:
//explicit ContentMemento(not_null<Data::Feed*> feed) : _feed(feed) { // #feed //explicit ContentMemento(not_null<Data::Feed*> feed) : _feed(feed) { // #feed
//} //}
explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Settings::Tag settings);
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
: _poll(poll)
, _pollContextId(contextId) {
}
virtual object_ptr<ContentWidget> createWidget( virtual object_ptr<ContentWidget> createWidget(
QWidget *parent, QWidget *parent,
@ -141,6 +145,12 @@ public:
UserData *settingsSelf() const { UserData *settingsSelf() const {
return _settingsSelf; return _settingsSelf;
} }
PollData *poll() const {
return _poll;
}
FullMsgId pollContextId() const {
return _pollContextId;
}
Key key() const; Key key() const;
virtual Section section() const = 0; virtual Section section() const = 0;
@ -177,6 +187,9 @@ private:
const PeerId _migratedPeerId = 0; const PeerId _migratedPeerId = 0;
//Data::Feed * const _feed = nullptr; // #feed //Data::Feed * const _feed = nullptr; // #feed
UserData * const _settingsSelf = nullptr; UserData * const _settingsSelf = nullptr;
PollData * const _poll = nullptr;
const FullMsgId _pollContextId;
int _scrollTop = 0; int _scrollTop = 0;
QString _searchFieldQuery; QString _searchFieldQuery;
bool _searchEnabledByContent = false; 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_channel.h"
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_media_types.h"
#include "history/history_item.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "window/window_session_controller.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(Settings::Tag settings) : _value(settings) {
} }
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
: _value(PollKey{ poll, contextId }) {
}
PeerData *Key::peer() const { PeerData *Key::peer() const {
if (const auto peer = base::get_if<not_null<PeerData*>>(&_value)) { if (const auto peer = base::get_if<not_null<PeerData*>>(&_value)) {
return *peer; return *peer;
@ -67,6 +73,20 @@ UserData *Key::settingsSelf() const {
return nullptr; 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( rpl::producer<SparseIdsMergedSlice> AbstractController::mediaSource(
SparseIdsMergedSlice::UniversalMsgId aroundId, SparseIdsMergedSlice::UniversalMsgId aroundId,
int limitBefore, int limitBefore,
@ -106,6 +126,15 @@ PeerId AbstractController::migratedPeerId() const {
return PeerId(0); 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( void AbstractController::showSection(
Window::SectionMemento &&memento, Window::SectionMemento &&memento,
const Window::SectionShow &params) { const Window::SectionShow &params) {

View file

@ -33,16 +33,24 @@ public:
Key(not_null<PeerData*> peer); Key(not_null<PeerData*> peer);
//Key(not_null<Data::Feed*> feed); // #feed //Key(not_null<Data::Feed*> feed); // #feed
Key(Settings::Tag settings); Key(Settings::Tag settings);
Key(not_null<PollData*> poll, FullMsgId contextId);
PeerData *peer() const; PeerData *peer() const;
//Data::Feed *feed() const; // #feed //Data::Feed *feed() const; // #feed
UserData *settingsSelf() const; UserData *settingsSelf() const;
PollData *poll() const;
FullMsgId pollContextId() const;
private: private:
struct PollKey {
not_null<PollData*> poll;
FullMsgId contextId;
};
base::variant< base::variant<
not_null<PeerData*>, not_null<PeerData*>,
//not_null<Data::Feed*>, // #feed //not_null<Data::Feed*>, // #feed
Settings::Tag> _value; Settings::Tag,
PollKey> _value;
}; };
@ -60,6 +68,7 @@ public:
Members, Members,
//Channels, // #feed //Channels, // #feed
Settings, Settings,
PollResults,
}; };
using SettingsType = ::Settings::Type; using SettingsType = ::Settings::Type;
using MediaType = Storage::SharedMediaType; using MediaType = Storage::SharedMediaType;
@ -113,6 +122,10 @@ public:
UserData *settingsSelf() const { UserData *settingsSelf() const {
return key().settingsSelf(); return key().settingsSelf();
} }
PollData *poll() const;
FullMsgId pollContextId() const {
return key().pollContextId();
}
virtual void setSearchEnabledByContent(bool enabled) { 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/common_groups/info_common_groups_widget.h"
//#include "info/feed/info_feed_profile_widget.h" // #feed //#include "info/feed/info_feed_profile_widget.h" // #feed
#include "info/settings/info_settings_widget.h" #include "info/settings/info_settings_widget.h"
#include "info/polls/info_polls_results_widget.h"
#include "info/info_section_widget.h" #include "info/info_section_widget.h"
#include "info/info_layer_widget.h" #include "info/info_layer_widget.h"
#include "info/info_controller.h" #include "info/info_controller.h"
@ -42,6 +43,10 @@ Memento::Memento(Settings::Tag settings, Section section)
: Memento(DefaultStack(settings, 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) Memento::Memento(std::vector<std::unique_ptr<ContentMemento>> stack)
: _stack(std::move(stack)) { : _stack(std::move(stack)) {
} }
@ -72,6 +77,14 @@ std::vector<std::unique_ptr<ContentMemento>> Memento::DefaultStack(
return result; 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) { Section Memento::DefaultSection(not_null<PeerData*> peer) {
if (peer->isSelf()) { if (peer->isSelf()) {
return Section(Section::MediaType::Photo); return Section(Section::MediaType::Photo);

View file

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

View file

@ -635,6 +635,11 @@ rpl::producer<QString> TitleValue(
return tr::ktg_settings_kotato(); return tr::ktg_settings_kotato();
} }
Unexpected("Bad settings type in Info::TitleValue()"); 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()"); 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()); return Dialogs::RowDescriptor(peer->owner().history(peer), FullMsgId());
//} else if (const auto feed = key().feed()) { // #feed //} else if (const auto feed = key().feed()) { // #feed
// return Dialogs::RowDescriptor(feed, FullMsgId()); // return Dialogs::RowDescriptor(feed, FullMsgId());
} else if (key().settingsSelf()) { } else if (key().settingsSelf() || key().poll()) {
return Dialogs::RowDescriptor(); return Dialogs::RowDescriptor();
} }
Unexpected("Owner in WrapWidget::activeChat()."); 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; using Type = Section::SettingsType;
struct Tag; struct Tag;
class InnerWidget;
class Memento final : public ContentMemento { class Memento final : public ContentMemento {
public: public:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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