Merge remote-tracking branch 'tdesktop/dev' into dev
|
|
@ -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
|
||||
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
|
|
@ -8,7 +8,7 @@ about: Report errors or unexpected behavior.
|
|||
Thanks for reporting issues of Telegram Desktop!
|
||||
|
||||
To make it easier for us to help you please enter detailed information below.
|
||||
-->
|
||||
-->
|
||||
### Steps to reproduce
|
||||
1.
|
||||
2.
|
||||
|
|
|
|||
103
.github/workflows/issue_closer.yml
vendored
Normal file
|
|
@ -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'
|
||||
});
|
||||
|
||||
4
.github/workflows/mac.yml
vendored
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
|
||||
git clone $GIT/desktop-app/zlib.git
|
||||
cd zlib
|
||||
CFLAGS="$MIN_MAC $UNGUARDED" LDFLAGS="$MIN_MAC" ./configure
|
||||
CFLAGS="$MIN_MAC $UNGUARDED" LDFLAGS="$MIN_MAC" ./configure --prefix=$PREFIX
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
|
||||
|
|
@ -429,4 +429,4 @@ jobs:
|
|||
name: Upload artifact.
|
||||
with:
|
||||
name: Telegram
|
||||
path: $REPO_NAME\out\Debug\artifact\
|
||||
path: ${{ env.REPO_NAME }}/out/Debug/artifact/
|
||||
|
|
|
|||
11
.github/workflows/win.yml
vendored
|
|
@ -23,7 +23,6 @@ jobs:
|
|||
env:
|
||||
SDK: "10.0.18362.0"
|
||||
VC: "call vcvars32.bat && cd Libraries"
|
||||
PY2: 'C:\hostedtoolcache\windows\Python\2.7.16\x64'
|
||||
GIT: "https://github.com"
|
||||
QT: "5_12_5"
|
||||
OPENSSL_VER: "1_1_1"
|
||||
|
|
@ -186,6 +185,16 @@ jobs:
|
|||
run: |
|
||||
cd %LibrariesPath%
|
||||
|
||||
echo Find any version of Python 2.
|
||||
for /D %%a in (C:\hostedtoolcache\windows\Python\2.*) do (
|
||||
SET PY2=%%a\x64
|
||||
)
|
||||
IF [%PY2%] == [] (
|
||||
echo Python 2 is not found.
|
||||
exit 1
|
||||
)
|
||||
echo Found %PY2%.
|
||||
|
||||
git clone %GIT%/telegramdesktop/gyp.git
|
||||
cd gyp
|
||||
SET PATH=%PY2%;%cd%;%PATH%
|
||||
|
|
|
|||
3
.gitmodules
vendored
|
|
@ -28,9 +28,6 @@
|
|||
[submodule "Telegram/lib_base"]
|
||||
path = Telegram/lib_base
|
||||
url = https://github.com/desktop-app/lib_base.git
|
||||
[submodule "Telegram/gyp/helpers"]
|
||||
path = Telegram/gyp/helpers
|
||||
url = https://github.com/desktop-app/gyp_helpers.git
|
||||
[submodule "Telegram/codegen"]
|
||||
path = Telegram/codegen
|
||||
url = https://github.com/desktop-app/codegen.git
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ buildRange() {
|
|||
rm -rf *
|
||||
|
||||
cd "$EXTERNAL"
|
||||
git clone --depth 1 --branch 0.9.1 https://github.com/ericniebler/range-v3
|
||||
git clone --depth 1 --branch 0.10.0 https://github.com/ericniebler/range-v3
|
||||
|
||||
cd "$EXTERNAL/range-v3"
|
||||
cp -r * "$RANGE_PATH/"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ travisStartFold() {
|
|||
fi
|
||||
|
||||
echo "travis_fold:start:$NAME"
|
||||
sameLineInfoMessage "$TITLE"
|
||||
sameLineInfoMessage "$TITLE"
|
||||
|
||||
TRAVIS_LAST_FOLD="$NAME"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -524,6 +524,10 @@ PRIVATE
|
|||
info/media/info_media_widget.h
|
||||
info/members/info_members_widget.cpp
|
||||
info/members/info_members_widget.h
|
||||
info/polls/info_polls_results_inner_widget.cpp
|
||||
info/polls/info_polls_results_inner_widget.h
|
||||
info/polls/info_polls_results_widget.cpp
|
||||
info/polls/info_polls_results_widget.h
|
||||
info/profile/info_profile_actions.cpp
|
||||
info/profile/info_profile_actions.h
|
||||
info/profile/info_profile_cover.cpp
|
||||
|
|
@ -1096,7 +1100,11 @@ elseif (build_osx)
|
|||
else()
|
||||
set(bundle_identifier "com.tdesktop.Telegram$<$<CONFIG:Debug>:Debug>")
|
||||
set(bundle_entitlements "Telegram.entitlements")
|
||||
set(output_name "Telegram")
|
||||
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
|
||||
set(output_name "telegram-desktop")
|
||||
else()
|
||||
set(output_name "Telegram")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set_target_properties(Telegram PROPERTIES
|
||||
|
|
@ -1193,3 +1201,17 @@ if ((NOT disable_autoupdate OR NOT LINUX) AND NOT build_macstore AND NOT build_w
|
|||
set_target_properties(Packer PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "telegram.png")
|
||||
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "telegram.png")
|
||||
install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "telegram.png")
|
||||
install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "telegram.png")
|
||||
install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "telegram.png")
|
||||
install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "telegram.png")
|
||||
install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "telegram.png")
|
||||
install(FILES "../lib/xdg/telegramdesktop.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications" RENAME "${TDESKTOP_LAUNCHER_BASENAME}.desktop")
|
||||
install(FILES "../lib/xdg/telegramdesktop.appdata.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo" RENAME "${TDESKTOP_LAUNCHER_BASENAME}.appdata.xml")
|
||||
endif()
|
||||
|
|
|
|||
BIN
Telegram/Resources/icons/poll_choice_right.png
Normal file
|
After Width: | Height: | Size: 166 B |
BIN
Telegram/Resources/icons/poll_choice_right@2x.png
Normal file
|
After Width: | Height: | Size: 219 B |
BIN
Telegram/Resources/icons/poll_choice_right@3x.png
Normal file
|
After Width: | Height: | Size: 294 B |
BIN
Telegram/Resources/icons/poll_choice_wrong.png
Normal file
|
After Width: | Height: | Size: 157 B |
BIN
Telegram/Resources/icons/poll_choice_wrong@2x.png
Normal file
|
After Width: | Height: | Size: 213 B |
BIN
Telegram/Resources/icons/poll_choice_wrong@3x.png
Normal file
|
After Width: | Height: | Size: 260 B |
BIN
Telegram/Resources/icons/poll_select_check.png
Normal file
|
After Width: | Height: | Size: 249 B |
BIN
Telegram/Resources/icons/poll_select_check@2x.png
Normal file
|
After Width: | Height: | Size: 364 B |
BIN
Telegram/Resources/icons/poll_select_check@3x.png
Normal file
|
After Width: | Height: | Size: 556 B |
|
|
@ -158,6 +158,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_deleted_message" = "Deleted message";
|
||||
"lng_pinned_message" = "Pinned message";
|
||||
"lng_pinned_poll" = "Pinned poll";
|
||||
"lng_pinned_quiz" = "Pinned quiz";
|
||||
"lng_pinned_unpin_sure" = "Would you like to unpin this message?";
|
||||
"lng_pinned_pin_sure" = "Would you like to pin this message?";
|
||||
"lng_pinned_pin" = "Pin";
|
||||
|
|
@ -1294,6 +1295,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
"lng_scheduled_messages" = "Scheduled Messages";
|
||||
"lng_reminder_messages" = "Reminders";
|
||||
"lng_scheduled_date" = "Scheduled for {date}";
|
||||
"lng_scheduled_date_until_online" = "Scheduled until online";
|
||||
"lng_scheduled_send_until_online" = "Send when online";
|
||||
"lng_scheduled_send_now" = "Send message now?";
|
||||
"lng_scheduled_send_now_many#one" = "Send {count} message now?";
|
||||
"lng_scheduled_send_now_many#other" = "Send {count} messages now?";
|
||||
|
|
@ -1453,6 +1457,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_send_album" = "Send as an album";
|
||||
"lng_send_photo" = "Send as a photo";
|
||||
"lng_send_file" = "Send as a file";
|
||||
"lng_send_media_invalid_files" = "Sorry, no valid files found.";
|
||||
|
||||
"lng_forward_choose" = "Choose recipient...";
|
||||
"lng_forward_cant" = "Sorry, no way to forward here :(";
|
||||
|
|
@ -1797,6 +1802,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_restricted_send_inline_all" = "Posting inline content isn't allowed in this group.";
|
||||
"lng_restricted_send_polls_all" = "Posting polls isn't allowed in this group.";
|
||||
|
||||
"lng_restricted_send_public_polls" = "Sorry, public polls can't be forwarded to channels.";
|
||||
|
||||
"lng_exceptions_list_title" = "Exceptions";
|
||||
"lng_removed_list_title" = "Removed users";
|
||||
|
||||
|
|
@ -2166,10 +2173,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_launch_exe_dont_ask" = "Don't ask me again";
|
||||
|
||||
"lng_polls_anonymous" = "Anonymous Poll";
|
||||
"lng_polls_public" = "Poll";
|
||||
"lng_polls_anonymous_quiz" = "Anonymous Quiz";
|
||||
"lng_polls_public_quiz" = "Quiz";
|
||||
"lng_polls_closed" = "Final results";
|
||||
"lng_polls_votes_count#one" = "{count} vote";
|
||||
"lng_polls_votes_count#other" = "{count} votes";
|
||||
"lng_polls_votes_none" = "No votes";
|
||||
"lng_polls_answers_count#one" = "{count} answer";
|
||||
"lng_polls_answers_count#other" = "{count} answers";
|
||||
"lng_polls_answers_none" = "No answers";
|
||||
"lng_polls_submit_votes" = "Vote";
|
||||
"lng_polls_view_results" = "View results";
|
||||
"lng_polls_retract" = "Retract vote";
|
||||
"lng_polls_stop" = "Stop poll";
|
||||
"lng_polls_stop_warning" = "If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone.";
|
||||
|
|
@ -2183,8 +2198,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_polls_create_limit#one" = "You can add {count} more option.";
|
||||
"lng_polls_create_limit#other" = "You can add {count} more options.";
|
||||
"lng_polls_create_maximum" = "You have added the maximum number of options.";
|
||||
"lng_polls_create_settings" = "Settings";
|
||||
"lng_polls_create_hint" = "Tap to select the right option";
|
||||
"lng_polls_create_anonymous" = "Anonymous Votes";
|
||||
"lng_polls_create_multiple_choice" = "Multiple Answers";
|
||||
"lng_polls_create_quiz_mode" = "Quiz Mode";
|
||||
"lng_polls_create_button" = "Create";
|
||||
|
||||
"lng_polls_poll_results_title" = "Poll results";
|
||||
"lng_polls_quiz_results_title" = "Quiz results";
|
||||
"lng_polls_show_more#one" = "Show more ({count})";
|
||||
"lng_polls_show_more#other" = "Show more ({count})";
|
||||
"lng_polls_votes_collapse" = "Collapse";
|
||||
|
||||
"lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM.";
|
||||
"lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}.";
|
||||
"lng_outdated_now" = "So that Telegram Desktop can update to newer versions.";
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int =
|
|||
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
||||
inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
|
||||
inputMediaGeoLive#ce4e82fd flags:# stopped:flags.0?true geo_point:InputGeoPoint period:flags.1?int = InputMedia;
|
||||
inputMediaPoll#6b3765b poll:Poll = InputMedia;
|
||||
inputMediaPoll#abe9ca25 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> = InputMedia;
|
||||
|
||||
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
|
||||
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
|
||||
|
|
@ -351,6 +351,7 @@ updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector<int> = Update;
|
|||
updateTheme#8216fba3 theme:Theme = Update;
|
||||
updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update;
|
||||
updateLoginToken#564fe691 = Update;
|
||||
updateMessagePollVote#42f88f2c poll_id:long user_id:int options:Vector<bytes> = Update;
|
||||
|
||||
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
|
||||
|
||||
|
|
@ -546,6 +547,7 @@ keyboardButtonGame#50f41ccf text:string = KeyboardButton;
|
|||
keyboardButtonBuy#afd93fbb text:string = KeyboardButton;
|
||||
keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton;
|
||||
inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton;
|
||||
keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = KeyboardButton;
|
||||
|
||||
keyboardButtonRow#77608b83 buttons:Vector<KeyboardButton> = KeyboardButtonRow;
|
||||
|
||||
|
|
@ -1015,11 +1017,11 @@ help.userInfo#1eb3758 message:string entities:Vector<MessageEntity> author:strin
|
|||
|
||||
pollAnswer#6ca9c2e9 text:string option:bytes = PollAnswer;
|
||||
|
||||
poll#d5529d06 id:long flags:# closed:flags.0?true question:string answers:Vector<PollAnswer> = Poll;
|
||||
poll#d5529d06 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector<PollAnswer> = Poll;
|
||||
|
||||
pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true option:bytes voters:int = PollAnswerVoters;
|
||||
pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters;
|
||||
|
||||
pollResults#5755785a flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int = PollResults;
|
||||
pollResults#c87024a2 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<int> = PollResults;
|
||||
|
||||
chatOnlines#f041e250 onlines:int = ChatOnlines;
|
||||
|
||||
|
|
@ -1077,16 +1079,11 @@ restrictionReason#d072acb4 platform:string reason:string text:string = Restricti
|
|||
inputTheme#3c5693e9 id:long access_hash:long = InputTheme;
|
||||
inputThemeSlug#f5890df1 slug:string = InputTheme;
|
||||
|
||||
themeDocumentNotModified#483d270c = Theme;
|
||||
theme#28f1114 flags:# creator:flags.0?true default:flags.1?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?ThemeSettings installs_count:int = Theme;
|
||||
|
||||
account.themesNotModified#f41eb622 = account.Themes;
|
||||
account.themes#7f676421 hash:int themes:Vector<Theme> = account.Themes;
|
||||
|
||||
wallet.liteResponse#764386d7 response:bytes = wallet.LiteResponse;
|
||||
|
||||
wallet.secretSalt#dd484d64 salt:bytes = wallet.KeySecretSalt;
|
||||
|
||||
auth.loginToken#629f1980 expires:int token:bytes = auth.LoginToken;
|
||||
auth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken;
|
||||
auth.loginTokenSuccess#390d5c5e authorization:auth.Authorization = auth.LoginToken;
|
||||
|
|
@ -1107,6 +1104,12 @@ themeSettings#9c14984a flags:# base_theme:BaseTheme accent_color:int message_top
|
|||
|
||||
webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector<Document> settings:flags.1?ThemeSettings = WebPageAttribute;
|
||||
|
||||
messageUserVote#a28e5559 user_id:int option:bytes date:int = MessageUserVote;
|
||||
messageUserVoteInputOption#36377430 user_id:int date:int = MessageUserVote;
|
||||
messageUserVoteMultiple#e8fe0de user_id:int options:Vector<bytes> date:int = MessageUserVote;
|
||||
|
||||
messages.votesList#823f649 flags:# count:int votes:Vector<MessageUserVote> users:Vector<User> next_offset:flags.0?string = messages.VotesList;
|
||||
|
||||
---functions---
|
||||
|
||||
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||
|
|
@ -1343,6 +1346,7 @@ messages.getScheduledHistory#e2c2685b peer:InputPeer hash:int = messages.Message
|
|||
messages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector<int> = messages.Messages;
|
||||
messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates;
|
||||
messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector<int> = Updates;
|
||||
messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList;
|
||||
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
|
|
@ -1354,7 +1358,7 @@ photos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
|
|||
photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;
|
||||
|
||||
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
|
||||
upload.getFile#b15a9afc flags:# precise:flags.0?true location:InputFileLocation offset:int limit:int = upload.File;
|
||||
upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File;
|
||||
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
||||
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
|
||||
upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile;
|
||||
|
|
@ -1451,7 +1455,4 @@ langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLangua
|
|||
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
|
||||
folders.deleteFolder#1c295881 folder_id:int = Updates;
|
||||
|
||||
wallet.sendLiteRequest#e2c9d33e body:bytes = wallet.LiteResponse;
|
||||
wallet.getKeySecretSalt#b57f346 revoke:Bool = wallet.KeySecretSalt;
|
||||
|
||||
// LAYER 108
|
||||
// LAYER 109
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="1.9.4.0" />
|
||||
Version="1.9.7.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
</application>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
|
|
@ -21,6 +21,7 @@ struct SendOptions {
|
|||
enum class SendType {
|
||||
Normal,
|
||||
Scheduled,
|
||||
ScheduledToUser, // For "Send when online".
|
||||
};
|
||||
|
||||
struct SendAction {
|
||||
|
|
|
|||
|
|
@ -5842,12 +5842,24 @@ void ApiWrap::createPoll(
|
|||
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
|
||||
}
|
||||
|
||||
const auto inputFlags = data.quiz()
|
||||
? MTPDinputMediaPoll::Flag::f_correct_answers
|
||||
: MTPDinputMediaPoll::Flag(0);
|
||||
auto correct = QVector<MTPbytes>();
|
||||
for (const auto &answer : data.answers) {
|
||||
if (answer.correct) {
|
||||
correct.push_back(MTP_bytes(answer.option));
|
||||
}
|
||||
}
|
||||
const auto replyTo = action.replyTo;
|
||||
history->sendRequestId = request(MTPmessages_SendMedia(
|
||||
MTP_flags(sendFlags),
|
||||
peer->input,
|
||||
MTP_int(replyTo),
|
||||
MTP_inputMediaPoll(PollDataToMTP(&data)),
|
||||
MTP_inputMediaPoll(
|
||||
MTP_flags(inputFlags),
|
||||
PollDataToMTP(&data),
|
||||
MTP_vector<MTPbytes>(correct)),
|
||||
MTP_string(),
|
||||
MTP_long(rand_value<uint64>()),
|
||||
MTPReplyMarkup(),
|
||||
|
|
@ -5879,13 +5891,13 @@ void ApiWrap::sendPollVotes(
|
|||
const auto hideSending = [=] {
|
||||
if (showSending) {
|
||||
if (const auto item = _session->data().message(itemId)) {
|
||||
poll->sendingVote = QByteArray();
|
||||
poll->sendingVotes.clear();
|
||||
_session->data().requestItemRepaint(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (showSending) {
|
||||
poll->sendingVote = options.front();
|
||||
poll->sendingVotes = options;
|
||||
_session->data().requestItemRepaint(item);
|
||||
}
|
||||
|
||||
|
|
@ -5921,12 +5933,24 @@ void ApiWrap::closePoll(not_null<HistoryItem*> item) {
|
|||
return;
|
||||
}
|
||||
|
||||
const auto inputFlags = poll->quiz()
|
||||
? MTPDinputMediaPoll::Flag::f_correct_answers
|
||||
: MTPDinputMediaPoll::Flag(0);
|
||||
auto correct = QVector<MTPbytes>();
|
||||
for (const auto &answer : poll->answers) {
|
||||
if (answer.correct) {
|
||||
correct.push_back(MTP_bytes(answer.option));
|
||||
}
|
||||
}
|
||||
const auto requestId = request(MTPmessages_EditMessage(
|
||||
MTP_flags(MTPmessages_EditMessage::Flag::f_media),
|
||||
item->history()->peer->input,
|
||||
MTP_int(item->id),
|
||||
MTPstring(),
|
||||
MTP_inputMediaPoll(PollDataToMTP(poll)),
|
||||
MTP_inputMediaPoll(
|
||||
MTP_flags(inputFlags),
|
||||
PollDataToMTP(poll, true),
|
||||
MTP_vector<MTPbytes>(correct)),
|
||||
MTPReplyMarkup(),
|
||||
MTPVector<MTPMessageEntity>(),
|
||||
MTP_int(0) // schedule_date
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "numbers.h"
|
||||
#include "observer_peer.h"
|
||||
#include "main/main_session.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_overview.h"
|
||||
#include "styles/style_mediaview.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
|
@ -180,6 +181,8 @@ namespace App {
|
|||
prepareCorners(MessageInSelectedCorners, st::historyMessageRadius, st::msgInBgSelected, &st::msgInShadowSelected);
|
||||
prepareCorners(MessageOutCorners, st::historyMessageRadius, st::msgOutBg, &st::msgOutShadow);
|
||||
prepareCorners(MessageOutSelectedCorners, st::historyMessageRadius, st::msgOutBgSelected, &st::msgOutShadowSelected);
|
||||
|
||||
prepareCorners(SendFilesBoxAlbumGroupCorners, st::sendBoxAlbumGroupRadius, st::callFingerprintBg);
|
||||
}
|
||||
|
||||
void createCorners() {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ enum RoundCorners : int {
|
|||
MessageOutCorners,
|
||||
MessageOutSelectedCorners,
|
||||
|
||||
SendFilesBoxAlbumGroupCorners,
|
||||
|
||||
RoundCornersCount
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -514,6 +514,29 @@ editMediaButton: IconButton {
|
|||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
|
||||
// SendFilesBox
|
||||
|
||||
sendBoxAlbumGroupEditInternalSkip: 9px;
|
||||
sendBoxAlbumGroupSkipRight: 6px;
|
||||
sendBoxAlbumGroupSkipTop: 6px;
|
||||
sendBoxAlbumGroupRadius: 12px;
|
||||
sendBoxAlbumGroupHeight: 25px;
|
||||
|
||||
sendBoxAlbumGroupEditButtonIcon: editMediaButtonIconPhoto;
|
||||
sendBoxAlbumGroupEditButtonIconPosition: point(4px, -1px);
|
||||
|
||||
sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) {
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgRipple;
|
||||
}
|
||||
}
|
||||
|
||||
sendBoxAlbumGroupDeleteButtonIconPosition: point(-3px, 0px);
|
||||
sendBoxAlbumGroupDeleteButtonIcon: icon {{ "history_file_cancel", msgServiceFg}};
|
||||
sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "history_file_cancel", menuIconFg, point(6px, 6px) }};
|
||||
|
||||
// End of SendFilesBox
|
||||
|
||||
calendarTitleHeight: boxTitleHeight;
|
||||
calendarPrevious: IconButton {
|
||||
width: calendarTitleHeight;
|
||||
|
|
@ -812,15 +835,15 @@ createPollField: InputField(defaultInputField) {
|
|||
}
|
||||
createPollFieldPadding: margins(22px, 5px, 22px, 5px);
|
||||
createPollOptionField: InputField(createPollField) {
|
||||
textMargins: margins(22px, 8px, 40px, 8px);
|
||||
textMargins: margins(22px, 11px, 40px, 11px);
|
||||
placeholderMargins: margins(2px, 0px, 2px, 0px);
|
||||
heightMax: 64px;
|
||||
heightMax: 68px;
|
||||
}
|
||||
createPollLimitLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 274px;
|
||||
align: align(topleft);
|
||||
}
|
||||
createPollLimitPadding: margins(22px, 10px, 22px, 5px);
|
||||
createPollLimitPadding: margins(22px, 10px, 22px, 16px);
|
||||
createPollOptionRemove: CrossButton {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
|
@ -841,7 +864,7 @@ createPollOptionRemove: CrossButton {
|
|||
color: windowBgOver;
|
||||
}
|
||||
}
|
||||
createPollOptionRemovePosition: point(10px, 7px);
|
||||
createPollOptionRemovePosition: point(11px, 9px);
|
||||
createPollWarning: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
palette: TextPalette(defaultTextPalette) {
|
||||
|
|
@ -849,6 +872,8 @@ createPollWarning: FlatLabel(defaultFlatLabel) {
|
|||
}
|
||||
}
|
||||
createPollWarningPosition: point(16px, 6px);
|
||||
createPollCheckboxMargin: margins(23px, 10px, 23px, 10px);
|
||||
createPollFieldTitlePadding: margins(22px, 7px, 10px, 6px);
|
||||
|
||||
callSettingsButton: IconButton {
|
||||
width: 50px;
|
||||
|
|
@ -922,6 +947,33 @@ customBadgeField: InputField(defaultInputField) {
|
|||
heightMin: 32px;
|
||||
}
|
||||
|
||||
pollResultsQuestion: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 320px;
|
||||
textFg: windowBoldFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(16px semibold);
|
||||
linkFont: font(16px semibold);
|
||||
linkFontOver: font(16px semibold underline);
|
||||
}
|
||||
}
|
||||
pollResultsVotesCount: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
pollResultsHeaderPadding: margins(22px, 22px, 22px, 8px);
|
||||
pollResultsShowMore: SettingsButton {
|
||||
textFg: lightButtonFg;
|
||||
textFgOver: lightButtonFgOver;
|
||||
textBg: windowBg;
|
||||
textBgOver: windowBgOver;
|
||||
|
||||
font: semiboldFont;
|
||||
|
||||
height: 20px;
|
||||
padding: margins(71px, 10px, 8px, 8px);
|
||||
|
||||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
|
||||
fontsBoxTextStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(13px);
|
||||
linkFont: font(13px);
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/toast/toast.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "main/main_session.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
|
|
@ -42,13 +45,16 @@ public:
|
|||
Options(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session);
|
||||
not_null<Main::Session*> session,
|
||||
bool chooseCorrectEnabled);
|
||||
|
||||
[[nodiscard]] bool isValid() const;
|
||||
[[nodiscard]] rpl::producer<bool> isValidChanged() const;
|
||||
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
|
||||
void focusFirst();
|
||||
|
||||
void enableChooseCorrect(bool enabled);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> usedCount() const;
|
||||
[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
|
||||
[[nodiscard]] rpl::producer<> backspaceInFront() const;
|
||||
|
|
@ -56,23 +62,31 @@ public:
|
|||
private:
|
||||
class Option {
|
||||
public:
|
||||
static Option Create(
|
||||
Option(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
int position);
|
||||
int position,
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group);
|
||||
|
||||
Option(const Option &other) = delete;
|
||||
Option &operator=(const Option &other) = delete;
|
||||
|
||||
void toggleRemoveAlways(bool toggled);
|
||||
void enableChooseCorrect(
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group);
|
||||
|
||||
void show(anim::type animated);
|
||||
void destroy(FnMut<void()> done);
|
||||
|
||||
//[[nodisacrd]] bool hasShadow() const;
|
||||
//void destroyShadow();
|
||||
[[nodisacrd]] bool hasShadow() const;
|
||||
void createShadow();
|
||||
void destroyShadow();
|
||||
|
||||
[[nodiscard]] bool isEmpty() const;
|
||||
[[nodiscard]] bool isGood() const;
|
||||
[[nodiscard]] bool isTooLong() const;
|
||||
[[nodiscard]] bool isCorrect() const;
|
||||
[[nodiscard]] bool hasFocus() const;
|
||||
void setFocus() const;
|
||||
void clearValue();
|
||||
|
|
@ -86,29 +100,18 @@ private:
|
|||
|
||||
[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;
|
||||
|
||||
inline bool operator<(const Option &other) const {
|
||||
return field() < other.field();
|
||||
}
|
||||
|
||||
friend inline bool operator<(
|
||||
const Option &option,
|
||||
Ui::InputField *field) {
|
||||
return option.field() < field;
|
||||
}
|
||||
friend inline bool operator<(
|
||||
Ui::InputField *field,
|
||||
const Option &option) {
|
||||
return field < option.field();
|
||||
}
|
||||
|
||||
private:
|
||||
Option() = default;
|
||||
|
||||
void createShadow();
|
||||
void createRemove();
|
||||
void createWarning();
|
||||
void toggleCorrectSpace(bool visible);
|
||||
void updateFieldGeometry();
|
||||
|
||||
base::unique_qptr<Ui::SlideWrap<Ui::InputField>> _field;
|
||||
base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;
|
||||
not_null<Ui::RpWidget*> _content;
|
||||
base::unique_qptr<Ui::FadeWrapScaled<Ui::Radiobutton>> _correct;
|
||||
Ui::Animations::Simple _correctShown;
|
||||
bool _hasCorrect = false;
|
||||
Ui::InputField *_field = nullptr;
|
||||
base::unique_qptr<Ui::PlainShadow> _shadow;
|
||||
base::unique_qptr<Ui::CrossButton> _remove;
|
||||
rpl::variable<bool> *_removeAlways = nullptr;
|
||||
|
|
@ -116,23 +119,26 @@ private:
|
|||
};
|
||||
|
||||
[[nodiscard]] bool full() const;
|
||||
//[[nodiscard]] bool correctShadows() const;
|
||||
//void fixShadows();
|
||||
[[nodiscard]] bool correctShadows() const;
|
||||
void fixShadows();
|
||||
void removeEmptyTail();
|
||||
void addEmptyOption();
|
||||
void checkLastOption();
|
||||
void validateState();
|
||||
void fixAfterErase();
|
||||
void destroy(Option &&option);
|
||||
void removeDestroyed(not_null<Ui::InputField*> field);
|
||||
void destroy(std::unique_ptr<Option> option);
|
||||
void removeDestroyed(not_null<Option*> field);
|
||||
int findField(not_null<Ui::InputField*> field) const;
|
||||
[[nodiscard]] auto createChooseCorrectGroup()
|
||||
-> std::shared_ptr<Ui::RadiobuttonGroup>;
|
||||
|
||||
not_null<QWidget*> _outer;
|
||||
not_null<Ui::VerticalLayout*> _container;
|
||||
const not_null<Main::Session*> _session;
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
|
||||
int _position = 0;
|
||||
std::vector<Option> _list;
|
||||
std::set<Option, std::less<>> _destroyed;
|
||||
std::vector<std::unique_ptr<Option>> _list;
|
||||
std::vector<std::unique_ptr<Option>> _destroyed;
|
||||
rpl::variable<bool> _valid = false;
|
||||
rpl::variable<int> _usedCount = 0;
|
||||
rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
|
||||
|
|
@ -187,58 +193,85 @@ void FocusAtEnd(not_null<Ui::InputField*> field) {
|
|||
field->ensureCursorVisible();
|
||||
}
|
||||
|
||||
Options::Option Options::Option::Create(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
int position) {
|
||||
auto result = Option();
|
||||
const auto field = container->insert(
|
||||
position,
|
||||
object_ptr<Ui::SlideWrap<Ui::InputField>>(
|
||||
container,
|
||||
object_ptr<Ui::InputField>(
|
||||
container,
|
||||
st::createPollOptionField,
|
||||
Ui::InputField::Mode::NoNewlines,
|
||||
tr::lng_polls_create_option_add())));
|
||||
InitField(outer, field->entity(), session);
|
||||
field->entity()->setMaxLength(kOptionLimit + kErrorLimit);
|
||||
result._field.reset(field);
|
||||
Options::Option::Option(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
int position,
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group)
|
||||
: _wrap(container->insert(
|
||||
position,
|
||||
object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
|
||||
container,
|
||||
object_ptr<Ui::RpWidget>(container))))
|
||||
, _content(_wrap->entity())
|
||||
, _field(
|
||||
Ui::CreateChild<Ui::InputField>(
|
||||
_content.get(),
|
||||
st::createPollOptionField,
|
||||
Ui::InputField::Mode::NoNewlines,
|
||||
tr::lng_polls_create_option_add())) {
|
||||
InitField(outer, _field, session);
|
||||
_field->setMaxLength(kOptionLimit + kErrorLimit);
|
||||
_field->show();
|
||||
|
||||
result.createShadow();
|
||||
result.createRemove();
|
||||
result.createWarning();
|
||||
return result;
|
||||
_wrap->hide(anim::type::instant);
|
||||
|
||||
_content->widthValue(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateFieldGeometry();
|
||||
}, _field->lifetime());
|
||||
|
||||
_field->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
_content->resize(_content->width(), height);
|
||||
}, _field->lifetime());
|
||||
|
||||
QObject::connect(_field, &Ui::InputField::changed, [=] {
|
||||
Ui::PostponeCall(crl::guard(_field, [=] {
|
||||
if (_hasCorrect) {
|
||||
_correct->toggle(isGood(), anim::type::normal);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
createShadow();
|
||||
createRemove();
|
||||
createWarning();
|
||||
enableChooseCorrect(group);
|
||||
_correctShown.stop();
|
||||
if (_correct) {
|
||||
_correct->finishAnimating();
|
||||
}
|
||||
updateFieldGeometry();
|
||||
}
|
||||
|
||||
//bool Options::Option::hasShadow() const {
|
||||
// return (_shadow != nullptr);
|
||||
//}
|
||||
bool Options::Option::hasShadow() const {
|
||||
return (_shadow != nullptr);
|
||||
}
|
||||
|
||||
void Options::Option::createShadow() {
|
||||
Expects(_field != nullptr);
|
||||
Expects(_content != nullptr);
|
||||
|
||||
if (_shadow) {
|
||||
return;
|
||||
}
|
||||
const auto value = Ui::CreateChild<Ui::PlainShadow>(field().get());
|
||||
value->show();
|
||||
_shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));
|
||||
_shadow->show();
|
||||
field()->sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
const auto left = st::createPollFieldPadding.left();
|
||||
value->setGeometry(
|
||||
_shadow->setGeometry(
|
||||
left,
|
||||
size.height() - st::lineWidth,
|
||||
size.width() - left,
|
||||
st::lineWidth);
|
||||
}, value->lifetime());
|
||||
_shadow.reset(value);
|
||||
}, _shadow->lifetime());
|
||||
}
|
||||
|
||||
//void Options::Option::destroyShadow() {
|
||||
// _shadow = nullptr;
|
||||
//}
|
||||
void Options::Option::destroyShadow() {
|
||||
_shadow = nullptr;
|
||||
}
|
||||
|
||||
void Options::Option::createRemove() {
|
||||
using namespace rpl::mappers;
|
||||
|
|
@ -313,6 +346,10 @@ bool Options::Option::isTooLong() const {
|
|||
return (field()->getLastText().size() > kOptionLimit);
|
||||
}
|
||||
|
||||
bool Options::Option::isCorrect() const {
|
||||
return isGood() && _correct && _correct->entity()->Checkbox::checked();
|
||||
}
|
||||
|
||||
bool Options::Option::hasFocus() const {
|
||||
return field()->hasFocus();
|
||||
}
|
||||
|
|
@ -333,8 +370,66 @@ void Options::Option::toggleRemoveAlways(bool toggled) {
|
|||
*_removeAlways = toggled;
|
||||
}
|
||||
|
||||
void Options::Option::enableChooseCorrect(
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group) {
|
||||
if (!group) {
|
||||
if (_correct) {
|
||||
_hasCorrect = false;
|
||||
_correct->hide(anim::type::normal);
|
||||
toggleCorrectSpace(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
static auto Index = 0;
|
||||
const auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Radiobutton>>(
|
||||
_content.get(),
|
||||
object_ptr<Ui::Radiobutton>(
|
||||
_content.get(),
|
||||
group,
|
||||
++Index,
|
||||
QString(),
|
||||
st::defaultCheckbox));
|
||||
button->entity()->resize(
|
||||
button->entity()->height(),
|
||||
button->entity()->height());
|
||||
button->hide(anim::type::instant);
|
||||
_content->sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
const auto left = st::createPollFieldPadding.left();
|
||||
button->moveToLeft(
|
||||
left,
|
||||
(size.height() - button->heightNoMargins()) / 2);
|
||||
}, button->lifetime());
|
||||
_correct.reset(button);
|
||||
_hasCorrect = true;
|
||||
if (isGood()) {
|
||||
_correct->show(anim::type::normal);
|
||||
} else {
|
||||
_correct->hide(anim::type::instant);
|
||||
}
|
||||
toggleCorrectSpace(true);
|
||||
}
|
||||
|
||||
void Options::Option::toggleCorrectSpace(bool visible) {
|
||||
_correctShown.start(
|
||||
[=] { updateFieldGeometry(); },
|
||||
visible ? 0. : 1.,
|
||||
visible ? 1. : 0.,
|
||||
st::fadeWrapDuration);
|
||||
}
|
||||
|
||||
void Options::Option::updateFieldGeometry() {
|
||||
const auto shown = _correctShown.value(_hasCorrect ? 1. : 0.);
|
||||
const auto skip = st::defaultRadio.diameter
|
||||
+ st::defaultCheckbox.textPosition.x();
|
||||
const auto left = anim::interpolate(0, skip, shown);
|
||||
const auto width = _content->width() - left;
|
||||
_field->resizeToWidth(_content->width() - left);
|
||||
_field->moveToLeft(left, 0);
|
||||
}
|
||||
|
||||
not_null<Ui::InputField*> Options::Option::field() const {
|
||||
return _field->entity();
|
||||
return _field;
|
||||
}
|
||||
|
||||
void Options::Option::removePlaceholder() const {
|
||||
|
|
@ -344,10 +439,12 @@ void Options::Option::removePlaceholder() const {
|
|||
PollAnswer Options::Option::toPollAnswer(int index) const {
|
||||
Expects(index >= 0 && index < kMaxOptionsCount);
|
||||
|
||||
return PollAnswer{
|
||||
auto result = PollAnswer{
|
||||
field()->getLastText().trimmed(),
|
||||
QByteArray(1, ('0' + index))
|
||||
};
|
||||
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
|
||||
return result;
|
||||
}
|
||||
|
||||
rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
|
||||
|
|
@ -357,10 +454,14 @@ rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
|
|||
Options::Options(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session)
|
||||
not_null<Main::Session*> session,
|
||||
bool chooseCorrectEnabled)
|
||||
: _outer(outer)
|
||||
, _container(container)
|
||||
, _session(session)
|
||||
, _chooseCorrectGroup(chooseCorrectEnabled
|
||||
? createChooseCorrectGroup()
|
||||
: nullptr)
|
||||
, _position(_container->count()) {
|
||||
checkLastOption();
|
||||
}
|
||||
|
|
@ -390,19 +491,18 @@ rpl::producer<> Options::backspaceInFront() const {
|
|||
}
|
||||
|
||||
void Options::Option::show(anim::type animated) {
|
||||
_field->hide(anim::type::instant);
|
||||
_field->show(animated);
|
||||
_wrap->show(animated);
|
||||
}
|
||||
|
||||
void Options::Option::destroy(FnMut<void()> done) {
|
||||
if (anim::Disabled() || _field->isHidden()) {
|
||||
if (anim::Disabled() || _wrap->isHidden()) {
|
||||
Ui::PostponeCall(std::move(done));
|
||||
return;
|
||||
}
|
||||
_field->hide(anim::type::normal);
|
||||
_wrap->hide(anim::type::normal);
|
||||
base::call_delayed(
|
||||
st::slideWrapDuration * 2,
|
||||
_field.get(),
|
||||
_content.get(),
|
||||
std::move(done));
|
||||
}
|
||||
|
||||
|
|
@ -410,8 +510,8 @@ std::vector<PollAnswer> Options::toPollAnswers() const {
|
|||
auto result = std::vector<PollAnswer>();
|
||||
result.reserve(_list.size());
|
||||
auto counter = int(0);
|
||||
const auto makeAnswer = [&](const Option &option) {
|
||||
return option.toPollAnswer(counter++);
|
||||
const auto makeAnswer = [&](const std::unique_ptr<Option> &option) {
|
||||
return option->toPollAnswer(counter++);
|
||||
};
|
||||
ranges::copy(
|
||||
_list
|
||||
|
|
@ -424,29 +524,45 @@ std::vector<PollAnswer> Options::toPollAnswers() const {
|
|||
void Options::focusFirst() {
|
||||
Expects(!_list.empty());
|
||||
|
||||
_list.front().setFocus();
|
||||
_list.front()->setFocus();
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> Options::createChooseCorrectGroup() {
|
||||
auto result = std::make_shared<Ui::RadiobuttonGroup>(0);
|
||||
result->setChangedCallback([=](int) {
|
||||
validateState();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void Options::enableChooseCorrect(bool enabled) {
|
||||
_chooseCorrectGroup = enabled
|
||||
? createChooseCorrectGroup()
|
||||
: nullptr;
|
||||
validateState();
|
||||
for (auto &option : _list) {
|
||||
option->enableChooseCorrect(_chooseCorrectGroup);
|
||||
}
|
||||
}
|
||||
|
||||
bool Options::correctShadows() const {
|
||||
// Last one should be without shadow.
|
||||
const auto noShadow = ranges::find(
|
||||
_list,
|
||||
true,
|
||||
ranges::not_fn(&Option::hasShadow));
|
||||
return (noShadow == end(_list) - 1);
|
||||
}
|
||||
|
||||
void Options::fixShadows() {
|
||||
if (correctShadows()) {
|
||||
return;
|
||||
}
|
||||
for (auto &option : _list) {
|
||||
option->createShadow();
|
||||
}
|
||||
_list.back()->destroyShadow();
|
||||
}
|
||||
//
|
||||
//bool Options::correctShadows() const {
|
||||
// // Last one should be without shadow if all options were used.
|
||||
// const auto noShadow = ranges::find(
|
||||
// _list,
|
||||
// true,
|
||||
// ranges::not_fn(&Option::hasShadow));
|
||||
// return (noShadow == end(_list) - (full() ? 1 : 0));
|
||||
//}
|
||||
//
|
||||
//void Options::fixShadows() {
|
||||
// if (correctShadows()) {
|
||||
// return;
|
||||
// }
|
||||
// for (auto &option : _list) {
|
||||
// option.createShadow();
|
||||
// }
|
||||
// if (full()) {
|
||||
// _list.back().destroyShadow();
|
||||
// }
|
||||
//}
|
||||
|
||||
void Options::removeEmptyTail() {
|
||||
// Only one option at the end of options list can be empty.
|
||||
|
|
@ -465,7 +581,7 @@ void Options::removeEmptyTail() {
|
|||
return;
|
||||
}
|
||||
if (focusLast) {
|
||||
emptyItem->setFocus();
|
||||
(*emptyItem)->setFocus();
|
||||
}
|
||||
for (auto i = emptyItem + 1; i != end; ++i) {
|
||||
destroy(std::move(*i));
|
||||
|
|
@ -474,44 +590,46 @@ void Options::removeEmptyTail() {
|
|||
fixAfterErase();
|
||||
}
|
||||
|
||||
void Options::destroy(Option &&option) {
|
||||
const auto field = option.field();
|
||||
option.destroy([=] { removeDestroyed(field); });
|
||||
_destroyed.emplace(std::move(option));
|
||||
void Options::destroy(std::unique_ptr<Option> option) {
|
||||
const auto value = option.get();
|
||||
option->destroy([=] { removeDestroyed(value); });
|
||||
_destroyed.push_back(std::move(option));
|
||||
}
|
||||
|
||||
void Options::fixAfterErase() {
|
||||
Expects(!_list.empty());
|
||||
|
||||
const auto last = _list.end() - 1;
|
||||
last->setPlaceholder();
|
||||
last->toggleRemoveAlways(false);
|
||||
(*last)->setPlaceholder();
|
||||
(*last)->toggleRemoveAlways(false);
|
||||
if (last != begin(_list)) {
|
||||
(last - 1)->setPlaceholder();
|
||||
(last - 1)->toggleRemoveAlways(false);
|
||||
(*(last - 1))->setPlaceholder();
|
||||
(*(last - 1))->toggleRemoveAlways(false);
|
||||
}
|
||||
fixShadows();
|
||||
}
|
||||
|
||||
void Options::addEmptyOption() {
|
||||
if (full()) {
|
||||
return;
|
||||
} else if (!_list.empty() && _list.back().isEmpty()) {
|
||||
} else if (!_list.empty() && _list.back()->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (_list.size() > 1) {
|
||||
(_list.end() - 2)->removePlaceholder();
|
||||
(_list.end() - 2)->toggleRemoveAlways(true);
|
||||
(*(_list.end() - 2))->removePlaceholder();
|
||||
(*(_list.end() - 2))->toggleRemoveAlways(true);
|
||||
}
|
||||
_list.push_back(Option::Create(
|
||||
_list.push_back(std::make_unique<Option>(
|
||||
_outer,
|
||||
_container,
|
||||
_session,
|
||||
_position + _list.size() + _destroyed.size()));
|
||||
const auto field = _list.back().field();
|
||||
_position + _list.size() + _destroyed.size(),
|
||||
_chooseCorrectGroup));
|
||||
const auto field = _list.back()->field();
|
||||
QObject::connect(field, &Ui::InputField::submitted, [=] {
|
||||
const auto index = findField(field);
|
||||
if (_list[index].isGood() && index + 1 < _list.size()) {
|
||||
_list[index + 1].setFocus();
|
||||
if (_list[index]->isGood() && index + 1 < _list.size()) {
|
||||
_list[index + 1]->setFocus();
|
||||
}
|
||||
});
|
||||
QObject::connect(field, &Ui::InputField::changed, [=] {
|
||||
|
|
@ -534,25 +652,25 @@ void Options::addEmptyOption() {
|
|||
|
||||
const auto index = findField(field);
|
||||
if (index > 0) {
|
||||
_list[index - 1].setFocus();
|
||||
_list[index - 1]->setFocus();
|
||||
} else {
|
||||
_backspaceInFront.fire({});
|
||||
}
|
||||
return base::EventFilterResult::Cancel;
|
||||
});
|
||||
|
||||
_list.back().removeClicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
_list.back()->removeClicks(
|
||||
) | rpl::take(1) | rpl::start_with_next([=] {
|
||||
Ui::PostponeCall(crl::guard(field, [=] {
|
||||
Expects(!_list.empty());
|
||||
|
||||
const auto item = begin(_list) + findField(field);
|
||||
if (item == _list.end() - 1) {
|
||||
item->clearValue();
|
||||
(*item)->clearValue();
|
||||
return;
|
||||
}
|
||||
if (item->hasFocus()) {
|
||||
(item + 1)->setFocus();
|
||||
if ((*item)->hasFocus()) {
|
||||
(*(item + 1))->setFocus();
|
||||
}
|
||||
destroy(std::move(*item));
|
||||
_list.erase(item);
|
||||
|
|
@ -561,21 +679,28 @@ void Options::addEmptyOption() {
|
|||
}));
|
||||
}, field->lifetime());
|
||||
|
||||
_list.back().show((_list.size() == 1)
|
||||
_list.back()->show((_list.size() == 1)
|
||||
? anim::type::instant
|
||||
: anim::type::normal);
|
||||
//fixShadows();
|
||||
fixShadows();
|
||||
}
|
||||
|
||||
void Options::removeDestroyed(not_null<Ui::InputField*> field) {
|
||||
_destroyed.erase(_destroyed.find(field));
|
||||
void Options::removeDestroyed(not_null<Option*> option) {
|
||||
const auto i = ranges::find(
|
||||
_destroyed,
|
||||
option.get(),
|
||||
&std::unique_ptr<Option>::get);
|
||||
Assert(i != end(_destroyed));
|
||||
_destroyed.erase(i);
|
||||
}
|
||||
|
||||
void Options::validateState() {
|
||||
checkLastOption();
|
||||
_valid = (ranges::count_if(_list, &Option::isGood) > 1)
|
||||
&& (ranges::find_if(_list, &Option::isTooLong) == end(_list));
|
||||
const auto lastEmpty = !_list.empty() && _list.back().isEmpty();
|
||||
&& (ranges::find_if(_list, &Option::isTooLong) == end(_list))
|
||||
&& (!_chooseCorrectGroup
|
||||
|| ranges::find_if(_list, &Option::isCorrect) != end(_list));
|
||||
const auto lastEmpty = !_list.empty() && _list.back()->isEmpty();
|
||||
_usedCount = _list.size() - (lastEmpty ? 1 : 0);
|
||||
}
|
||||
|
||||
|
|
@ -599,8 +724,12 @@ void Options::checkLastOption() {
|
|||
CreatePollBox::CreatePollBox(
|
||||
QWidget*,
|
||||
not_null<Main::Session*> session,
|
||||
PollData::Flags chosen,
|
||||
PollData::Flags disabled,
|
||||
Api::SendType sendType)
|
||||
: _session(session)
|
||||
, _chosen(chosen)
|
||||
, _disabled(disabled)
|
||||
, _sendType(sendType) {
|
||||
}
|
||||
|
||||
|
|
@ -668,11 +797,17 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
const auto question = setupQuestion(container);
|
||||
AddDivider(container);
|
||||
AddSkip(container);
|
||||
AddSubsectionTitle(container, tr::lng_polls_create_options());
|
||||
container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_polls_create_options(),
|
||||
st::settingsSubsectionTitle),
|
||||
st::createPollFieldTitlePadding);
|
||||
const auto options = lifetime().make_state<Options>(
|
||||
getDelegate()->outerContainer(),
|
||||
container,
|
||||
_session);
|
||||
_session,
|
||||
(_chosen & PollData::Flag::Quiz));
|
||||
auto limit = options->usedCount() | rpl::after_next([=](int count) {
|
||||
setCloseByEscape(!count);
|
||||
setCloseByOutsideClick(!count);
|
||||
|
|
@ -684,11 +819,68 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
container->resizeToWidth(container->widthNoMargins());
|
||||
});
|
||||
container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
container,
|
||||
std::move(limit),
|
||||
st::createPollLimitLabel),
|
||||
st::createPollLimitPadding);
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
std::move(limit),
|
||||
st::boxDividerLabel),
|
||||
st::createPollLimitPadding));
|
||||
|
||||
AddSkip(container);
|
||||
AddSubsectionTitle(container, tr::lng_polls_create_settings());
|
||||
|
||||
const auto anonymous = (!(_disabled & PollData::Flag::PublicVotes))
|
||||
? container->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
container,
|
||||
tr::lng_polls_create_anonymous(tr::now),
|
||||
!(_chosen & PollData::Flag::PublicVotes),
|
||||
st::defaultCheckbox),
|
||||
st::createPollCheckboxMargin)
|
||||
: nullptr;
|
||||
const auto hasMultiple = !(_chosen & PollData::Flag::Quiz)
|
||||
|| !(_disabled & PollData::Flag::Quiz);
|
||||
const auto multiple = hasMultiple
|
||||
? container->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
container,
|
||||
tr::lng_polls_create_multiple_choice(tr::now),
|
||||
(_chosen & PollData::Flag::MultiChoice),
|
||||
st::defaultCheckbox),
|
||||
st::createPollCheckboxMargin)
|
||||
: nullptr;
|
||||
const auto quiz = container->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
container,
|
||||
tr::lng_polls_create_quiz_mode(tr::now),
|
||||
(_chosen & PollData::Flag::Quiz),
|
||||
st::defaultCheckbox),
|
||||
st::createPollCheckboxMargin);
|
||||
quiz->setDisabled(_disabled & PollData::Flag::Quiz);
|
||||
if (multiple) {
|
||||
multiple->setDisabled((_disabled & PollData::Flag::MultiChoice)
|
||||
|| (_chosen & PollData::Flag::Quiz));
|
||||
multiple->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::MouseButtonPress) && quiz->checked();
|
||||
}) | rpl::start_with_next([=] {
|
||||
Ui::Toast::Show("Quiz has only one right answer.");
|
||||
}, multiple->lifetime());
|
||||
}
|
||||
|
||||
using namespace rpl::mappers;
|
||||
quiz->checkedChanges(
|
||||
) | rpl::start_with_next([=](bool checked) {
|
||||
if (multiple) {
|
||||
if (checked && multiple->checked()) {
|
||||
multiple->setChecked(false);
|
||||
}
|
||||
multiple->setDisabled(checked
|
||||
|| (_disabled & PollData::Flag::MultiChoice));
|
||||
}
|
||||
options->enableChooseCorrect(checked);
|
||||
}, quiz->lifetime());
|
||||
|
||||
const auto isValidQuestion = [=] {
|
||||
const auto text = question->getLastText().trimmed();
|
||||
|
|
@ -706,9 +898,16 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
};
|
||||
|
||||
const auto collectResult = [=] {
|
||||
auto result = PollData(id);
|
||||
using Flag = PollData::Flag;
|
||||
auto result = PollData(&_session->data(), id);
|
||||
result.question = question->getLastText().trimmed();
|
||||
result.answers = options->toPollAnswers();
|
||||
const auto publicVotes = (anonymous && !anonymous->checked());
|
||||
const auto multiChoice = (multiple && multiple->checked());
|
||||
result.setFlags(Flag(0)
|
||||
| (publicVotes ? Flag::PublicVotes : Flag(0))
|
||||
| (multiChoice ? Flag::MultiChoice : Flag(0))
|
||||
| (quiz->checked() ? Flag::Quiz : Flag(0)));
|
||||
return result;
|
||||
};
|
||||
const auto send = [=](Api::SendOptions options) {
|
||||
|
|
@ -743,7 +942,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||
tr::lng_polls_create_button(),
|
||||
[=] { send({}); });
|
||||
if (_sendType == Api::SendType::Normal) {
|
||||
SetupSendMenu(
|
||||
SetupSendMenuAndShortcuts(
|
||||
submit.data(),
|
||||
[=] { return SendMenuType::Scheduled; },
|
||||
sendSilent,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ public:
|
|||
CreatePollBox(
|
||||
QWidget*,
|
||||
not_null<Main::Session*> session,
|
||||
PollData::Flags chosen,
|
||||
PollData::Flags disabled,
|
||||
Api::SendType sendType);
|
||||
|
||||
rpl::producer<Result> submitRequests() const;
|
||||
|
|
@ -47,6 +49,8 @@ private:
|
|||
not_null<Ui::VerticalLayout*> container);
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
const PollData::Flags _chosen = PollData::Flags();
|
||||
const PollData::Flags _disabled = PollData::Flags();
|
||||
const Api::SendType _sendType = Api::SendType();
|
||||
Fn<void()> _setInnerFocus;
|
||||
Fn<rpl::producer<bool>()> _dataIsValidValue;
|
||||
|
|
|
|||
|
|
@ -488,97 +488,30 @@ void EditCaptionBox::updateEditMediaButton() {
|
|||
|
||||
void EditCaptionBox::createEditMediaButton() {
|
||||
const auto callback = [=](FileDialog::OpenResult &&result) {
|
||||
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto isValidFile = [](QString mimeType) {
|
||||
if (mimeType == qstr("image/webp")) {
|
||||
Ui::show(
|
||||
Box<InformBox>(tr::lng_edit_media_invalid_file(tr::now)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
auto showBoxErrorCallback = [](tr::phrase<> t) {
|
||||
Ui::show(Box<InformBox>(t(tr::now)), Ui::LayerOption::KeepOther);
|
||||
};
|
||||
|
||||
if (!result.remoteContent.isEmpty()) {
|
||||
auto list = Storage::PreparedList::PreparedFileFromFilesDialog(
|
||||
std::move(result),
|
||||
_isAlbum,
|
||||
std::move(showBoxErrorCallback),
|
||||
st::sendMediaPreviewSize);
|
||||
|
||||
auto list = Storage::PrepareMediaFromImage(
|
||||
QImage(),
|
||||
std::move(result.remoteContent),
|
||||
st::sendMediaPreviewSize);
|
||||
|
||||
if (!isValidFile(list.files.front().mime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isAlbum) {
|
||||
const auto albumMimes = {
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"video/mp4",
|
||||
};
|
||||
const auto file = &list.files.front();
|
||||
if (ranges::find(albumMimes, file->mime) == end(albumMimes)
|
||||
|| file->type == Storage::PreparedFile::AlbumType::None) {
|
||||
Ui::show(
|
||||
Box<InformBox>(tr::lng_edit_media_album_error(tr::now)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_preparedList = std::move(list);
|
||||
} else if (!result.paths.isEmpty()) {
|
||||
auto list = Storage::PrepareMediaList(
|
||||
QStringList(result.paths.front()),
|
||||
st::sendMediaPreviewSize);
|
||||
|
||||
// Don't rewrite _preparedList if new list is not valid for album.
|
||||
if (_isAlbum) {
|
||||
using Info = FileMediaInformation;
|
||||
|
||||
const auto media = &list.files.front().information->media;
|
||||
const auto valid = media->match([&](const Info::Image &data) {
|
||||
return Storage::ValidateThumbDimensions(
|
||||
data.data.width(),
|
||||
data.data.height())
|
||||
&& !data.animated;
|
||||
}, [&](Info::Video &data) {
|
||||
data.isGifv = false;
|
||||
return true;
|
||||
}, [](auto &&other) {
|
||||
return false;
|
||||
});
|
||||
if (!valid) {
|
||||
Ui::show(
|
||||
Box<InformBox>(tr::lng_edit_media_album_error(tr::now)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto info = QFileInfo(result.paths.front());
|
||||
if (!isValidFile(Core::MimeTypeForFile(info).name())) {
|
||||
return;
|
||||
}
|
||||
|
||||
_preparedList = std::move(list);
|
||||
} else {
|
||||
return;
|
||||
if (list) {
|
||||
_preparedList = std::move(*list);
|
||||
updateEditPreview();
|
||||
}
|
||||
|
||||
updateEditPreview();
|
||||
};
|
||||
|
||||
const auto buttonCallback = [=] {
|
||||
const auto filters = _isAlbum
|
||||
? QStringList(qsl("Image and Video Files (*.png *.jpg *.mp4)"))
|
||||
: QStringList(FileDialog::AllFilesFilter());
|
||||
? FileDialog::AlbumFilesFilter()
|
||||
: FileDialog::AllFilesFilter();
|
||||
FileDialog::GetOpenPath(
|
||||
this,
|
||||
tr::lng_choose_file(tr::now),
|
||||
filters.join(qsl(";;")),
|
||||
filters,
|
||||
crl::guard(this, callback));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "confirm_box.h"
|
||||
#include "history/view/history_view_schedule_box.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/mime_type.h"
|
||||
|
|
@ -48,8 +49,102 @@ namespace {
|
|||
constexpr auto kMinPreviewWidth = 20;
|
||||
constexpr auto kShrinkDuration = crl::time(150);
|
||||
constexpr auto kDragDuration = crl::time(200);
|
||||
const auto kStickerMimeString = qstr("image/webp");
|
||||
const auto kAnimatedStickerMimeString = qstr("application/x-tgsticker");
|
||||
|
||||
enum class ButtonType {
|
||||
Edit,
|
||||
Delete,
|
||||
None,
|
||||
};
|
||||
|
||||
inline bool CanAddUrls(const QList<QUrl> &urls) {
|
||||
return !urls.isEmpty() && ranges::find_if(
|
||||
urls,
|
||||
[](const QUrl &url) { return !url.isLocalFile(); }
|
||||
) == urls.end();
|
||||
}
|
||||
|
||||
inline bool IsFirstAlbumItem(const Storage::PreparedList &list) {
|
||||
using AlbumType = Storage::PreparedFile::AlbumType;
|
||||
return (list.files.size() > 0)
|
||||
&& (list.files.front().type != AlbumType::None);
|
||||
}
|
||||
|
||||
inline bool IsSingleItem(const Storage::PreparedList &list) {
|
||||
return list.files.size() == 1;
|
||||
}
|
||||
|
||||
QRect PaintAlbumThumbButtons(
|
||||
Painter &p,
|
||||
QPoint point,
|
||||
int outerWidth,
|
||||
float64 shrinkProgress) {
|
||||
|
||||
const auto skipInternal = st::sendBoxAlbumGroupEditInternalSkip;
|
||||
const auto size = st::sendBoxAlbumGroupHeight;
|
||||
const auto skipRight = st::sendBoxAlbumGroupSkipRight;
|
||||
const auto skipTop = st::sendBoxAlbumGroupSkipTop;
|
||||
const auto groupWidth = size * 2 + skipInternal;
|
||||
|
||||
// If the width is tiny, it would be better to not display the buttons.
|
||||
if (groupWidth > outerWidth) {
|
||||
return QRect();
|
||||
}
|
||||
|
||||
// If the width is too small,
|
||||
// it would be better to display the buttons in the center.
|
||||
const auto groupX = point.x() + ((groupWidth + skipRight * 2 > outerWidth)
|
||||
? (outerWidth - groupWidth) / 2
|
||||
: outerWidth - skipRight - groupWidth);
|
||||
const auto groupY = point.y() + skipTop;
|
||||
const auto deleteLeft = skipInternal + size;
|
||||
|
||||
p.setOpacity(1.0 - shrinkProgress);
|
||||
|
||||
QRect groupRect(groupX, groupY, groupWidth, size);
|
||||
App::roundRect(
|
||||
p,
|
||||
groupRect,
|
||||
st::callFingerprintBg,
|
||||
SendFilesBoxAlbumGroupCorners);
|
||||
|
||||
const auto editP = st::sendBoxAlbumGroupEditButtonIconPosition;
|
||||
const auto deleteP = st::sendBoxAlbumGroupDeleteButtonIconPosition;
|
||||
|
||||
st::sendBoxAlbumGroupEditButtonIcon.paintInCenter(
|
||||
p,
|
||||
QRect(groupX + editP.x(), groupY + editP.y(), size, size));
|
||||
st::sendBoxAlbumGroupDeleteButtonIcon.paintInCenter(
|
||||
p,
|
||||
QRect(
|
||||
groupX + deleteLeft + deleteP.x(),
|
||||
groupY + deleteP.y(),
|
||||
size,
|
||||
size));
|
||||
p.setOpacity(1);
|
||||
|
||||
return groupRect;
|
||||
}
|
||||
|
||||
void FileDialogCallback(
|
||||
FileDialog::OpenResult &&result,
|
||||
bool isAlbum,
|
||||
Fn<void(Storage::PreparedList)> callback) {
|
||||
auto showBoxErrorCallback = [](tr::phrase<> text) {
|
||||
Ui::show(Box<InformBox>(text(tr::now)), Ui::LayerOption::KeepOther);
|
||||
};
|
||||
|
||||
auto list = Storage::PreparedList::PreparedFileFromFilesDialog(
|
||||
std::move(result),
|
||||
isAlbum,
|
||||
std::move(showBoxErrorCallback),
|
||||
st::sendMediaPreviewSize);
|
||||
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(std::move(*list));
|
||||
}
|
||||
|
||||
class SingleMediaPreview : public Ui::RpWidget {
|
||||
public:
|
||||
|
|
@ -123,7 +218,10 @@ class AlbumThumb {
|
|||
public:
|
||||
AlbumThumb(
|
||||
const Storage::PreparedFile &file,
|
||||
const Ui::GroupMediaLayout &layout);
|
||||
const Ui::GroupMediaLayout &layout,
|
||||
QWidget *parent,
|
||||
Fn<void()> editCallback,
|
||||
Fn<void()> deleteCallback);
|
||||
|
||||
void moveToLayout(const Ui::GroupMediaLayout &layout);
|
||||
void animateLayoutToInitial();
|
||||
|
|
@ -141,6 +239,8 @@ public:
|
|||
void paintFile(Painter &p, int left, int top, int outerWidth);
|
||||
|
||||
bool containsPoint(QPoint position) const;
|
||||
bool buttonsContainPoint(QPoint position) const;
|
||||
ButtonType buttonTypeFromPoint(QPoint position) const;
|
||||
int distanceTo(QPoint position) const;
|
||||
bool isPointAfter(QPoint position) const;
|
||||
void moveInAlbum(QPoint to);
|
||||
|
|
@ -148,6 +248,8 @@ public:
|
|||
void suggestMove(float64 delta, Fn<void()> callback);
|
||||
void finishAnimations();
|
||||
|
||||
void updateFileRow(int row);
|
||||
|
||||
private:
|
||||
QRect countRealGeometry() const;
|
||||
QRect countCurrentGeometry(float64 progress) const;
|
||||
|
|
@ -173,11 +275,19 @@ private:
|
|||
Ui::Animations::Simple _suggestedMoveAnimation;
|
||||
int _lastShrinkValue = 0;
|
||||
|
||||
QRect _lastRectOfButtons;
|
||||
|
||||
object_ptr<Ui::IconButton> _editMedia = nullptr;
|
||||
object_ptr<Ui::IconButton> _deleteMedia = nullptr;
|
||||
|
||||
};
|
||||
|
||||
AlbumThumb::AlbumThumb(
|
||||
const Storage::PreparedFile &file,
|
||||
const Ui::GroupMediaLayout &layout)
|
||||
const Ui::GroupMediaLayout &layout,
|
||||
QWidget *parent,
|
||||
Fn<void()> editCallback,
|
||||
Fn<void()> deleteCallback)
|
||||
: _layout(layout)
|
||||
, _fullPreview(file.preview)
|
||||
, _shrinkSize(int(std::ceil(st::historyMessageRadius / 1.4)))
|
||||
|
|
@ -218,7 +328,11 @@ AlbumThumb::AlbumThumb(
|
|||
|
||||
const auto availableFileWidth = st::sendMediaPreviewSize
|
||||
- st::sendMediaFileThumbSkip
|
||||
- st::sendMediaFileThumbSize;
|
||||
- st::sendMediaFileThumbSize
|
||||
// Right buttons.
|
||||
- st::sendBoxAlbumGroupButtonFile.width * 2
|
||||
- st::sendBoxAlbumGroupEditInternalSkip
|
||||
- st::sendBoxAlbumGroupSkipRight;
|
||||
const auto filepath = file.path;
|
||||
if (filepath.isEmpty()) {
|
||||
_name = filedialogDefaultName(
|
||||
|
|
@ -245,6 +359,45 @@ AlbumThumb::AlbumThumb(
|
|||
_nameWidth = st::semiboldFont->width(_name);
|
||||
}
|
||||
_statusWidth = st::normalFont->width(_status);
|
||||
|
||||
_editMedia.create(parent, st::sendBoxAlbumGroupButtonFile);
|
||||
_deleteMedia.create(parent, st::sendBoxAlbumGroupButtonFile);
|
||||
|
||||
const auto duration = st::historyAttach.ripple.hideDuration;
|
||||
_editMedia->setClickedCallback(App::LambdaDelayed(
|
||||
duration,
|
||||
parent,
|
||||
std::move(editCallback)));
|
||||
_deleteMedia->setClickedCallback(App::LambdaDelayed(
|
||||
duration,
|
||||
parent,
|
||||
std::move(deleteCallback)));
|
||||
|
||||
_editMedia->setIconOverride(&st::editMediaButtonIconFile);
|
||||
_deleteMedia->setIconOverride(&st::sendBoxAlbumGroupDeleteButtonIconFile);
|
||||
|
||||
updateFileRow(-1);
|
||||
}
|
||||
|
||||
void AlbumThumb::updateFileRow(int row) {
|
||||
if (row < 0) {
|
||||
_editMedia->hide();
|
||||
_deleteMedia->hide();
|
||||
return;
|
||||
}
|
||||
_editMedia->show();
|
||||
_deleteMedia->show();
|
||||
|
||||
const auto fileHeight = st::sendMediaFileThumbSize
|
||||
+ st::sendMediaFileThumbSkip;
|
||||
|
||||
const auto top = row * fileHeight + st::sendBoxAlbumGroupSkipTop;
|
||||
const auto size = st::editMediaButtonSize;
|
||||
|
||||
auto right = st::sendBoxAlbumGroupSkipRight + size;
|
||||
_deleteMedia->moveToRight(right, top);
|
||||
right += st::sendBoxAlbumGroupEditInternalSkip + size;
|
||||
_editMedia->moveToRight(right, top);
|
||||
}
|
||||
|
||||
void AlbumThumb::resetLayoutAnimation() {
|
||||
|
|
@ -337,6 +490,12 @@ void AlbumThumb::paintInAlbum(
|
|||
}
|
||||
st::historyFileThumbPlay.paintInCenter(p, inner);
|
||||
}
|
||||
|
||||
_lastRectOfButtons = PaintAlbumThumbButtons(
|
||||
p,
|
||||
{ x, y },
|
||||
geometry.width(),
|
||||
shrinkProgress);
|
||||
}
|
||||
|
||||
void AlbumThumb::prepareCache(QSize size, int shrink) {
|
||||
|
|
@ -490,6 +649,12 @@ void AlbumThumb::paintPhoto(Painter &p, int left, int top, int outerWidth) {
|
|||
top,
|
||||
outerWidth,
|
||||
_photo);
|
||||
|
||||
_lastRectOfButtons = PaintAlbumThumbButtons(
|
||||
p,
|
||||
{ left, top },
|
||||
st::sendMediaPreviewSize,
|
||||
0);
|
||||
}
|
||||
|
||||
void AlbumThumb::paintFile(Painter &p, int left, int top, int outerWidth) {
|
||||
|
|
@ -520,6 +685,19 @@ bool AlbumThumb::containsPoint(QPoint position) const {
|
|||
return _layout.geometry.contains(position);
|
||||
}
|
||||
|
||||
bool AlbumThumb::buttonsContainPoint(QPoint position) const {
|
||||
return _lastRectOfButtons.contains(position);
|
||||
}
|
||||
|
||||
ButtonType AlbumThumb::buttonTypeFromPoint(QPoint position) const {
|
||||
if (!buttonsContainPoint(position)) {
|
||||
return ButtonType::None;
|
||||
}
|
||||
return (position.x() < _lastRectOfButtons.center().x())
|
||||
? ButtonType::Edit
|
||||
: ButtonType::Delete;
|
||||
}
|
||||
|
||||
int AlbumThumb::distanceTo(QPoint position) const {
|
||||
const auto delta = (_layout.geometry.center() - position);
|
||||
return QPoint::dotProduct(delta, delta);
|
||||
|
|
@ -601,14 +779,12 @@ SingleMediaPreview *SingleMediaPreview::Create(
|
|||
preview.height())) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto sticker = (file.information->filemime == kStickerMimeString)
|
||||
|| (file.information->filemime == kAnimatedStickerMimeString);
|
||||
return Ui::CreateChild<SingleMediaPreview>(
|
||||
parent,
|
||||
controller,
|
||||
preview,
|
||||
animated,
|
||||
sticker,
|
||||
Core::IsMimeSticker(file.information->filemime),
|
||||
animationPreview ? file.path : QString());
|
||||
}
|
||||
|
||||
|
|
@ -960,6 +1136,14 @@ public:
|
|||
void setSendWay(SendFilesWay way);
|
||||
std::vector<int> takeOrder();
|
||||
|
||||
auto thumbDeleted() {
|
||||
return _thumbDeleted.events();
|
||||
}
|
||||
|
||||
auto thumbChanged() {
|
||||
return _thumbChanged.events();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
|
@ -974,6 +1158,15 @@ private:
|
|||
void prepareThumbs();
|
||||
void updateSizeAnimated(const std::vector<Ui::GroupMediaLayout> &layout);
|
||||
void updateSize();
|
||||
void updateFileRows();
|
||||
|
||||
int thumbIndex(AlbumThumb *thumb);
|
||||
AlbumThumb *thumbUnderCursor();
|
||||
void deleteThumbByIndex(int index);
|
||||
void changeThumbByIndex(int index);
|
||||
void thumbButtonsCallback(
|
||||
not_null<AlbumThumb*> thumb,
|
||||
ButtonType type);
|
||||
|
||||
void paintAlbum(Painter &p) const;
|
||||
void paintPhotos(Painter &p, QRect clip) const;
|
||||
|
|
@ -1003,6 +1196,9 @@ private:
|
|||
AlbumThumb *_paintedAbove = nullptr;
|
||||
QPoint _draggedStartPosition;
|
||||
|
||||
rpl::event_stream<int> _thumbDeleted;
|
||||
rpl::event_stream<int> _thumbChanged;
|
||||
|
||||
mutable Ui::Animations::Simple _thumbsHeightAnimation;
|
||||
mutable Ui::Animations::Simple _shrinkAnimation;
|
||||
mutable Ui::Animations::Simple _finishDragAnimation;
|
||||
|
|
@ -1019,6 +1215,7 @@ SendFilesBox::AlbumPreview::AlbumPreview(
|
|||
setMouseTracking(true);
|
||||
prepareThumbs();
|
||||
updateSize();
|
||||
updateFileRows();
|
||||
}
|
||||
|
||||
void SendFilesBox::AlbumPreview::setSendWay(SendFilesWay way) {
|
||||
|
|
@ -1027,9 +1224,18 @@ void SendFilesBox::AlbumPreview::setSendWay(SendFilesWay way) {
|
|||
_sendWay = way;
|
||||
}
|
||||
updateSize();
|
||||
updateFileRows();
|
||||
update();
|
||||
}
|
||||
|
||||
void SendFilesBox::AlbumPreview::updateFileRows() {
|
||||
Expects(_order.size() == _thumbs.size());
|
||||
const auto isFile = (_sendWay == SendFilesWay::Files);
|
||||
for (auto i = 0; i < _order.size(); i++) {
|
||||
_thumbs[i]->updateFileRow(isFile ? _order[i] : -1);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int> SendFilesBox::AlbumPreview::takeOrder() {
|
||||
auto reordered = std::vector<std::unique_ptr<AlbumThumb>>();
|
||||
reordered.reserve(_thumbs.size());
|
||||
|
|
@ -1071,7 +1277,10 @@ void SendFilesBox::AlbumPreview::prepareThumbs() {
|
|||
for (auto i = 0; i != count; ++i) {
|
||||
_thumbs.push_back(std::make_unique<AlbumThumb>(
|
||||
_list.files[i],
|
||||
layout[i]));
|
||||
layout[i],
|
||||
this,
|
||||
[=] { changeThumbByIndex(thumbIndex(thumbUnderCursor())); },
|
||||
[=] { deleteThumbByIndex(thumbIndex(thumbUnderCursor())); }));
|
||||
}
|
||||
_thumbsHeight = countLayoutHeight(layout);
|
||||
_photosHeight = ranges::accumulate(ranges::view::all(
|
||||
|
|
@ -1094,9 +1303,27 @@ int SendFilesBox::AlbumPreview::contentTop() const {
|
|||
|
||||
AlbumThumb *SendFilesBox::AlbumPreview::findThumb(QPoint position) const {
|
||||
position -= QPoint(contentLeft(), contentTop());
|
||||
const auto i = ranges::find_if(_thumbs, [&](const auto &thumb) {
|
||||
return thumb->containsPoint(position);
|
||||
});
|
||||
|
||||
auto top = 0;
|
||||
const auto isPhotosWay = (_sendWay == SendFilesWay::Photos);
|
||||
const auto skip = isPhotosWay
|
||||
? st::sendMediaPreviewPhotoSkip
|
||||
: st::sendMediaFileThumbSkip;
|
||||
auto find = [&](const auto &thumb) {
|
||||
if (_sendWay == SendFilesWay::Album) {
|
||||
return thumb->containsPoint(position);
|
||||
} else if (isPhotosWay || _sendWay == SendFilesWay::Files) {
|
||||
const auto bottom = top + (isPhotosWay
|
||||
? thumb->photoHeight()
|
||||
: st::sendMediaFileThumbSize);
|
||||
const auto isUnderTop = (position.y() > top);
|
||||
top = bottom + skip;
|
||||
return isUnderTop && (position.y() < bottom);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto i = ranges::find_if(_thumbs, std::move(find));
|
||||
return (i == _thumbs.end()) ? nullptr : i->get();
|
||||
}
|
||||
|
||||
|
|
@ -1277,6 +1504,56 @@ void SendFilesBox::AlbumPreview::paintFiles(Painter &p, QRect clip) const {
|
|||
}
|
||||
}
|
||||
|
||||
int SendFilesBox::AlbumPreview::thumbIndex(AlbumThumb *thumb) {
|
||||
if (!thumb) {
|
||||
return -1;
|
||||
}
|
||||
const auto thumbIt = ranges::find_if(_thumbs, [&](auto &t) {
|
||||
return t.get() == thumb;
|
||||
});
|
||||
Expects(thumbIt != _thumbs.end());
|
||||
return std::distance(_thumbs.begin(), thumbIt);
|
||||
}
|
||||
|
||||
AlbumThumb *SendFilesBox::AlbumPreview::thumbUnderCursor() {
|
||||
return findThumb(mapFromGlobal(QCursor::pos()));
|
||||
}
|
||||
|
||||
void SendFilesBox::AlbumPreview::deleteThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
const auto orderIt = ranges::find(_order, index);
|
||||
Expects(orderIt != _order.end());
|
||||
|
||||
_order.erase(orderIt);
|
||||
ranges::for_each(_order, [=](auto &i) {
|
||||
if (i > index) {
|
||||
i--;
|
||||
}
|
||||
});
|
||||
_thumbDeleted.fire(std::move(index));
|
||||
}
|
||||
|
||||
void SendFilesBox::AlbumPreview::changeThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
_thumbChanged.fire(std::move(index));
|
||||
}
|
||||
|
||||
void SendFilesBox::AlbumPreview::thumbButtonsCallback(
|
||||
not_null<AlbumThumb*> thumb,
|
||||
ButtonType type) {
|
||||
const auto index = thumbIndex(thumb);
|
||||
|
||||
switch (type) {
|
||||
case ButtonType::None: return;
|
||||
case ButtonType::Edit: changeThumbByIndex(index); break;
|
||||
case ButtonType::Delete: deleteThumbByIndex(index); break;
|
||||
}
|
||||
}
|
||||
|
||||
void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
|
||||
if (_finishDragAnimation.animating()) {
|
||||
return;
|
||||
|
|
@ -1284,6 +1561,10 @@ void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
|
|||
const auto position = e->pos();
|
||||
cancelDrag();
|
||||
if (const auto thumb = findThumb(position)) {
|
||||
if (thumb->buttonsContainPoint(e->pos())) {
|
||||
thumbButtonsCallback(thumb, thumb->buttonTypeFromPoint(e->pos()));
|
||||
return;
|
||||
}
|
||||
_paintedAbove = _suggestedThumb = _draggedThumb = thumb;
|
||||
_draggedStartPosition = position;
|
||||
_shrinkAnimation.start([=] { update(); }, 0., 1., kShrinkDuration);
|
||||
|
|
@ -1291,19 +1572,26 @@ void SendFilesBox::AlbumPreview::mousePressEvent(QMouseEvent *e) {
|
|||
}
|
||||
|
||||
void SendFilesBox::AlbumPreview::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (_sendWay != SendFilesWay::Album) {
|
||||
if (_sendWay == SendFilesWay::Files) {
|
||||
applyCursor(style::cur_default);
|
||||
return;
|
||||
}
|
||||
if (_draggedThumb) {
|
||||
const auto isAlbum = (_sendWay == SendFilesWay::Album);
|
||||
if (isAlbum && _draggedThumb) {
|
||||
const auto position = e->pos();
|
||||
_draggedThumb->moveInAlbum(position - _draggedStartPosition);
|
||||
updateSuggestedDrag(_draggedThumb->center());
|
||||
update();
|
||||
} else {
|
||||
const auto cursor = findThumb(e->pos())
|
||||
const auto thumb = findThumb(e->pos());
|
||||
const auto regularCursor = isAlbum
|
||||
? style::cur_sizeall
|
||||
: style::cur_default;
|
||||
const auto cursor = thumb
|
||||
? (thumb->buttonsContainPoint(e->pos())
|
||||
? style::cur_pointer
|
||||
: regularCursor)
|
||||
: style::cur_default;
|
||||
applyCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
|
@ -1405,7 +1693,8 @@ void SendFilesBox::initPreview(rpl::producer<int> desiredPreviewHeight) {
|
|||
) | rpl::start_with_next([=](int height) {
|
||||
setDimensions(
|
||||
st::boxWideWidth,
|
||||
std::min(st::sendMediaPreviewHeightMax, height));
|
||||
std::min(st::sendMediaPreviewHeightMax, height),
|
||||
true);
|
||||
}, lifetime());
|
||||
|
||||
if (_preview) {
|
||||
|
|
@ -1414,7 +1703,7 @@ void SendFilesBox::initPreview(rpl::producer<int> desiredPreviewHeight) {
|
|||
}
|
||||
|
||||
void SendFilesBox::prepareSingleFilePreview() {
|
||||
Expects(_list.files.size() == 1);
|
||||
Expects(IsSingleItem(_list));
|
||||
|
||||
const auto &file = _list.files[0];
|
||||
const auto media = SingleMediaPreview::Create(this, _controller, file);
|
||||
|
|
@ -1442,11 +1731,74 @@ void SendFilesBox::prepareAlbumPreview() {
|
|||
this,
|
||||
_list,
|
||||
_sendWay->value()));
|
||||
|
||||
addThumbButtonHandlers(wrap);
|
||||
|
||||
_preview = wrap;
|
||||
_albumPreview->show();
|
||||
setupShadows(wrap, _albumPreview);
|
||||
|
||||
initPreview(_albumPreview->desiredHeightValue());
|
||||
|
||||
crl::on_main([=] {
|
||||
wrap->scrollToY(_lastScrollTop);
|
||||
_lastScrollTop = 0;
|
||||
});
|
||||
}
|
||||
|
||||
void SendFilesBox::addThumbButtonHandlers(not_null<Ui::ScrollArea*> wrap) {
|
||||
_albumPreview->thumbDeleted(
|
||||
) | rpl::start_with_next([=](auto index) {
|
||||
_lastScrollTop = wrap->scrollTop();
|
||||
|
||||
_list.files.erase(_list.files.begin() + index);
|
||||
applyAlbumOrder();
|
||||
|
||||
if (_preview) {
|
||||
_preview->deleteLater();
|
||||
}
|
||||
_albumPreview = nullptr;
|
||||
|
||||
if (IsSingleItem(_list)) {
|
||||
_list.albumIsPossible = false;
|
||||
if (_sendWay->value() == SendFilesWay::Album) {
|
||||
_sendWay->setValue(SendFilesWay::Photos);
|
||||
}
|
||||
}
|
||||
|
||||
_compressConfirm = _compressConfirmInitial;
|
||||
refreshAllAfterAlbumChanges();
|
||||
|
||||
}, _albumPreview->lifetime());
|
||||
|
||||
_albumPreview->thumbChanged(
|
||||
) | rpl::start_with_next([=](auto index) {
|
||||
_lastScrollTop = wrap->scrollTop();
|
||||
|
||||
const auto callback = [=](FileDialog::OpenResult &&result) {
|
||||
FileDialogCallback(
|
||||
std::move(result),
|
||||
true,
|
||||
[=] (auto list) {
|
||||
_list.files[index] = std::move(list.files.front());
|
||||
applyAlbumOrder();
|
||||
|
||||
if (_preview) {
|
||||
_preview->deleteLater();
|
||||
}
|
||||
_albumPreview = nullptr;
|
||||
|
||||
refreshAllAfterAlbumChanges();
|
||||
});
|
||||
};
|
||||
|
||||
FileDialog::GetOpenPath(
|
||||
this,
|
||||
tr::lng_choose_file(tr::now),
|
||||
FileDialog::AlbumFilesFilter(),
|
||||
crl::guard(this, callback));
|
||||
|
||||
}, _albumPreview->lifetime());
|
||||
}
|
||||
|
||||
void SendFilesBox::setupShadows(
|
||||
|
|
@ -1482,7 +1834,7 @@ void SendFilesBox::setupShadows(
|
|||
void SendFilesBox::prepare() {
|
||||
_send = addButton(tr::lng_send_button(), [=] { send({}); });
|
||||
if (_sendType == Api::SendType::Normal) {
|
||||
SetupSendMenu(
|
||||
SetupSendMenuAndShortcuts(
|
||||
_send,
|
||||
[=] { return _sendMenuType; },
|
||||
[=] { sendSilent(); },
|
||||
|
|
@ -1497,6 +1849,49 @@ void SendFilesBox::prepare() {
|
|||
_cancelledCallback();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
const auto title = tr::lng_stickers_featured_add(tr::now) + qsl("...");
|
||||
_addFileToAlbum = addLeftButton(
|
||||
rpl::single(title),
|
||||
App::LambdaDelayed(st::historyAttach.ripple.hideDuration, this, [=] {
|
||||
openDialogToAddFileToAlbum();
|
||||
}));
|
||||
|
||||
updateLeftButtonVisibility();
|
||||
}
|
||||
|
||||
void SendFilesBox::updateLeftButtonVisibility() {
|
||||
const auto isAlbum = _list.albumIsPossible
|
||||
&& (_list.files.size() < Storage::MaxAlbumItems());
|
||||
if (isAlbum || (IsSingleItem(_list) && IsFirstAlbumItem(_list))) {
|
||||
_addFileToAlbum->show();
|
||||
} else {
|
||||
_addFileToAlbum->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void SendFilesBox::refreshAllAfterAlbumChanges() {
|
||||
refreshAlbumMediaCount();
|
||||
preparePreview();
|
||||
captionResized();
|
||||
updateLeftButtonVisibility();
|
||||
}
|
||||
|
||||
void SendFilesBox::openDialogToAddFileToAlbum() {
|
||||
const auto callback = [=](FileDialog::OpenResult &&result) {
|
||||
FileDialogCallback(
|
||||
std::move(result),
|
||||
true,
|
||||
[=] (auto list) {
|
||||
addFiles(std::move(list));
|
||||
});
|
||||
};
|
||||
|
||||
FileDialog::GetOpenPaths(
|
||||
this,
|
||||
tr::lng_choose_file(tr::now),
|
||||
FileDialog::AlbumFilesFilter(),
|
||||
crl::guard(this, callback));
|
||||
}
|
||||
|
||||
void SendFilesBox::initSendWay() {
|
||||
|
|
@ -1574,7 +1969,7 @@ void SendFilesBox::refreshAlbumMediaCount() {
|
|||
}
|
||||
|
||||
void SendFilesBox::preparePreview() {
|
||||
if (_list.files.size() == 1) {
|
||||
if (IsSingleItem(_list)) {
|
||||
prepareSingleFilePreview();
|
||||
} else {
|
||||
if (_list.albumIsPossible) {
|
||||
|
|
@ -1611,7 +2006,7 @@ void SendFilesBox::setupSendWayControls() {
|
|||
addRadio(_sendAlbum, SendFilesWay::Album, tr::lng_send_album(tr::now));
|
||||
}
|
||||
if (!_list.albumIsPossible || _albumPhotosCount > 0) {
|
||||
addRadio(_sendPhotos, SendFilesWay::Photos, (_list.files.size() == 1)
|
||||
addRadio(_sendPhotos, SendFilesWay::Photos, IsSingleItem(_list)
|
||||
? tr::lng_send_photo(tr::now)
|
||||
: (_albumVideosCount > 0)
|
||||
? tr::lng_send_separate_photos_videos(tr::now)
|
||||
|
|
@ -1619,7 +2014,7 @@ void SendFilesBox::setupSendWayControls() {
|
|||
? tr::lng_send_separate_photos(tr::now)
|
||||
: tr::lng_send_photos(tr::now, lt_count, _list.files.size())));
|
||||
}
|
||||
addRadio(_sendFiles, SendFilesWay::Files, (_list.files.size() == 1)
|
||||
addRadio(_sendFiles, SendFilesWay::Files, (IsSingleItem(_list))
|
||||
? tr::lng_send_file(tr::now)
|
||||
: tr::lng_send_files(tr::now, lt_count, _list.files.size()));
|
||||
}
|
||||
|
|
@ -1746,16 +2141,9 @@ void SendFilesBox::captionResized() {
|
|||
update();
|
||||
}
|
||||
|
||||
bool SendFilesBox::canAddUrls(const QList<QUrl> &urls) const {
|
||||
return !urls.isEmpty() && ranges::find_if(
|
||||
urls,
|
||||
[](const QUrl &url) { return !url.isLocalFile(); }
|
||||
) == urls.end();
|
||||
}
|
||||
|
||||
bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
|
||||
const auto urls = data->hasUrls() ? data->urls() : QList<QUrl>();
|
||||
auto filesCount = canAddUrls(urls) ? urls.size() : 0;
|
||||
auto filesCount = CanAddUrls(urls) ? urls.size() : 0;
|
||||
if (!filesCount && data->hasImage()) {
|
||||
++filesCount;
|
||||
}
|
||||
|
|
@ -1764,8 +2152,7 @@ bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
|
|||
return false;
|
||||
} else if (_list.files.size() > 1 && !_albumPreview) {
|
||||
return false;
|
||||
} else if (_list.files.front().type
|
||||
== Storage::PreparedFile::AlbumType::None) {
|
||||
} else if (!IsFirstAlbumItem(_list)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -1774,7 +2161,7 @@ bool SendFilesBox::canAddFiles(not_null<const QMimeData*> data) const {
|
|||
bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
|
||||
auto list = [&] {
|
||||
const auto urls = data->hasUrls() ? data->urls() : QList<QUrl>();
|
||||
auto result = canAddUrls(urls)
|
||||
auto result = CanAddUrls(urls)
|
||||
? Storage::PrepareMediaList(urls, st::sendMediaPreviewSize)
|
||||
: Storage::PreparedList(
|
||||
Storage::PreparedList::Error::EmptyFile,
|
||||
|
|
@ -1792,35 +2179,35 @@ bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
|
|||
}
|
||||
return result;
|
||||
}();
|
||||
if (_list.files.size() + list.files.size() > Storage::MaxAlbumItems()) {
|
||||
return addFiles(std::move(list));
|
||||
}
|
||||
|
||||
bool SendFilesBox::addFiles(Storage::PreparedList list) {
|
||||
const auto sumFiles = _list.files.size() + list.files.size();
|
||||
const auto cutToAlbumSize = (sumFiles > Storage::MaxAlbumItems());
|
||||
if (list.error != Storage::PreparedList::Error::None) {
|
||||
return false;
|
||||
} else if (list.error != Storage::PreparedList::Error::None) {
|
||||
} else if (!IsSingleItem(list) && !list.albumIsPossible) {
|
||||
return false;
|
||||
} else if (list.files.size() != 1 && !list.albumIsPossible) {
|
||||
return false;
|
||||
} else if (list.files.front().type
|
||||
== Storage::PreparedFile::AlbumType::None) {
|
||||
} else if (!IsFirstAlbumItem(list)) {
|
||||
return false;
|
||||
} else if (_list.files.size() > 1 && !_albumPreview) {
|
||||
return false;
|
||||
} else if (_list.files.front().type
|
||||
== Storage::PreparedFile::AlbumType::None) {
|
||||
} else if (!IsFirstAlbumItem(_list)) {
|
||||
return false;
|
||||
}
|
||||
applyAlbumOrder();
|
||||
delete base::take(_preview);
|
||||
_albumPreview = nullptr;
|
||||
|
||||
if (_list.files.size() == 1
|
||||
if (IsSingleItem(_list)
|
||||
&& _sendWay->value() == SendFilesWay::Photos) {
|
||||
_sendWay->setValue(SendFilesWay::Album);
|
||||
}
|
||||
_list.mergeToEnd(std::move(list));
|
||||
_list.mergeToEnd(std::move(list), cutToAlbumSize);
|
||||
|
||||
_compressConfirm = _compressConfirmInitial;
|
||||
refreshAlbumMediaCount();
|
||||
preparePreview();
|
||||
captionResized();
|
||||
refreshAllAfterAlbumChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1858,7 +2245,9 @@ void SendFilesBox::updateBoxSize() {
|
|||
}
|
||||
|
||||
void SendFilesBox::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
|
||||
if (e->matches(QKeySequence::Open) && !_addFileToAlbum->isHidden()) {
|
||||
openDialogToAddFileToAlbum();
|
||||
} else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
|
||||
const auto modifiers = e->modifiers();
|
||||
const auto ctrl = modifiers.testFlag(Qt::ControlModifier)
|
||||
|| modifiers.testFlag(Qt::MetaModifier);
|
||||
|
|
@ -1937,7 +2326,9 @@ void SendFilesBox::setInnerFocus() {
|
|||
void SendFilesBox::send(
|
||||
Api::SendOptions options,
|
||||
bool ctrlShiftEnter) {
|
||||
if (_sendType == Api::SendType::Scheduled && !options.scheduled) {
|
||||
if ((_sendType == Api::SendType::Scheduled
|
||||
|| _sendType == Api::SendType::ScheduledToUser)
|
||||
&& !options.scheduled) {
|
||||
return sendScheduled();
|
||||
}
|
||||
|
||||
|
|
@ -1982,9 +2373,12 @@ void SendFilesBox::sendSilent() {
|
|||
}
|
||||
|
||||
void SendFilesBox::sendScheduled() {
|
||||
const auto type = (_sendType == Api::SendType::ScheduledToUser)
|
||||
? SendMenuType::ScheduledToUser
|
||||
: _sendMenuType;
|
||||
const auto callback = [=](Api::SendOptions options) { send(options); };
|
||||
Ui::show(
|
||||
HistoryView::PrepareScheduleBox(this, _sendMenuType, callback),
|
||||
HistoryView::PrepareScheduleBox(this, type, callback),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,9 +120,15 @@ private:
|
|||
void updateControlsGeometry();
|
||||
void updateCaptionPlaceholder();
|
||||
|
||||
void addThumbButtonHandlers(not_null<Ui::ScrollArea*> wrap);
|
||||
|
||||
bool canAddFiles(not_null<const QMimeData*> data) const;
|
||||
bool canAddUrls(const QList<QUrl> &urls) const;
|
||||
bool addFiles(not_null<const QMimeData*> data);
|
||||
bool addFiles(Storage::PreparedList list);
|
||||
|
||||
void openDialogToAddFileToAlbum();
|
||||
void updateLeftButtonVisibility();
|
||||
void refreshAllAfterAlbumChanges();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const Api::SendType _sendType = Api::SendType();
|
||||
|
|
@ -163,6 +169,9 @@ private:
|
|||
int _albumVideosCount = 0;
|
||||
int _albumPhotosCount = 0;
|
||||
|
||||
int _lastScrollTop = 0;
|
||||
|
||||
QPointer<Ui::RoundButton> _send;
|
||||
QPointer<Ui::RoundButton> _addFileToAlbum;
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -413,7 +413,9 @@ void ShareBox::keyPressEvent(QKeyEvent *e) {
|
|||
|
||||
SendMenuType ShareBox::sendMenuType() const {
|
||||
const auto selected = _inner->selected();
|
||||
return (selected.size() == 1 && selected.front()->isSelf())
|
||||
return ranges::all_of(selected, HistoryView::CanScheduleUntilOnline)
|
||||
? SendMenuType::ScheduledToUser
|
||||
: (selected.size() == 1 && selected.front()->isSelf())
|
||||
? SendMenuType::Reminder
|
||||
: SendMenuType::Scheduled;
|
||||
}
|
||||
|
|
@ -424,7 +426,7 @@ void ShareBox::createButtons() {
|
|||
const auto send = addButton(tr::lng_share_confirm(), [=] {
|
||||
submit({});
|
||||
});
|
||||
SetupSendMenu(
|
||||
SetupSendMenuAndShortcuts(
|
||||
send,
|
||||
[=] { return sendMenuType(); },
|
||||
[=] { submitSilent(); },
|
||||
|
|
|
|||
|
|
@ -435,7 +435,9 @@ void EmojiKeywords::LangPack::applyDifference(
|
|||
LangPackData &&result) {
|
||||
applyData(std::move(result));
|
||||
});
|
||||
crl::async([=, callback = std::move(callback)]() mutable {
|
||||
crl::async([=,
|
||||
copy = std::move(copy),
|
||||
callback = std::move(callback)]() mutable {
|
||||
ApplyDifference(copy, keywords, version);
|
||||
WriteLocalCache(id, copy);
|
||||
crl::on_main([
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "base/qthelp_url.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "core/shortcuts.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
|
@ -33,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QTextBlock>
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -666,7 +668,7 @@ void MessageLinksParser::apply(
|
|||
_list = std::move(parsed);
|
||||
}
|
||||
|
||||
void SetupSendMenu(
|
||||
void SetupSendMenuAndShortcuts(
|
||||
not_null<Ui::RpWidget*> button,
|
||||
Fn<SendMenuType()> type,
|
||||
Fn<void()> silent,
|
||||
|
|
@ -688,9 +690,9 @@ void SetupSendMenu(
|
|||
}
|
||||
if (schedule && now != SendMenuType::SilentOnly) {
|
||||
(*menu)->addAction(
|
||||
(now == SendMenuType::Scheduled
|
||||
? tr::lng_schedule_message(tr::now)
|
||||
: tr::lng_reminder_message(tr::now)),
|
||||
(now == SendMenuType::Reminder
|
||||
? tr::lng_reminder_message(tr::now)
|
||||
: tr::lng_schedule_message(tr::now)),
|
||||
schedule);
|
||||
}
|
||||
(*menu)->popup(QCursor::pos());
|
||||
|
|
@ -702,4 +704,46 @@ void SetupSendMenu(
|
|||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
Shortcuts::Requests(
|
||||
) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
|
||||
using Command = Shortcuts::Command;
|
||||
|
||||
const auto now = type();
|
||||
if (now == SendMenuType::Disabled
|
||||
|| (!silent && now == SendMenuType::SilentOnly)) {
|
||||
return;
|
||||
}
|
||||
(silent
|
||||
&& (now != SendMenuType::Reminder)
|
||||
&& request->check(Command::SendSilentMessage)
|
||||
&& request->handle([=] {
|
||||
silent();
|
||||
return true;
|
||||
}))
|
||||
||
|
||||
(schedule
|
||||
&& (now != SendMenuType::SilentOnly)
|
||||
&& request->check(Command::ScheduleMessage)
|
||||
&& request->handle([=] {
|
||||
schedule();
|
||||
return true;
|
||||
}))
|
||||
||
|
||||
(request->check(Command::JustSendMessage) && request->handle([=] {
|
||||
const auto post = [&](QEvent::Type type) {
|
||||
QApplication::postEvent(
|
||||
button,
|
||||
new QMouseEvent(
|
||||
type,
|
||||
QPointF(0, 0),
|
||||
Qt::LeftButton,
|
||||
Qt::LeftButton,
|
||||
Qt::NoModifier));
|
||||
};
|
||||
post(QEvent::MouseButtonPress);
|
||||
post(QEvent::MouseButtonRelease);
|
||||
return true;
|
||||
}));
|
||||
}, button->lifetime());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,10 +99,11 @@ enum class SendMenuType {
|
|||
Disabled,
|
||||
SilentOnly,
|
||||
Scheduled,
|
||||
ScheduledToUser, // For "Send when online".
|
||||
Reminder,
|
||||
};
|
||||
|
||||
void SetupSendMenu(
|
||||
void SetupSendMenuAndShortcuts(
|
||||
not_null<Ui::RpWidget*> button,
|
||||
Fn<SendMenuType()> type,
|
||||
Fn<void()> silent,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ enum {
|
|||
|
||||
StickerMaxSize = 2048, // 2048x2048 is a max image size for sticker
|
||||
|
||||
MaxZoomLevel = 7, // x8
|
||||
ZoomToScreenLevel = 1024, // just constant
|
||||
|
||||
PreloadHeightsCount = 3, // when 3 screens to scroll left make a preload request
|
||||
|
|
|
|||
|
|
@ -295,6 +295,10 @@ QString AllFilesFilter() {
|
|||
#endif // Q_OS_WIN
|
||||
}
|
||||
|
||||
QString AlbumFilesFilter() {
|
||||
return qsl("Image and Video Files (*.png *.jpg *.mp4 *.jpeg)");
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
void InitLastPathDefault() {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ void GetFolder(
|
|||
Fn<void()> failed = Fn<void()>());
|
||||
|
||||
QString AllFilesFilter();
|
||||
QString AlbumFilesFilter();
|
||||
|
||||
namespace internal {
|
||||
|
||||
|
|
|
|||
|
|
@ -248,10 +248,8 @@ void Launcher::init() {
|
|||
|
||||
QApplication::setApplicationName(qsl("KotatogramDesktop"));
|
||||
|
||||
#ifdef TDESKTOP_LAUNCHER_FILENAME
|
||||
QApplication::setDesktopFileName(qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_FILENAME)));
|
||||
#elif defined(Q_OS_LINUX) && QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
|
||||
QApplication::setDesktopFileName(qsl("kotatogramdesktop.desktop"));
|
||||
#if defined(Q_OS_LINUX) && QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
|
||||
QApplication::setDesktopFileName(qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_BASENAME)) + ".desktop");
|
||||
#endif
|
||||
|
||||
#ifndef OS_MAC_OLD
|
||||
|
|
|
|||
|
|
@ -102,4 +102,9 @@ MimeType MimeTypeForData(const QByteArray &data) {
|
|||
return MimeType(QMimeDatabase().mimeTypeForData(data));
|
||||
}
|
||||
|
||||
bool IsMimeSticker(const QString &mime) {
|
||||
return mime == qsl("image/webp")
|
||||
|| mime == qsl("application/x-tgsticker");
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
|
|
|||
|
|
@ -39,4 +39,6 @@ MimeType MimeTypeForName(const QString &mime);
|
|||
MimeType MimeTypeForFile(const QFileInfo &file);
|
||||
MimeType MimeTypeForData(const QByteArray &data);
|
||||
|
||||
bool IsMimeSticker(const QString &mime);
|
||||
|
||||
} // namespace Core
|
||||
|
|
|
|||
|
|
@ -98,12 +98,7 @@ int Sandbox::start() {
|
|||
const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());
|
||||
char h[33] = { 0 };
|
||||
hashMd5Hex(d.constData(), d.size(), h);
|
||||
#ifndef OS_MAC_STORE
|
||||
_localServerName = psServerPrefix() + h + '-' + cGUIDStr();
|
||||
#else // OS_MAC_STORE
|
||||
h[4] = 0; // use only first 4 chars
|
||||
_localServerName = psServerPrefix() + h;
|
||||
#endif // OS_MAC_STORE
|
||||
_localServerName = Platform::SingleInstanceLocalServerName(h);
|
||||
|
||||
connect(
|
||||
&_localSocket,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,12 @@ const auto CommandByName = base::flat_map<QString, Command>{
|
|||
|
||||
{ qsl("show_archive") , Command::ShowArchive },
|
||||
|
||||
// Shortcuts that have no default values.
|
||||
{ qsl("message") , Command::JustSendMessage },
|
||||
{ qsl("message_silently") , Command::SendSilentMessage },
|
||||
{ qsl("message_scheduled"), Command::ScheduleMessage },
|
||||
//
|
||||
|
||||
{ qsl("save_draft") , Command::SaveDraft },
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ enum class Command {
|
|||
|
||||
ShowArchive,
|
||||
|
||||
JustSendMessage,
|
||||
SendSilentMessage,
|
||||
ScheduleMessage,
|
||||
|
||||
SupportReloadTemplates,
|
||||
SupportToggleMuted,
|
||||
SupportScrollToCurrent,
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#define TDESKTOP_ALPHA_VERSION (0ULL)
|
||||
#endif // TDESKTOP_ALLOW_CLOSED_ALPHA
|
||||
|
||||
constexpr auto AppVersion = 1009004;
|
||||
constexpr auto AppVersionStr = "1.9.4";
|
||||
constexpr auto AppVersion = 1009007;
|
||||
constexpr auto AppVersionStr = "1.9.7";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
constexpr auto AppKotatoVersion = 1001003;
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ void CloudThemes::applyUpdate(const MTPTheme &theme) {
|
|||
return;
|
||||
}
|
||||
applyFromDocument(cloud);
|
||||
}, [&](const MTPDthemeDocumentNotModified &data) {
|
||||
});
|
||||
scheduleReload();
|
||||
}
|
||||
|
|
@ -160,8 +159,6 @@ void CloudThemes::resolve(
|
|||
void CloudThemes::showPreview(const MTPTheme &data) {
|
||||
data.match([&](const MTPDtheme &data) {
|
||||
showPreview(CloudTheme::Parse(_session, data));
|
||||
}, [&](const MTPDthemeDocumentNotModified &data) {
|
||||
LOG(("API Error: Unexpected themeDocumentNotModified."));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -269,8 +266,6 @@ void CloudThemes::parseThemes(const QVector<MTPTheme> &list) {
|
|||
for (const auto &theme : list) {
|
||||
theme.match([&](const MTPDtheme &data) {
|
||||
_list.push_back(CloudTheme::Parse(_session, data));
|
||||
}, [&](const MTPDthemeDocumentNotModified &data) {
|
||||
LOG(("API Error: Unexpected themeDocumentNotModified."));
|
||||
});
|
||||
}
|
||||
checkCurrentTheme();
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ struct FileReferenceAccumulator {
|
|||
if (const auto document = data.vdocument()) {
|
||||
push(*document);
|
||||
}
|
||||
}, [&](const MTPDthemeDocumentNotModified &data) {
|
||||
});
|
||||
}
|
||||
void push(const MTPWebPageAttribute &data) {
|
||||
|
|
|
|||
|
|
@ -1302,6 +1302,9 @@ TextForMimeData MediaPoll::clipboardText() const {
|
|||
}
|
||||
|
||||
QString MediaPoll::errorTextForForward(not_null<PeerData*> peer) const {
|
||||
if (_poll->publicVotes() && peer->isChannel() && !peer->isMegagroup()) {
|
||||
return tr::lng_restricted_send_public_polls(tr::now);
|
||||
}
|
||||
return Data::RestrictionError(
|
||||
peer,
|
||||
ChatRestriction::f_send_polls
|
||||
|
|
|
|||
|
|
@ -754,6 +754,17 @@ int PeerData::slowmodeSecondsLeft() const {
|
|||
return 0;
|
||||
}
|
||||
|
||||
bool PeerData::canSendPolls() const {
|
||||
if (const auto user = asUser()) {
|
||||
return user->isBot();
|
||||
} else if (const auto chat = asChat()) {
|
||||
return chat->canSendPolls();
|
||||
} else if (const auto channel = asChannel()) {
|
||||
return channel->canSendPolls();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace Data {
|
||||
|
||||
std::vector<ChatRestrictions> ListOfRestrictions() {
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ public:
|
|||
[[nodiscard]] bool canRevokeFullHistory() const;
|
||||
[[nodiscard]] bool slowmodeApplied() const;
|
||||
[[nodiscard]] int slowmodeSecondsLeft() const;
|
||||
[[nodiscard]] bool canSendPolls() const;
|
||||
|
||||
[[nodiscard]] UserData *asUser();
|
||||
[[nodiscard]] const UserData *asUser() const;
|
||||
|
|
@ -366,7 +367,7 @@ private:
|
|||
|
||||
static constexpr auto kUnknownPhotoId = PhotoId(0xFFFFFFFFFFFFFFFFULL);
|
||||
|
||||
not_null<Data::Session*> _owner;
|
||||
const not_null<Data::Session*> _owner;
|
||||
|
||||
ImagePtr _userpic;
|
||||
PhotoId _userpicPhotoId = kUnknownPhotoId;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_poll.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace {
|
||||
|
|
@ -34,14 +36,19 @@ PollAnswer *AnswerByOption(
|
|||
|
||||
} // namespace
|
||||
|
||||
PollData::PollData(PollId id) : id(id) {
|
||||
PollData::PollData(not_null<Data::Session*> owner, PollId id)
|
||||
: id(id)
|
||||
, _owner(owner) {
|
||||
}
|
||||
|
||||
bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
Expects(poll.vid().v == id);
|
||||
|
||||
const auto newQuestion = qs(poll.vquestion());
|
||||
const auto newClosed = poll.is_closed();
|
||||
const auto newFlags = (poll.is_closed() ? Flag::Closed : Flag(0))
|
||||
| (poll.is_public_voters() ? Flag::PublicVotes : Flag(0))
|
||||
| (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0))
|
||||
| (poll.is_quiz() ? Flag::Quiz : Flag(0));
|
||||
auto newAnswers = ranges::view::all(
|
||||
poll.vanswers().v
|
||||
) | ranges::view::transform([](const MTPPollAnswer &data) {
|
||||
|
|
@ -56,14 +63,14 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
|||
) | ranges::to_vector;
|
||||
|
||||
const auto changed1 = (question != newQuestion)
|
||||
|| (closed != newClosed);
|
||||
|| (_flags != newFlags);
|
||||
const auto changed2 = (answers != newAnswers);
|
||||
if (!changed1 && !changed2) {
|
||||
return false;
|
||||
}
|
||||
if (changed1) {
|
||||
question = newQuestion;
|
||||
closed = newClosed;
|
||||
_flags = newFlags;
|
||||
}
|
||||
if (changed2) {
|
||||
std::swap(answers, newAnswers);
|
||||
|
|
@ -71,6 +78,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
|||
if (const auto current = answerByOption(old.option)) {
|
||||
current->votes = old.votes;
|
||||
current->chosen = old.chosen;
|
||||
current->correct = old.correct;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +100,29 @@ bool PollData::applyResults(const MTPPollResults &results) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (const auto recent = results.vrecent_voters()) {
|
||||
const auto recentChanged = !ranges::equal(
|
||||
recentVoters,
|
||||
recent->v,
|
||||
ranges::equal_to(),
|
||||
&UserData::id,
|
||||
&MTPint::v);
|
||||
if (recentChanged) {
|
||||
changed = true;
|
||||
recentVoters = ranges::view::all(
|
||||
recent->v
|
||||
) | ranges::view::transform([&](MTPint userId) {
|
||||
const auto user = _owner->user(userId.v);
|
||||
return (user->loadedStatus != PeerData::NotLoaded)
|
||||
? user.get()
|
||||
: nullptr;
|
||||
}) | ranges::view::filter([](UserData *user) {
|
||||
return user != nullptr;
|
||||
}) | ranges::view::transform([](UserData *user) {
|
||||
return not_null<UserData*>(user);
|
||||
}) | ranges::to_vector;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -104,11 +135,11 @@ bool PollData::applyResults(const MTPPollResults &results) {
|
|||
void PollData::checkResultsReload(not_null<HistoryItem*> item, crl::time now) {
|
||||
if (lastResultsUpdate && lastResultsUpdate + kShortPollTimeout > now) {
|
||||
return;
|
||||
} else if (closed) {
|
||||
} else if (closed()) {
|
||||
return;
|
||||
}
|
||||
lastResultsUpdate = now;
|
||||
Auth().api().reloadPollResults(item);
|
||||
_owner->session().api().reloadPollResults(item);
|
||||
}
|
||||
|
||||
PollAnswer *PollData::answerByOption(const QByteArray &option) {
|
||||
|
|
@ -137,18 +168,50 @@ bool PollData::applyResultToAnswers(
|
|||
answer->chosen = voters.is_chosen();
|
||||
changed = true;
|
||||
}
|
||||
if (answer->correct != voters.is_correct()) {
|
||||
answer->correct = voters.is_correct();
|
||||
changed = true;
|
||||
}
|
||||
} else if (const auto existing = answerByOption(option)) {
|
||||
answer->chosen = existing->chosen;
|
||||
answer->correct = existing->correct;
|
||||
}
|
||||
return changed;
|
||||
});
|
||||
}
|
||||
|
||||
void PollData::setFlags(Flags flags) {
|
||||
if (_flags != flags) {
|
||||
_flags = flags;
|
||||
++version;
|
||||
}
|
||||
}
|
||||
|
||||
PollData::Flags PollData::flags() const {
|
||||
return _flags;
|
||||
}
|
||||
|
||||
bool PollData::voted() const {
|
||||
return ranges::find(answers, true, &PollAnswer::chosen) != end(answers);
|
||||
}
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll) {
|
||||
bool PollData::closed() const {
|
||||
return (_flags & Flag::Closed);
|
||||
}
|
||||
|
||||
bool PollData::publicVotes() const {
|
||||
return (_flags & Flag::PublicVotes);
|
||||
}
|
||||
|
||||
bool PollData::multiChoice() const {
|
||||
return (_flags & Flag::MultiChoice);
|
||||
}
|
||||
|
||||
bool PollData::quiz() const {
|
||||
return (_flags & Flag::Quiz);
|
||||
}
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
||||
const auto convert = [](const PollAnswer &answer) {
|
||||
return MTP_pollAnswer(
|
||||
MTP_string(answer.text),
|
||||
|
|
@ -160,9 +223,14 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll) {
|
|||
poll->answers,
|
||||
ranges::back_inserter(answers),
|
||||
convert);
|
||||
using Flag = MTPDpoll::Flag;
|
||||
const auto flags = ((poll->closed() || close) ? Flag::f_closed : Flag(0))
|
||||
| (poll->multiChoice() ? Flag::f_multiple_choice : Flag(0))
|
||||
| (poll->publicVotes() ? Flag::f_public_voters : Flag(0))
|
||||
| (poll->quiz() ? Flag::f_quiz : Flag(0));
|
||||
return MTP_poll(
|
||||
MTP_long(poll->id),
|
||||
MTP_flags(MTPDpoll::Flag::f_closed),
|
||||
MTP_flags(flags),
|
||||
MTP_string(poll->question),
|
||||
MTP_vector<MTPPollAnswer>(answers));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
class Session;
|
||||
} // namespace Data
|
||||
|
||||
struct PollAnswer {
|
||||
QString text;
|
||||
QByteArray option;
|
||||
int votes = 0;
|
||||
bool chosen = false;
|
||||
bool correct = false;
|
||||
};
|
||||
|
||||
inline bool operator==(const PollAnswer &a, const PollAnswer &b) {
|
||||
|
|
@ -24,23 +29,39 @@ inline bool operator!=(const PollAnswer &a, const PollAnswer &b) {
|
|||
}
|
||||
|
||||
struct PollData {
|
||||
explicit PollData(PollId id);
|
||||
PollData(not_null<Data::Session*> owner, PollId id);
|
||||
|
||||
enum class Flag {
|
||||
Closed = 0x01,
|
||||
PublicVotes = 0x02,
|
||||
MultiChoice = 0x04,
|
||||
Quiz = 0x08,
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
using Flags = base::flags<Flag>;
|
||||
|
||||
bool applyChanges(const MTPDpoll &poll);
|
||||
bool applyResults(const MTPPollResults &results);
|
||||
void checkResultsReload(not_null<HistoryItem*> item, crl::time now);
|
||||
|
||||
PollAnswer *answerByOption(const QByteArray &option);
|
||||
const PollAnswer *answerByOption(const QByteArray &option) const;
|
||||
[[nodiscard]] PollAnswer *answerByOption(const QByteArray &option);
|
||||
[[nodiscard]] const PollAnswer *answerByOption(
|
||||
const QByteArray &option) const;
|
||||
|
||||
bool voted() const;
|
||||
void setFlags(Flags flags);
|
||||
[[nodiscard]] Flags flags() const;
|
||||
[[nodiscard]] bool voted() const;
|
||||
[[nodiscard]] bool closed() const;
|
||||
[[nodiscard]] bool publicVotes() const;
|
||||
[[nodiscard]] bool multiChoice() const;
|
||||
[[nodiscard]] bool quiz() const;
|
||||
|
||||
PollId id = 0;
|
||||
QString question;
|
||||
std::vector<PollAnswer> answers;
|
||||
std::vector<not_null<UserData*>> recentVoters;
|
||||
int totalVoters = 0;
|
||||
bool closed = false;
|
||||
QByteArray sendingVote;
|
||||
std::vector<QByteArray> sendingVotes;
|
||||
crl::time lastResultsUpdate = 0;
|
||||
|
||||
int version = 0;
|
||||
|
|
@ -52,6 +73,9 @@ private:
|
|||
const MTPPollAnswerVoters &result,
|
||||
bool isMinResults);
|
||||
|
||||
not_null<Data::Session*> _owner;
|
||||
Flags _flags = Flags();
|
||||
|
||||
};
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll);
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close = false);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ public:
|
|||
[[nodiscard]] rpl::producer<> updates(not_null<History*> history);
|
||||
[[nodiscard]] Data::MessagesSlice list(not_null<History*> history);
|
||||
|
||||
static constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
|
||||
|
||||
private:
|
||||
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
|
||||
struct List {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "mainwidget.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "core/application.h"
|
||||
#include "core/mime_type.h" // Core::IsMimeSticker
|
||||
#include "core/crash_reports.h" // CrashReports::SetAnnotation
|
||||
#include "ui/image/image.h"
|
||||
#include "ui/image/image_source.h" // Images::LocalFileSource
|
||||
|
|
@ -2390,8 +2391,7 @@ not_null<DocumentData*> Session::processDocument(
|
|||
case mtpc_document: {
|
||||
const auto &fields = data.c_document();
|
||||
const auto mime = qs(fields.vmime_type());
|
||||
const auto format = (mime == qstr("image/webp")
|
||||
|| mime == qstr("application/x-tgsticker"))
|
||||
const auto format = Core::IsMimeSticker(mime)
|
||||
? "WEBP"
|
||||
: "JPG";
|
||||
return document(
|
||||
|
|
@ -2895,7 +2895,7 @@ void Session::gameApplyFields(
|
|||
not_null<PollData*> Session::poll(PollId id) {
|
||||
auto i = _polls.find(id);
|
||||
if (i == _polls.cend()) {
|
||||
i = _polls.emplace(id, std::make_unique<PollData>(id)).first;
|
||||
i = _polls.emplace(id, std::make_unique<PollData>(this, id)).first;
|
||||
}
|
||||
return i->second.get();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "core/application.h"
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "data/data_peer.h"
|
||||
|
|
@ -115,6 +116,19 @@ void activateBotCommand(
|
|||
}));
|
||||
} break;
|
||||
|
||||
case ButtonType::RequestPoll: {
|
||||
hideSingleUseKeyboard(msg);
|
||||
auto chosen = PollData::Flags();
|
||||
auto disabled = PollData::Flags();
|
||||
if (!button->data.isEmpty()) {
|
||||
disabled |= PollData::Flag::Quiz;
|
||||
if (button->data[0]) {
|
||||
chosen |= PollData::Flag::Quiz;
|
||||
}
|
||||
}
|
||||
Window::PeerMenuCreatePoll(msg->history()->peer, chosen, disabled);
|
||||
} break;
|
||||
|
||||
case ButtonType::SwitchInlineSame:
|
||||
case ButtonType::SwitchInline: {
|
||||
if (auto m = App::main()) {
|
||||
|
|
|
|||
|
|
@ -577,6 +577,11 @@ bool InnerWidget::elementIntersectsRange(
|
|||
void InnerWidget::elementStartStickerLoop(not_null<const Element*> view) {
|
||||
}
|
||||
|
||||
void InnerWidget::elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) {
|
||||
}
|
||||
|
||||
void InnerWidget::saveState(not_null<SectionMemento*> memento) {
|
||||
memento->setFilter(std::move(_filter));
|
||||
memento->setAdmins(std::move(_admins));
|
||||
|
|
|
|||
|
|
@ -101,6 +101,9 @@ public:
|
|||
int till) override;
|
||||
void elementStartStickerLoop(
|
||||
not_null<const HistoryView::Element*> view) override;
|
||||
void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) override;
|
||||
|
||||
~InnerWidget();
|
||||
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@ historyReceivedIcon: icon {{ "history_received", historyOutIconFg, point(2px, 4p
|
|||
historyReceivedSelectedIcon: icon {{ "history_received", historyOutIconFgSelected, point(2px, 4px) }};
|
||||
historyReceivedInvertedIcon: icon {{ "history_received", historyIconFgInverted, point(2px, 4px) }};
|
||||
|
||||
historyScheduledUntilOnlineStateSpace: 17px;
|
||||
|
||||
historyViewsSpace: 11px;
|
||||
historyViewsWidth: 20px;
|
||||
historyViewsTop: -15px;
|
||||
|
|
@ -539,9 +541,9 @@ historyPollQuestionStyle: TextStyle(defaultTextStyle) {
|
|||
}
|
||||
historyPollAnswerStyle: defaultTextStyle;
|
||||
historyPollQuestionTop: 7px;
|
||||
historyPollSubtitleSkip: 2px;
|
||||
historyPollSubtitleSkip: 4px;
|
||||
historyPollAnswerPadding: margins(31px, 10px, 0px, 10px);
|
||||
historyPollAnswersSkip: 3px;
|
||||
historyPollAnswersSkip: 2px;
|
||||
historyPollPercentFont: semiboldFont;
|
||||
historyPollPercentSkip: 6px;
|
||||
historyPollPercentTop: 0px;
|
||||
|
|
@ -575,6 +577,17 @@ historyPollRippleOut: RippleAnimation(defaultRippleAnimation) {
|
|||
color: msgWaveformOutInactive;
|
||||
}
|
||||
historyPollRippleOpacity: 0.3;
|
||||
historyPollRecentVotersSkip: 4px;
|
||||
historyPollRecentVoterSize: 18px;
|
||||
historyPollRecentVoterSkip: 13px;
|
||||
historyPollBottomButtonSkip: 15px;
|
||||
historyPollBottomButtonTop: 4px;
|
||||
historyPollChoiceRight: icon {{ "poll_choice_right", activeButtonFg }};
|
||||
historyPollChoiceWrong: icon {{ "poll_choice_wrong", activeButtonFg }};
|
||||
historyPollOutChosen: icon {{ "poll_select_check", historyFileOutIconFg }};
|
||||
historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgSelected }};
|
||||
historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }};
|
||||
historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }};
|
||||
|
||||
boxAttachEmoji: IconButton(historyAttachEmoji) {
|
||||
width: 30px;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/inactive_press.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "boxes/report_box.h"
|
||||
#include "boxes/sticker_set_box.h"
|
||||
|
|
@ -1719,8 +1720,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
|||
}
|
||||
if (const auto media = item->media()) {
|
||||
if (const auto poll = media->poll()) {
|
||||
if (!poll->closed) {
|
||||
if (poll->voted()) {
|
||||
if (!poll->closed()) {
|
||||
if (poll->voted() && !poll->quiz()) {
|
||||
_menu->addAction(tr::lng_polls_retract(tr::now), [=] {
|
||||
session().api().sendPollVotes(itemId, {});
|
||||
});
|
||||
|
|
@ -2418,6 +2419,12 @@ void HistoryInner::elementStartStickerLoop(
|
|||
_animatedStickersPlayed.emplace(view->data());
|
||||
}
|
||||
|
||||
void HistoryInner::elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) {
|
||||
_controller->showPollResults(poll, context);
|
||||
}
|
||||
|
||||
auto HistoryInner::getSelectionState() const
|
||||
-> HistoryView::TopBarWidget::SelectedState {
|
||||
auto result = HistoryView::TopBarWidget::SelectedState {};
|
||||
|
|
@ -3278,6 +3285,13 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
|
|||
Instance->elementStartStickerLoop(view);
|
||||
}
|
||||
}
|
||||
void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) override {
|
||||
if (Instance) {
|
||||
Instance->elementShowPollResults(poll, context);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ public:
|
|||
int from,
|
||||
int till) const;
|
||||
void elementStartStickerLoop(not_null<const Element*> view);
|
||||
void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context);
|
||||
|
||||
void updateBotInfo(bool recount = true);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "window/window_session_controller.h"
|
||||
#include "core/crash_reports.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_messages.h"
|
||||
#include "data/data_media_types.h"
|
||||
|
|
@ -862,6 +863,21 @@ QDateTime ItemDateTime(not_null<const HistoryItem*> item) {
|
|||
return base::unixtime::parse(item->date());
|
||||
}
|
||||
|
||||
QString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline) {
|
||||
const auto dateText = langDayOfMonthFull(ItemDateTime(item).date());
|
||||
return !item->isScheduled()
|
||||
? dateText
|
||||
: isUntilOnline
|
||||
? tr::lng_scheduled_date_until_online(tr::now)
|
||||
: tr::lng_scheduled_date(tr::now, lt_date, dateText);
|
||||
}
|
||||
|
||||
bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {
|
||||
return item->isScheduled()
|
||||
&& (item->date() ==
|
||||
Data::ScheduledMessages::kScheduledUntilOnlineTimestamp);
|
||||
}
|
||||
|
||||
ClickHandlerPtr goToMessageClickHandler(
|
||||
not_null<HistoryItem*> item,
|
||||
FullMsgId returnToId) {
|
||||
|
|
|
|||
|
|
@ -374,6 +374,8 @@ private:
|
|||
};
|
||||
|
||||
QDateTime ItemDateTime(not_null<const HistoryItem*> item);
|
||||
QString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline);
|
||||
bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item);
|
||||
|
||||
ClickHandlerPtr goToMessageClickHandler(
|
||||
not_null<PeerData*> peer,
|
||||
|
|
|
|||
|
|
@ -899,6 +899,21 @@ void HistoryMessageReplyMarkup::createFromButtonRows(
|
|||
}, [&](const MTPDinputKeyboardButtonUrlAuth &data) {
|
||||
LOG(("API Error: inputKeyboardButtonUrlAuth received."));
|
||||
// Should not get those for the users.
|
||||
}, [&](const MTPDkeyboardButtonRequestPoll &data) {
|
||||
const auto quiz = [&] {
|
||||
if (!data.vquiz()) {
|
||||
return QByteArray();
|
||||
}
|
||||
return data.vquiz()->match([&](const MTPDboolTrue&) {
|
||||
return QByteArray(1, 1);
|
||||
}, [&](const MTPDboolFalse&) {
|
||||
return QByteArray(1, 0);
|
||||
});
|
||||
}();
|
||||
row.emplace_back(
|
||||
Type::RequestPoll,
|
||||
qs(data.vtext()),
|
||||
quiz);
|
||||
});
|
||||
}
|
||||
if (!row.empty()) {
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ struct HistoryMessageMarkupButton {
|
|||
Callback,
|
||||
RequestPhone,
|
||||
RequestLocation,
|
||||
RequestPoll,
|
||||
SwitchInline,
|
||||
SwitchInlineSame,
|
||||
Game,
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ HistoryWidget::HistoryWidget(
|
|||
_fieldBarCancel->addClickHandler([=] { cancelFieldAreaState(); });
|
||||
_send->addClickHandler([=] { sendButtonClicked(); });
|
||||
|
||||
SetupSendMenu(
|
||||
SetupSendMenuAndShortcuts(
|
||||
_send,
|
||||
[=] { return sendButtonMenuType(); },
|
||||
[=] { sendSilent(); },
|
||||
|
|
@ -3066,6 +3066,8 @@ SendMenuType HistoryWidget::sendMenuType() const {
|
|||
? SendMenuType::Disabled
|
||||
: _peer->isSelf()
|
||||
? SendMenuType::Reminder
|
||||
: HistoryView::CanScheduleUntilOnline(_peer)
|
||||
? SendMenuType::ScheduledToUser
|
||||
: SendMenuType::Scheduled;
|
||||
}
|
||||
|
||||
|
|
@ -6900,7 +6902,13 @@ void HistoryWidget::drawPinnedBar(Painter &p) {
|
|||
}
|
||||
p.setPen(st::historyReplyNameFg);
|
||||
p.setFont(st::msgServiceNameFont);
|
||||
p.drawText(left, top + st::msgServiceNameFont->ascent, (media && media->poll()) ? tr::lng_pinned_poll(tr::now) : tr::lng_pinned_message(tr::now));
|
||||
const auto poll = media ? media->poll() : nullptr;
|
||||
const auto pinnedHeader = !poll
|
||||
? tr::lng_pinned_message(tr::now)
|
||||
: poll->quiz()
|
||||
? tr::lng_pinned_quiz(tr::now)
|
||||
: tr::lng_pinned_poll(tr::now);
|
||||
p.drawText(left, top + st::msgServiceNameFont->ascent, pinnedHeader);
|
||||
|
||||
p.setPen(st::historyComposeAreaFg);
|
||||
p.setTextPalette(st::historyComposeAreaPalette);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ void SimpleElementDelegate::elementStartStickerLoop(
|
|||
not_null<const Element*> view) {
|
||||
}
|
||||
|
||||
void SimpleElementDelegate::elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) {
|
||||
}
|
||||
|
||||
TextSelection UnshiftItemSelection(
|
||||
TextSelection selection,
|
||||
uint16 byLength) {
|
||||
|
|
@ -180,8 +185,8 @@ void UnreadBar::paint(Painter &p, int y, int w) const {
|
|||
}
|
||||
|
||||
|
||||
void DateBadge::init(const QDateTime &date) {
|
||||
text = langDayOfMonthFull(date.date());
|
||||
void DateBadge::init(const QString &date) {
|
||||
text = date;
|
||||
width = st::msgServiceFont->width(text);
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +207,8 @@ Element::Element(
|
|||
not_null<HistoryItem*> data)
|
||||
: _delegate(delegate)
|
||||
, _data(data)
|
||||
, _dateTime(ItemDateTime(data))
|
||||
, _isScheduledUntilOnline(IsItemScheduledUntilOnline(data))
|
||||
, _dateTime(_isScheduledUntilOnline ? QDateTime() : ItemDateTime(data))
|
||||
, _context(delegate->elementContext()) {
|
||||
history()->owner().registerItemView(this);
|
||||
refreshMedia();
|
||||
|
|
@ -504,7 +510,7 @@ void Element::setDisplayDate(bool displayDate) {
|
|||
const auto item = data();
|
||||
if (displayDate && !Has<DateBadge>()) {
|
||||
AddComponents(DateBadge::Bit());
|
||||
Get<DateBadge>()->init(dateTime());
|
||||
Get<DateBadge>()->init(ItemDateText(item, _isScheduledUntilOnline));
|
||||
setPendingResize();
|
||||
} else if (!displayDate && Has<DateBadge>()) {
|
||||
RemoveComponents(DateBadge::Bit());
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ public:
|
|||
int from,
|
||||
int till) = 0;
|
||||
virtual void elementStartStickerLoop(not_null<const Element*> view) = 0;
|
||||
virtual void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) = 0;
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -71,6 +74,9 @@ public:
|
|||
int from,
|
||||
int till) override;
|
||||
void elementStartStickerLoop(not_null<const Element*> view) override;
|
||||
void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) override;
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -116,7 +122,7 @@ struct UnreadBar : public RuntimeComponent<UnreadBar, Element> {
|
|||
// Any HistoryView::Element can have this Component for
|
||||
// displaying the day mark above the message.
|
||||
struct DateBadge : public RuntimeComponent<DateBadge, Element> {
|
||||
void init(const QDateTime &date);
|
||||
void init(const QString &date);
|
||||
|
||||
int height() const;
|
||||
void paint(Painter &p, int y, int w) const;
|
||||
|
|
@ -305,6 +311,7 @@ private:
|
|||
const not_null<ElementDelegate*> _delegate;
|
||||
const not_null<HistoryItem*> _data;
|
||||
std::unique_ptr<Media> _media;
|
||||
bool _isScheduledUntilOnline = false;
|
||||
const QDateTime _dateTime;
|
||||
|
||||
int _y = 0;
|
||||
|
|
|
|||
|
|
@ -1153,6 +1153,11 @@ bool ListWidget::elementIntersectsRange(
|
|||
void ListWidget::elementStartStickerLoop(not_null<const Element*> view) {
|
||||
}
|
||||
|
||||
void ListWidget::elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) {
|
||||
}
|
||||
|
||||
void ListWidget::saveState(not_null<ListMemento*> memento) {
|
||||
memento->setAroundPosition(_aroundPosition);
|
||||
auto state = countScrollState();
|
||||
|
|
@ -1386,7 +1391,7 @@ void ListWidget::paintEvent(QPaintEvent *e) {
|
|||
} else {
|
||||
ServiceMessagePainter::paintDate(
|
||||
p,
|
||||
view->dateTime(),
|
||||
ItemDateText(view->data(), IsItemScheduledUntilOnline(view->data())),
|
||||
dateY,
|
||||
width);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,6 +197,9 @@ public:
|
|||
int from,
|
||||
int till) override;
|
||||
void elementStartStickerLoop(not_null<const Element*> view) override;
|
||||
void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) override;
|
||||
|
||||
~ListWidget();
|
||||
|
||||
|
|
|
|||
|
|
@ -1269,7 +1269,12 @@ int Message::infoWidth() const {
|
|||
result += st::historySendStateSpace;
|
||||
}
|
||||
}
|
||||
if (hasOutLayout()) {
|
||||
|
||||
// When message is scheduled until online, time is not displayed,
|
||||
// so message should have less space.
|
||||
if (!item->_timeWidth) {
|
||||
result += st::historyScheduledUntilOnlineStateSpace;
|
||||
} else if (hasOutLayout()) {
|
||||
result += st::historySendStateSpace;
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -8,17 +8,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/view/history_view_schedule_box.h"
|
||||
|
||||
#include "api/api_common.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/calendar_box.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_history.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
|
|
@ -535,20 +542,41 @@ void TimeInput::startBorderAnimation() {
|
|||
}
|
||||
}
|
||||
|
||||
void FillSendUntilOnlineMenu(
|
||||
not_null<Ui::IconButton*> button,
|
||||
Fn<void()> callback) {
|
||||
const auto menu = std::make_shared<base::unique_qptr<Ui::PopupMenu>>();
|
||||
button->setClickedCallback([=] {
|
||||
*menu = base::make_unique_q<Ui::PopupMenu>(button);
|
||||
(*menu)->addAction(
|
||||
tr::lng_scheduled_send_until_online(tr::now),
|
||||
std::move(callback));
|
||||
(*menu)->popup(QCursor::pos());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TimeId DefaultScheduleTime() {
|
||||
return base::unixtime::now() + 600;
|
||||
}
|
||||
|
||||
bool CanScheduleUntilOnline(not_null<PeerData*> peer) {
|
||||
return !peer->isSelf()
|
||||
&& peer->isUser()
|
||||
&& !peer->asUser()->isBot()
|
||||
&& (peer->asUser()->onlineTill > 0);
|
||||
}
|
||||
|
||||
void ScheduleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
SendMenuType type,
|
||||
FnMut<void(Api::SendOptions)> done,
|
||||
TimeId time) {
|
||||
box->setTitle((type == SendMenuType::Scheduled)
|
||||
? tr::lng_schedule_title()
|
||||
: tr::lng_remind_title());
|
||||
box->setTitle((type == SendMenuType::Reminder)
|
||||
? tr::lng_remind_title()
|
||||
: tr::lng_schedule_title());
|
||||
box->setWidth(st::boxWideWidth);
|
||||
|
||||
const auto date = Ui::CreateChild<rpl::variable<QDate>>(
|
||||
|
|
@ -637,10 +665,15 @@ void ScheduleBox(
|
|||
}
|
||||
return result;
|
||||
};
|
||||
const auto save = [=](bool silent) {
|
||||
const auto save = [=](bool silent, bool untilOnline = false) {
|
||||
// Pro tip: Hold Ctrl key to send a silent scheduled message!
|
||||
auto ctrl =
|
||||
(QGuiApplication::keyboardModifiers() == Qt::ControlModifier);
|
||||
auto result = Api::SendOptions();
|
||||
result.silent = silent;
|
||||
result.scheduled = collect();
|
||||
result.silent = silent || ctrl;
|
||||
result.scheduled = untilOnline
|
||||
? Data::ScheduledMessages::kScheduledUntilOnlineTimestamp
|
||||
: collect();
|
||||
if (!result.scheduled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -658,12 +691,20 @@ void ScheduleBox(
|
|||
const auto submit = box->addButton(tr::lng_settings_save(), [=] {
|
||||
save(false);
|
||||
});
|
||||
SetupSendMenu(
|
||||
SetupSendMenuAndShortcuts(
|
||||
submit.data(),
|
||||
[=] { return SendMenuType::SilentOnly; },
|
||||
[=] { save(true); },
|
||||
nullptr);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
if (type == SendMenuType::ScheduledToUser) {
|
||||
const auto sendUntilOnline = box->addTopButton(st::infoTopBarMenu);
|
||||
FillSendUntilOnlineMenu(
|
||||
sendUntilOnline.data(),
|
||||
[=] { save(false, true); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ enum class SendMenuType;
|
|||
namespace HistoryView {
|
||||
|
||||
[[nodiscard]] TimeId DefaultScheduleTime();
|
||||
[[nodiscard]] bool CanScheduleUntilOnline(not_null<PeerData*> peer);
|
||||
void ScheduleBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
SendMenuType type,
|
||||
|
|
|
|||
|
|
@ -240,7 +240,9 @@ bool ScheduledWidget::confirmSendingFiles(
|
|||
text,
|
||||
boxCompressConfirm,
|
||||
_history->peer->slowmodeApplied() ? SendLimit::One : SendLimit::Many,
|
||||
Api::SendType::Scheduled,
|
||||
CanScheduleUntilOnline(_history->peer)
|
||||
? Api::SendType::ScheduledToUser
|
||||
: Api::SendType::Scheduled,
|
||||
SendMenuType::Disabled);
|
||||
//_field->setTextWithTags({});
|
||||
|
||||
|
|
@ -545,6 +547,8 @@ void ScheduledWidget::sendInlineResult(
|
|||
SendMenuType ScheduledWidget::sendMenuType() const {
|
||||
return _history->peer->isSelf()
|
||||
? SendMenuType::Reminder
|
||||
: HistoryView::CanScheduleUntilOnline(_history->peer)
|
||||
? SendMenuType::ScheduledToUser
|
||||
: SendMenuType::Scheduled;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,10 @@ void ServiceMessagePainter::paintDate(Painter &p, const QDateTime &date, int y,
|
|||
paintPreparedDate(p, dateText, dateTextWidth, y, w);
|
||||
}
|
||||
|
||||
void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int y, int w) {
|
||||
paintPreparedDate(p, dateText, st::msgServiceFont->width(dateText), y, w);
|
||||
}
|
||||
|
||||
void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w) {
|
||||
paintPreparedDate(p, dateText, dateTextWidth, y, w);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ struct PaintContext {
|
|||
class ServiceMessagePainter {
|
||||
public:
|
||||
static void paintDate(Painter &p, const QDateTime &date, int y, int w);
|
||||
static void paintDate(Painter &p, const QString &dateText, int y, int w);
|
||||
static void paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w);
|
||||
|
||||
static void paintBubble(Painter &p, int x, int y, int w, int h);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,35 @@ namespace {
|
|||
|
||||
constexpr auto kAudioVoiceMsgUpdateView = crl::time(100);
|
||||
|
||||
[[nodiscard]] QString CleanTagSymbols(const QString &value) {
|
||||
auto result = QString();
|
||||
const auto begin = value.begin(), end = value.end();
|
||||
auto from = begin;
|
||||
for (auto ch = begin; ch != end; ++ch) {
|
||||
if (ch->isHighSurrogate()
|
||||
&& (ch + 1) != end
|
||||
&& (ch + 1)->isLowSurrogate()
|
||||
&& QChar::surrogateToUcs4(
|
||||
ch->unicode(),
|
||||
(ch + 1)->unicode()) >= 0xe0000) {
|
||||
if (ch > from) {
|
||||
if (result.isEmpty()) {
|
||||
result.reserve(value.size());
|
||||
}
|
||||
result.append(from, ch - from);
|
||||
}
|
||||
++ch;
|
||||
from = ch + 1;
|
||||
}
|
||||
}
|
||||
if (from == begin) {
|
||||
return value;
|
||||
} else if (end > from) {
|
||||
result.append(from, end - from);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Document::Document(
|
||||
|
|
@ -104,7 +133,8 @@ void Document::createComponents(bool caption) {
|
|||
}
|
||||
|
||||
void Document::fillNamedFromData(HistoryDocumentNamed *named) {
|
||||
const auto nameString = named->_name = _data->composeNameString();
|
||||
const auto nameString = named->_name = CleanTagSymbols(
|
||||
_data->composeNameString());
|
||||
named->_namew = st::semiboldFont->width(nameString);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ bool Gif::downloadInCorner() const {
|
|||
&& (_data->loading() || !autoplayEnabled())
|
||||
&& _data->canBeStreamed()
|
||||
&& !_data->inappPlaybackFailed()
|
||||
&& IsServerMsgId(_parent->data()->id);
|
||||
&& !_parent->data()->isSending();
|
||||
}
|
||||
|
||||
bool Gif::autoplayEnabled() const {
|
||||
|
|
@ -431,19 +431,22 @@ void Gif::draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms
|
|||
} else {
|
||||
p.drawPixmap(rthumb.topLeft(), normal->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners));
|
||||
}
|
||||
} else if (const auto blurred = _data->thumbnailInline()) {
|
||||
p.drawPixmap(rthumb.topLeft(), blurred->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners));
|
||||
} else if (!isRound) {
|
||||
const auto roundTop = (roundCorners & RectPart::TopLeft);
|
||||
const auto roundBottom = (roundCorners & RectPart::BottomLeft);
|
||||
const auto margin = inWebPage
|
||||
? st::buttonRadius
|
||||
: st::historyMessageRadius;
|
||||
const auto parts = roundCorners
|
||||
| RectPart::NoTopBottom
|
||||
| (roundTop ? RectPart::Top : RectPart::None)
|
||||
| (roundBottom ? RectPart::Bottom : RectPart::None);
|
||||
App::roundRect(p, rthumb.marginsAdded({ 0, roundTop ? 0 : margin, 0, roundBottom ? 0 : margin }), st::imageBg, roundRadius, parts);
|
||||
} else {
|
||||
_data->loadThumbnail(_realParent->fullId());
|
||||
if (const auto blurred = _data->thumbnailInline()) {
|
||||
p.drawPixmap(rthumb.topLeft(), blurred->pixBlurredSingle(_realParent->fullId(), _thumbw, _thumbh, usew, painth, roundRadius, roundCorners));
|
||||
} else if (!isRound) {
|
||||
const auto roundTop = (roundCorners & RectPart::TopLeft);
|
||||
const auto roundBottom = (roundCorners & RectPart::BottomLeft);
|
||||
const auto margin = inWebPage
|
||||
? st::buttonRadius
|
||||
: st::historyMessageRadius;
|
||||
const auto parts = roundCorners
|
||||
| RectPart::NoTopBottom
|
||||
| (roundTop ? RectPart::Top : RectPart::None)
|
||||
| (roundBottom ? RectPart::Bottom : RectPart::None);
|
||||
App::roundRect(p, rthumb.marginsAdded({ 0, roundTop ? 0 : margin, 0, roundBottom ? 0 : margin }), st::imageBg, roundRadius, parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -807,7 +810,7 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
|
|||
if (QRect(usex + paintx, painty, usew, painth).contains(point)) {
|
||||
result.link = _data->uploading()
|
||||
? _cancell
|
||||
: !IsServerMsgId(_realParent->id)
|
||||
: _realParent->isSending()
|
||||
? nullptr
|
||||
: (_data->loaded() || _data->canBePlayed())
|
||||
? _openl
|
||||
|
|
@ -1098,7 +1101,7 @@ TextState Gif::getStateGrouped(
|
|||
}
|
||||
return TextState(_parent, _data->uploading()
|
||||
? _cancell
|
||||
: !IsServerMsgId(_realParent->id)
|
||||
: _realParent->isSending()
|
||||
? nullptr
|
||||
: (_data->loaded() || _data->canBePlayed())
|
||||
? _openl
|
||||
|
|
@ -1279,6 +1282,7 @@ void Gif::parentTextUpdated() {
|
|||
}
|
||||
|
||||
void Gif::refreshParentId(not_null<HistoryItem*> realParent) {
|
||||
File::refreshParentId(realParent);
|
||||
if (_parent->media() == this) {
|
||||
refreshCaption();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "ui/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "layout.h"
|
||||
#include "main/main_session.h"
|
||||
|
|
@ -29,6 +31,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
constexpr auto kShowRecentVotersCount = 3;
|
||||
|
||||
struct PercentCounterItem {
|
||||
int index = 0;
|
||||
int percent = 0;
|
||||
|
|
@ -112,6 +116,8 @@ struct Poll::AnswerAnimation {
|
|||
anim::value percent;
|
||||
anim::value filling;
|
||||
anim::value opacity;
|
||||
bool chosen = false;
|
||||
bool correct = false;
|
||||
};
|
||||
|
||||
struct Poll::AnswersAnimation {
|
||||
|
|
@ -132,7 +138,7 @@ struct Poll::SendingAnimation {
|
|||
struct Poll::Answer {
|
||||
Answer();
|
||||
|
||||
void fillText(const PollAnswer &original);
|
||||
void fillData(not_null<PollData*> poll, const PollAnswer &original);
|
||||
|
||||
Ui::Text::String text;
|
||||
QByteArray option;
|
||||
|
|
@ -142,7 +148,10 @@ struct Poll::Answer {
|
|||
float64 filling = 0.;
|
||||
QString votesPercentString;
|
||||
bool chosen = false;
|
||||
bool correct = false;
|
||||
bool selected = false;
|
||||
ClickHandlerPtr handler;
|
||||
Ui::Animations::Simple selectedAnimation;
|
||||
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
};
|
||||
|
||||
|
|
@ -159,7 +168,11 @@ Poll::SendingAnimation::SendingAnimation(
|
|||
Poll::Answer::Answer() : text(st::msgMinWidth / 2) {
|
||||
}
|
||||
|
||||
void Poll::Answer::fillText(const PollAnswer &original) {
|
||||
void Poll::Answer::fillData(
|
||||
not_null<PollData*> poll,
|
||||
const PollAnswer &original) {
|
||||
chosen = original.chosen;
|
||||
correct = poll->quiz() ? original.correct : chosen;
|
||||
if (!text.isEmpty() && text.toString() == original.text) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -174,7 +187,15 @@ Poll::Poll(
|
|||
not_null<PollData*> poll)
|
||||
: Media(parent)
|
||||
, _poll(poll)
|
||||
, _question(st::msgMinWidth / 2) {
|
||||
, _question(st::msgMinWidth / 2)
|
||||
, _showResultsLink(
|
||||
std::make_shared<LambdaClickHandler>(crl::guard(
|
||||
this,
|
||||
[=] { showResults(); })))
|
||||
, _sendVotesLink(
|
||||
std::make_shared<LambdaClickHandler>(crl::guard(
|
||||
this,
|
||||
[=] { sendMultiOptions(); }))) {
|
||||
history()->owner().registerPollView(_poll, _parent);
|
||||
}
|
||||
|
||||
|
|
@ -202,13 +223,17 @@ QSize Poll::countOptimalSize() {
|
|||
+ st::historyPollAnswerPadding.bottom();
|
||||
}), 0);
|
||||
|
||||
const auto bottomButtonHeight = inlineFooter()
|
||||
? 0
|
||||
: st::historyPollBottomButtonSkip;
|
||||
auto minHeight = st::historyPollQuestionTop
|
||||
+ _question.minHeight()
|
||||
+ st::historyPollSubtitleSkip
|
||||
+ st::msgDateFont->height
|
||||
+ st::historyPollAnswersSkip
|
||||
+ answersHeight
|
||||
+ st::msgPadding.bottom()
|
||||
+ st::historyPollTotalVotesSkip
|
||||
+ bottomButtonHeight
|
||||
+ st::msgDateFont->height
|
||||
+ st::msgPadding.bottom();
|
||||
if (!isBubbleTop()) {
|
||||
|
|
@ -218,13 +243,28 @@ QSize Poll::countOptimalSize() {
|
|||
}
|
||||
|
||||
bool Poll::showVotes() const {
|
||||
return _voted || _closed;
|
||||
return _voted || (_flags & PollData::Flag::Closed);
|
||||
}
|
||||
|
||||
bool Poll::canVote() const {
|
||||
return !showVotes() && IsServerMsgId(_parent->data()->id);
|
||||
}
|
||||
|
||||
bool Poll::canSendVotes() const {
|
||||
return canVote() && _hasSelected;
|
||||
}
|
||||
|
||||
bool Poll::showVotersCount() const {
|
||||
return showVotes()
|
||||
? (!_totalVotes || !(_flags & PollData::Flag::PublicVotes))
|
||||
: !(_flags & PollData::Flag::MultiChoice);
|
||||
}
|
||||
|
||||
bool Poll::inlineFooter() const {
|
||||
return !(_flags
|
||||
& (PollData::Flag::PublicVotes | PollData::Flag::MultiChoice));
|
||||
}
|
||||
|
||||
int Poll::countAnswerTop(
|
||||
const Answer &answer,
|
||||
int innerWidth) const {
|
||||
|
|
@ -278,6 +318,9 @@ QSize Poll::countCurrentSize(int newWidth) {
|
|||
return countAnswerHeight(answer, innerWidth);
|
||||
}), 0);
|
||||
|
||||
const auto bottomButtonHeight = inlineFooter()
|
||||
? 0
|
||||
: st::historyPollBottomButtonSkip;
|
||||
auto newHeight = st::historyPollQuestionTop
|
||||
+ _question.countHeight(innerWidth)
|
||||
+ st::historyPollSubtitleSkip
|
||||
|
|
@ -285,6 +328,7 @@ QSize Poll::countCurrentSize(int newWidth) {
|
|||
+ st::historyPollAnswersSkip
|
||||
+ answersHeight
|
||||
+ st::historyPollTotalVotesSkip
|
||||
+ bottomButtonHeight
|
||||
+ st::msgDateFont->height
|
||||
+ st::msgPadding.bottom();
|
||||
if (!isBubbleTop()) {
|
||||
|
|
@ -309,13 +353,22 @@ void Poll::updateTexts() {
|
|||
_poll->question,
|
||||
options);
|
||||
}
|
||||
if (_closed != _poll->closed || _subtitle.isEmpty()) {
|
||||
_closed = _poll->closed;
|
||||
if (_flags != _poll->flags() || _subtitle.isEmpty()) {
|
||||
using Flag = PollData::Flag;
|
||||
_flags = _poll->flags();
|
||||
_subtitle.setText(
|
||||
st::msgDateTextStyle,
|
||||
_closed ? tr::lng_polls_closed(tr::now) : tr::lng_polls_anonymous(tr::now));
|
||||
((_flags & Flag::Closed)
|
||||
? tr::lng_polls_closed(tr::now)
|
||||
: (_flags & Flag::Quiz)
|
||||
? ((_flags & Flag::PublicVotes)
|
||||
? tr::lng_polls_public_quiz(tr::now)
|
||||
: tr::lng_polls_anonymous_quiz(tr::now))
|
||||
: ((_flags & Flag::PublicVotes)
|
||||
? tr::lng_polls_public(tr::now)
|
||||
: tr::lng_polls_anonymous(tr::now))));
|
||||
}
|
||||
|
||||
updateRecentVoters();
|
||||
updateAnswers();
|
||||
updateVotes();
|
||||
|
||||
|
|
@ -324,6 +377,16 @@ void Poll::updateTexts() {
|
|||
}
|
||||
}
|
||||
|
||||
void Poll::updateRecentVoters() {
|
||||
auto &&sliced = ranges::view::all(
|
||||
_poll->recentVoters
|
||||
) | ranges::view::take(kShowRecentVotersCount);
|
||||
const auto changed = !ranges::equal(_recentVoters, sliced);
|
||||
if (changed) {
|
||||
_recentVoters = sliced | ranges::to_vector;
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::updateAnswers() {
|
||||
const auto changed = !ranges::equal(
|
||||
_answers,
|
||||
|
|
@ -334,16 +397,16 @@ void Poll::updateAnswers() {
|
|||
if (!changed) {
|
||||
auto &&answers = ranges::view::zip(_answers, _poll->answers);
|
||||
for (auto &&[answer, original] : answers) {
|
||||
answer.fillText(original);
|
||||
answer.fillData(_poll, original);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_answers = ranges::view::all(
|
||||
_poll->answers
|
||||
) | ranges::view::transform([](const PollAnswer &answer) {
|
||||
) | ranges::view::transform([&](const PollAnswer &answer) {
|
||||
auto result = Answer();
|
||||
result.option = answer.option;
|
||||
result.fillText(answer);
|
||||
result.fillData(_poll, answer);
|
||||
return result;
|
||||
}) | ranges::to_vector;
|
||||
|
||||
|
|
@ -355,12 +418,66 @@ void Poll::updateAnswers() {
|
|||
}
|
||||
|
||||
ClickHandlerPtr Poll::createAnswerClickHandler(
|
||||
const Answer &answer) const {
|
||||
const Answer &answer) {
|
||||
const auto option = answer.option;
|
||||
const auto itemId = _parent->data()->fullId();
|
||||
return std::make_shared<LambdaClickHandler>([=] {
|
||||
history()->session().api().sendPollVotes(itemId, { option });
|
||||
});
|
||||
if (_flags & PollData::Flag::MultiChoice) {
|
||||
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
|
||||
toggleMultiOption(option);
|
||||
}));
|
||||
}
|
||||
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
|
||||
history()->session().api().sendPollVotes(
|
||||
_parent->data()->fullId(),
|
||||
{ option });
|
||||
}));
|
||||
}
|
||||
|
||||
void Poll::toggleMultiOption(const QByteArray &option) {
|
||||
const auto i = ranges::find(
|
||||
_answers,
|
||||
option,
|
||||
&Answer::option);
|
||||
if (i != end(_answers)) {
|
||||
const auto selected = i->selected;
|
||||
i->selected = !selected;
|
||||
i->selectedAnimation.start(
|
||||
[=] { history()->owner().requestViewRepaint(_parent); },
|
||||
selected ? 1. : 0.,
|
||||
selected ? 0. : 1.,
|
||||
st::defaultCheck.duration);
|
||||
if (selected) {
|
||||
const auto j = ranges::find(
|
||||
_answers,
|
||||
true,
|
||||
&Answer::selected);
|
||||
_hasSelected = (j != end(_answers));
|
||||
} else {
|
||||
_hasSelected = true;
|
||||
}
|
||||
history()->owner().requestViewRepaint(_parent);
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::sendMultiOptions() {
|
||||
auto chosen = _answers | ranges::view::filter(
|
||||
&Answer::selected
|
||||
) | ranges::view::transform(
|
||||
&Answer::option
|
||||
) | ranges::to_vector;
|
||||
if (!chosen.empty()) {
|
||||
for (auto &answer : _answers) {
|
||||
answer.selected = false;
|
||||
}
|
||||
history()->session().api().sendPollVotes(
|
||||
_parent->data()->fullId(),
|
||||
std::move(chosen));
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::showResults() {
|
||||
_parent->delegate()->elementShowPollResults(
|
||||
_poll,
|
||||
_parent->data()->fullId());
|
||||
}
|
||||
|
||||
void Poll::updateVotes() {
|
||||
|
|
@ -370,21 +487,23 @@ void Poll::updateVotes() {
|
|||
}
|
||||
|
||||
void Poll::checkSendingAnimation() const {
|
||||
const auto &sending = _poll->sendingVote;
|
||||
if (sending.isEmpty() == !_sendingAnimation) {
|
||||
const auto &sending = _poll->sendingVotes;
|
||||
const auto sendingRadial = (sending.size() == 1)
|
||||
&& !(_flags & PollData::Flag::MultiChoice);
|
||||
if (sendingRadial == (_sendingAnimation != nullptr)) {
|
||||
if (_sendingAnimation) {
|
||||
_sendingAnimation->option = sending;
|
||||
_sendingAnimation->option = sending.front();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sending.isEmpty()) {
|
||||
if (!sendingRadial) {
|
||||
if (!_answersAnimation) {
|
||||
_sendingAnimation = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_sendingAnimation = std::make_unique<SendingAnimation>(
|
||||
sending,
|
||||
sending.front(),
|
||||
[=] { radialAnimationCallback(); });
|
||||
_sendingAnimation->animation.start();
|
||||
}
|
||||
|
|
@ -394,9 +513,17 @@ void Poll::updateTotalVotes() {
|
|||
return;
|
||||
}
|
||||
_totalVotes = _poll->totalVoters;
|
||||
const auto quiz = _poll->quiz();
|
||||
const auto string = !_totalVotes
|
||||
? tr::lng_polls_votes_none(tr::now)
|
||||
: tr::lng_polls_votes_count(tr::now, lt_count_short, _totalVotes);
|
||||
? (quiz
|
||||
? tr::lng_polls_answers_none
|
||||
: tr::lng_polls_votes_none)(tr::now)
|
||||
: (quiz
|
||||
? tr::lng_polls_answers_count
|
||||
: tr::lng_polls_votes_count)(
|
||||
tr::now,
|
||||
lt_count_short,
|
||||
_totalVotes);
|
||||
_totalVotesLabel.setText(st::msgDateTextStyle, string);
|
||||
}
|
||||
|
||||
|
|
@ -485,6 +612,7 @@ void Poll::draw(Painter &p, const QRect &r, TextSelection selection, crl::time m
|
|||
|
||||
p.setPen(regular);
|
||||
_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());
|
||||
paintRecentVoters(p, padding.left() + _subtitle.maxWidth(), tshift, selection);
|
||||
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
|
||||
|
||||
const auto progress = _answersAnimation
|
||||
|
|
@ -517,23 +645,80 @@ void Poll::draw(Painter &p, const QRect &r, TextSelection selection, crl::time m
|
|||
selection);
|
||||
tshift += height;
|
||||
}
|
||||
if (!_totalVotesLabel.isEmpty()) {
|
||||
if (!inlineFooter()) {
|
||||
paintBottom(p, padding.left(), tshift, paintw, selection);
|
||||
} else if (!_totalVotesLabel.isEmpty()) {
|
||||
tshift += st::msgPadding.bottom();
|
||||
paintInlineFooter(p, padding.left(), tshift, paintw, selection);
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::paintInlineFooter(
|
||||
Painter &p,
|
||||
int left,
|
||||
int top,
|
||||
int paintw,
|
||||
TextSelection selection) const {
|
||||
const auto selected = (selection == FullSelection);
|
||||
const auto outbg = _parent->hasOutLayout();
|
||||
const auto ®ular = 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 ®ular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
|
||||
if (showVotersCount()) {
|
||||
p.setPen(regular);
|
||||
_totalVotesLabel.drawLeftElided(
|
||||
p,
|
||||
padding.left(),
|
||||
tshift,
|
||||
std::min(
|
||||
_totalVotesLabel.maxWidth(),
|
||||
paintw - _parent->infoWidth()),
|
||||
width());
|
||||
_totalVotesLabel.draw(p, left, stringtop, paintw, style::al_top);
|
||||
} else {
|
||||
const auto link = showVotes()
|
||||
? _showResultsLink
|
||||
: canSendVotes()
|
||||
? _sendVotesLink
|
||||
: nullptr;
|
||||
if (_linkRipple) {
|
||||
const auto linkHeight = bottomButtonHeight();
|
||||
p.setOpacity(st::historyPollRippleOpacity);
|
||||
_linkRipple->paint(p, left - st::msgPadding.left(), height() - linkHeight, width());
|
||||
if (_linkRipple->empty()) {
|
||||
_linkRipple.reset();
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
p.setFont(st::semiboldFont);
|
||||
if (!link) {
|
||||
p.setPen(regular);
|
||||
} else {
|
||||
p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg));
|
||||
}
|
||||
const auto string = showVotes()
|
||||
? tr::lng_polls_view_results(tr::now, Ui::Text::Upper)
|
||||
: tr::lng_polls_submit_votes(tr::now, Ui::Text::Upper);
|
||||
const auto stringw = st::semiboldFont->width(string);
|
||||
p.drawTextLeft(left + (paintw - stringw) / 2, stringtop, width(), string, stringw);
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::resetAnswersAnimation() const {
|
||||
_answersAnimation = nullptr;
|
||||
if (_poll->sendingVote.isEmpty()) {
|
||||
if (_poll->sendingVotes.size() != 1
|
||||
|| (_flags & PollData::Flag::MultiChoice)) {
|
||||
_sendingAnimation = nullptr;
|
||||
}
|
||||
}
|
||||
|
|
@ -544,6 +729,36 @@ void Poll::radialAnimationCallback() const {
|
|||
}
|
||||
}
|
||||
|
||||
void Poll::paintRecentVoters(
|
||||
Painter &p,
|
||||
int left,
|
||||
int top,
|
||||
TextSelection selection) const {
|
||||
const auto count = int(_recentVoters.size());
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
auto x = left
|
||||
+ st::historyPollRecentVotersSkip
|
||||
+ (count - 1) * st::historyPollRecentVoterSkip;
|
||||
auto y = top;
|
||||
const auto size = st::historyPollRecentVoterSize;
|
||||
const auto outbg = _parent->hasOutLayout();
|
||||
const auto selected = (selection == FullSelection);
|
||||
auto pen = (selected
|
||||
? (outbg ? st::msgOutBgSelected : st::msgInBgSelected)
|
||||
: (outbg ? st::msgOutBg : st::msgInBg))->p;
|
||||
pen.setWidth(st::lineWidth);
|
||||
for (const auto &recent : _recentVoters) {
|
||||
recent->paintUserpic(p, x, y, size);
|
||||
p.setPen(pen);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(x, y, size, size);
|
||||
x -= st::historyPollRecentVoterSkip;
|
||||
}
|
||||
}
|
||||
|
||||
int Poll::paintAnswer(
|
||||
Painter &p,
|
||||
const Answer &answer,
|
||||
|
|
@ -592,6 +807,8 @@ int Poll::paintAnswer(
|
|||
p.setOpacity(sqrt(opacity));
|
||||
paintFilling(
|
||||
p,
|
||||
animation->chosen,
|
||||
animation->correct,
|
||||
animation->filling.current(),
|
||||
left,
|
||||
top,
|
||||
|
|
@ -613,6 +830,8 @@ int Poll::paintAnswer(
|
|||
selection);
|
||||
paintFilling(
|
||||
p,
|
||||
answer.chosen,
|
||||
answer.correct,
|
||||
answer.filling,
|
||||
left,
|
||||
top,
|
||||
|
|
@ -644,9 +863,13 @@ void Poll::paintRadio(
|
|||
const auto over = ClickHandler::showAsActive(answer.handler);
|
||||
const auto ®ular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg);
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
const auto checkmark = answer.selectedAnimation.value(answer.selected ? 1. : 0.);
|
||||
|
||||
const auto o = p.opacity();
|
||||
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
|
||||
if (checkmark < 1.) {
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
|
||||
}
|
||||
|
||||
const auto rect = QRectF(left, top, st.diameter, st.diameter).marginsRemoved(QMarginsF(st.thickness / 2., st.thickness / 2., st.thickness / 2., st.thickness / 2.));
|
||||
if (_sendingAnimation && _sendingAnimation->option == answer.option) {
|
||||
|
|
@ -665,10 +888,24 @@ void Poll::paintRadio(
|
|||
state.arcLength);
|
||||
}
|
||||
} else {
|
||||
auto pen = regular->p;
|
||||
pen.setWidth(st.thickness);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(rect);
|
||||
if (checkmark < 1.) {
|
||||
auto pen = regular->p;
|
||||
pen.setWidth(st.thickness);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(rect);
|
||||
}
|
||||
if (checkmark > 0.) {
|
||||
const auto removeFull = (st.diameter / 2 - st.thickness);
|
||||
const auto removeNow = removeFull * (1. - checkmark);
|
||||
const auto color = outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg);
|
||||
auto pen = color->p;
|
||||
pen.setWidth(st.thickness);
|
||||
p.setPen(pen);
|
||||
p.setBrush(color);
|
||||
p.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow }));
|
||||
const auto &icon = outbg ? (selected ? st::historyPollOutChosenSelected : st::historyPollOutChosen) : (selected ? st::historyPollInChosenSelected : st::historyPollInChosen);
|
||||
icon.paint(p, left + (st.diameter - icon.width()) / 2, top + (st.diameter - icon.height()) / 2, width());
|
||||
}
|
||||
}
|
||||
|
||||
p.setOpacity(o);
|
||||
|
|
@ -696,6 +933,8 @@ void Poll::paintPercent(
|
|||
|
||||
void Poll::paintFilling(
|
||||
Painter &p,
|
||||
bool chosen,
|
||||
bool correct,
|
||||
float64 filling,
|
||||
int left,
|
||||
int top,
|
||||
|
|
@ -712,15 +951,36 @@ void Poll::paintFilling(
|
|||
|
||||
top += st::historyPollAnswerPadding.top();
|
||||
|
||||
const auto bar = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(bar);
|
||||
const auto thickness = st::historyPollFillingHeight;
|
||||
const auto max = awidth - st::historyPollFillingRight;
|
||||
const auto size = anim::interpolate(st::historyPollFillingMin, max, filling);
|
||||
const auto radius = st::historyPollFillingRadius;
|
||||
const auto ftop = bottom - st::historyPollFillingBottom - st::historyPollFillingHeight;
|
||||
p.drawRoundedRect(aleft, ftop, size, st::historyPollFillingHeight, radius, radius);
|
||||
const auto ftop = bottom - st::historyPollFillingBottom - thickness;
|
||||
|
||||
if (chosen && !correct) {
|
||||
p.setBrush(st::boxTextFgError);
|
||||
} else {
|
||||
const auto bar = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
|
||||
p.setBrush(bar);
|
||||
}
|
||||
auto barleft = aleft;
|
||||
auto barwidth = size;
|
||||
if (chosen || correct) {
|
||||
const auto &icon = (chosen && !correct)
|
||||
? st::historyPollChoiceWrong
|
||||
: st::historyPollChoiceRight;
|
||||
const auto cleft = aleft - st::historyPollPercentSkip - icon.width();
|
||||
const auto ctop = ftop - (icon.height() - thickness) / 2;
|
||||
p.drawEllipse(cleft, ctop, icon.width(), icon.height());
|
||||
icon.paint(p, cleft, ctop, width);
|
||||
//barleft += icon.width() - radius;
|
||||
//barwidth -= icon.width() - radius;
|
||||
}
|
||||
if (barwidth > 0) {
|
||||
p.drawRoundedRect(barleft, ftop, barwidth, thickness, radius, radius);
|
||||
}
|
||||
}
|
||||
|
||||
bool Poll::answerVotesChanged() const {
|
||||
|
|
@ -748,6 +1008,8 @@ void Poll::saveStateInAnimation() const {
|
|||
result.percent = show ? float64(answer.votesPercent) : 0.;
|
||||
result.filling = show ? answer.filling : 0.;
|
||||
result.opacity = show ? 1. : 0.;
|
||||
result.chosen = answer.chosen;
|
||||
result.correct = answer.correct;
|
||||
return result;
|
||||
};
|
||||
ranges::transform(
|
||||
|
|
@ -761,7 +1023,7 @@ bool Poll::checkAnimationStart() const {
|
|||
// Skip initial changes.
|
||||
return false;
|
||||
}
|
||||
const auto result = (showVotes() != (_poll->voted() || _poll->closed))
|
||||
const auto result = (showVotes() != (_poll->voted() || _poll->closed()))
|
||||
|| answerVotesChanged();
|
||||
if (result) {
|
||||
saveStateInAnimation();
|
||||
|
|
@ -780,6 +1042,8 @@ void Poll::startAnswersAnimation() const {
|
|||
data.percent.start(show ? float64(answer.votesPercent) : 0.);
|
||||
data.filling.start(show ? answer.filling : 0.);
|
||||
data.opacity.start(show ? 1. : 0.);
|
||||
data.chosen = data.chosen || answer.chosen;
|
||||
data.correct = data.correct || answer.correct;
|
||||
}
|
||||
_answersAnimation->progress.start(
|
||||
[=] { history()->owner().requestViewRepaint(_parent); },
|
||||
|
|
@ -790,7 +1054,7 @@ void Poll::startAnswersAnimation() const {
|
|||
|
||||
TextState Poll::textState(QPoint point, StateRequest request) const {
|
||||
auto result = TextState(_parent);
|
||||
if (!_poll->sendingVote.isEmpty()) {
|
||||
if (!_poll->sendingVotes.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -819,15 +1083,39 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
|
|||
result.customTooltip = true;
|
||||
using Flag = Ui::Text::StateRequest::Flag;
|
||||
if (request.flags & Flag::LookupCustomTooltip) {
|
||||
const auto quiz = _poll->quiz();
|
||||
result.customTooltipText = answer.votes
|
||||
? tr::lng_polls_votes_count(tr::now, lt_count_decimal, answer.votes)
|
||||
: tr::lng_polls_votes_none(tr::now);
|
||||
? (quiz
|
||||
? tr::lng_polls_answers_count
|
||||
: tr::lng_polls_votes_count)(
|
||||
tr::now,
|
||||
lt_count_decimal,
|
||||
answer.votes)
|
||||
: (quiz
|
||||
? tr::lng_polls_answers_none
|
||||
: tr::lng_polls_votes_none)(tr::now);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
tshift += height;
|
||||
}
|
||||
if (!showVotersCount()) {
|
||||
const auto link = showVotes()
|
||||
? _showResultsLink
|
||||
: canSendVotes()
|
||||
? _sendVotesLink
|
||||
: nullptr;
|
||||
if (link) {
|
||||
const auto linkHeight = bottomButtonHeight();
|
||||
const auto linkTop = height() - linkHeight;
|
||||
if (QRect(0, linkTop, width(), linkHeight).contains(point)) {
|
||||
_lastLinkPoint = point;
|
||||
result.link = link;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -842,6 +1130,8 @@ void Poll::clickHandlerPressedChanged(
|
|||
&Answer::handler);
|
||||
if (i != end(_answers)) {
|
||||
toggleRipple(*i, pressed);
|
||||
} else if (handler == _sendVotesLink || handler == _showResultsLink) {
|
||||
toggleLinkRipple(pressed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -864,10 +1154,55 @@ void Poll::toggleRipple(Answer &answer, bool pressed) {
|
|||
}
|
||||
const auto top = countAnswerTop(answer, innerWidth);
|
||||
answer.ripple->add(_lastLinkPoint - QPoint(0, top));
|
||||
} else {
|
||||
if (answer.ripple) {
|
||||
answer.ripple->lastStop();
|
||||
} else if (answer.ripple) {
|
||||
answer.ripple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
int Poll::bottomButtonHeight() const {
|
||||
const auto skip = st::historyPollChoiceRight.height()
|
||||
- st::historyPollFillingBottom
|
||||
- st::historyPollFillingHeight
|
||||
- (st::historyPollChoiceRight.height() - st::historyPollFillingHeight) / 2;
|
||||
return st::historyPollTotalVotesSkip
|
||||
- skip
|
||||
+ st::historyPollBottomButtonSkip
|
||||
+ st::msgDateFont->height
|
||||
+ st::msgPadding.bottom();
|
||||
}
|
||||
|
||||
void Poll::toggleLinkRipple(bool pressed) {
|
||||
if (pressed) {
|
||||
const auto linkWidth = width();
|
||||
const auto linkHeight = bottomButtonHeight();
|
||||
if (!_linkRipple) {
|
||||
const auto drawMask = [&](QPainter &p) {
|
||||
const auto radius = st::historyMessageRadius;
|
||||
p.drawRoundedRect(
|
||||
0,
|
||||
0,
|
||||
linkWidth,
|
||||
linkHeight,
|
||||
radius,
|
||||
radius);
|
||||
p.fillRect(0, 0, linkWidth, radius * 2, Qt::white);
|
||||
};
|
||||
auto mask = isBubbleBottom()
|
||||
? Ui::RippleAnimation::maskByDrawer(
|
||||
QSize(linkWidth, linkHeight),
|
||||
false,
|
||||
drawMask)
|
||||
: Ui::RippleAnimation::rectMask({ linkWidth, linkHeight });
|
||||
_linkRipple = std::make_unique<Ui::RippleAnimation>(
|
||||
(_parent->hasOutLayout()
|
||||
? st::historyPollRippleOut
|
||||
: st::historyPollRippleIn),
|
||||
std::move(mask),
|
||||
[=] { history()->owner().requestViewRepaint(_parent); });
|
||||
}
|
||||
_linkRipple->add(_lastLinkPoint - QPoint(0, height() - linkHeight));
|
||||
} else if (_linkRipple) {
|
||||
_linkRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#pragma once
|
||||
|
||||
#include "history/view/media/history_view_media.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
struct PollAnswer;
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
class Poll : public Media {
|
||||
class Poll : public Media, public base::has_weak_ptr {
|
||||
public:
|
||||
Poll(
|
||||
not_null<Element*> parent,
|
||||
|
|
@ -53,6 +57,7 @@ private:
|
|||
|
||||
[[nodiscard]] bool showVotes() const;
|
||||
[[nodiscard]] bool canVote() const;
|
||||
[[nodiscard]] bool canSendVotes() const;
|
||||
|
||||
[[nodiscard]] int countAnswerTop(
|
||||
const Answer &answer,
|
||||
|
|
@ -61,11 +66,14 @@ private:
|
|||
const Answer &answer,
|
||||
int innerWidth) const;
|
||||
[[nodiscard]] ClickHandlerPtr createAnswerClickHandler(
|
||||
const Answer &answer) const;
|
||||
const Answer &answer);
|
||||
void updateTexts();
|
||||
void updateRecentVoters();
|
||||
void updateAnswers();
|
||||
void updateVotes();
|
||||
void updateTotalVotes();
|
||||
bool showVotersCount() const;
|
||||
bool inlineFooter() const;
|
||||
void updateAnswerVotes();
|
||||
void updateAnswerVotesFromOriginal(
|
||||
Answer &answer,
|
||||
|
|
@ -74,6 +82,11 @@ private:
|
|||
int maxVotes);
|
||||
void checkSendingAnimation() const;
|
||||
|
||||
void paintRecentVoters(
|
||||
Painter &p,
|
||||
int left,
|
||||
int top,
|
||||
TextSelection selection) const;
|
||||
int paintAnswer(
|
||||
Painter &p,
|
||||
const Answer &answer,
|
||||
|
|
@ -99,12 +112,26 @@ private:
|
|||
TextSelection selection) const;
|
||||
void paintFilling(
|
||||
Painter &p,
|
||||
bool chosen,
|
||||
bool correct,
|
||||
float64 filling,
|
||||
int left,
|
||||
int top,
|
||||
int width,
|
||||
int height,
|
||||
TextSelection selection) const;
|
||||
void paintInlineFooter(
|
||||
Painter &p,
|
||||
int left,
|
||||
int top,
|
||||
int paintw,
|
||||
TextSelection selection) const;
|
||||
void paintBottom(
|
||||
Painter &p,
|
||||
int left,
|
||||
int top,
|
||||
int paintw,
|
||||
TextSelection selection) const;
|
||||
|
||||
bool checkAnimationStart() const;
|
||||
bool answerVotesChanged() const;
|
||||
|
|
@ -114,17 +141,30 @@ private:
|
|||
void radialAnimationCallback() const;
|
||||
|
||||
void toggleRipple(Answer &answer, bool pressed);
|
||||
void toggleLinkRipple(bool pressed);
|
||||
void toggleMultiOption(const QByteArray &option);
|
||||
void sendMultiOptions();
|
||||
void showResults();
|
||||
|
||||
not_null<PollData*> _poll;
|
||||
[[nodiscard]] int bottomButtonHeight() const;
|
||||
|
||||
const not_null<PollData*> _poll;
|
||||
int _pollVersion = 0;
|
||||
int _totalVotes = 0;
|
||||
bool _voted = false;
|
||||
bool _closed = false;
|
||||
PollData::Flags _flags = PollData::Flags();
|
||||
|
||||
Ui::Text::String _question;
|
||||
Ui::Text::String _subtitle;
|
||||
std::vector<not_null<UserData*>> _recentVoters;
|
||||
QImage _recentVotersImage;
|
||||
|
||||
std::vector<Answer> _answers;
|
||||
Ui::Text::String _totalVotesLabel;
|
||||
ClickHandlerPtr _showResultsLink;
|
||||
ClickHandlerPtr _sendVotesLink;
|
||||
mutable std::unique_ptr<Ui::RippleAnimation> _linkRipple;
|
||||
bool _hasSelected = false;
|
||||
|
||||
mutable std::unique_ptr<AnswersAnimation> _answersAnimation;
|
||||
mutable std::unique_ptr<SendingAnimation> _sendingAnimation;
|
||||
|
|
|
|||
|
|
@ -266,6 +266,8 @@ Key ContentMemento::key() const {
|
|||
return Key(Auth().data().peer(peerId));
|
||||
//} else if (const auto feed = this->feed()) { // #feed
|
||||
// return Key(feed);
|
||||
} else if (const auto poll = this->poll()) {
|
||||
return Key(poll, pollContextId());
|
||||
} else {
|
||||
return Settings::Tag{ settingsSelf() };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,10 @@ public:
|
|||
//explicit ContentMemento(not_null<Data::Feed*> feed) : _feed(feed) { // #feed
|
||||
//}
|
||||
explicit ContentMemento(Settings::Tag settings);
|
||||
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
|
||||
: _poll(poll)
|
||||
, _pollContextId(contextId) {
|
||||
}
|
||||
|
||||
virtual object_ptr<ContentWidget> createWidget(
|
||||
QWidget *parent,
|
||||
|
|
@ -141,6 +145,12 @@ public:
|
|||
UserData *settingsSelf() const {
|
||||
return _settingsSelf;
|
||||
}
|
||||
PollData *poll() const {
|
||||
return _poll;
|
||||
}
|
||||
FullMsgId pollContextId() const {
|
||||
return _pollContextId;
|
||||
}
|
||||
Key key() const;
|
||||
|
||||
virtual Section section() const = 0;
|
||||
|
|
@ -177,6 +187,9 @@ private:
|
|||
const PeerId _migratedPeerId = 0;
|
||||
//Data::Feed * const _feed = nullptr; // #feed
|
||||
UserData * const _settingsSelf = nullptr;
|
||||
PollData * const _poll = nullptr;
|
||||
const FullMsgId _pollContextId;
|
||||
|
||||
int _scrollTop = 0;
|
||||
QString _searchFieldQuery;
|
||||
bool _searchEnabledByContent = false;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "history/history_item.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
|
|
@ -46,6 +48,10 @@ Key::Key(not_null<PeerData*> peer) : _value(peer) {
|
|||
Key::Key(Settings::Tag settings) : _value(settings) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
|
||||
: _value(PollKey{ poll, contextId }) {
|
||||
}
|
||||
|
||||
PeerData *Key::peer() const {
|
||||
if (const auto peer = base::get_if<not_null<PeerData*>>(&_value)) {
|
||||
return *peer;
|
||||
|
|
@ -67,6 +73,20 @@ UserData *Key::settingsSelf() const {
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
PollData *Key::poll() const {
|
||||
if (const auto data = base::get_if<PollKey>(&_value)) {
|
||||
return data->poll;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FullMsgId Key::pollContextId() const {
|
||||
if (const auto data = base::get_if<PollKey>(&_value)) {
|
||||
return data->contextId;
|
||||
}
|
||||
return FullMsgId();
|
||||
}
|
||||
|
||||
rpl::producer<SparseIdsMergedSlice> AbstractController::mediaSource(
|
||||
SparseIdsMergedSlice::UniversalMsgId aroundId,
|
||||
int limitBefore,
|
||||
|
|
@ -106,6 +126,15 @@ PeerId AbstractController::migratedPeerId() const {
|
|||
return PeerId(0);
|
||||
}
|
||||
|
||||
PollData *AbstractController::poll() const {
|
||||
if (const auto item = session().data().message(pollContextId())) {
|
||||
if (const auto media = item->media()) {
|
||||
return media->poll();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AbstractController::showSection(
|
||||
Window::SectionMemento &&memento,
|
||||
const Window::SectionShow ¶ms) {
|
||||
|
|
|
|||
|
|
@ -33,16 +33,24 @@ public:
|
|||
Key(not_null<PeerData*> peer);
|
||||
//Key(not_null<Data::Feed*> feed); // #feed
|
||||
Key(Settings::Tag settings);
|
||||
Key(not_null<PollData*> poll, FullMsgId contextId);
|
||||
|
||||
PeerData *peer() const;
|
||||
//Data::Feed *feed() const; // #feed
|
||||
UserData *settingsSelf() const;
|
||||
PollData *poll() const;
|
||||
FullMsgId pollContextId() const;
|
||||
|
||||
private:
|
||||
struct PollKey {
|
||||
not_null<PollData*> poll;
|
||||
FullMsgId contextId;
|
||||
};
|
||||
base::variant<
|
||||
not_null<PeerData*>,
|
||||
//not_null<Data::Feed*>, // #feed
|
||||
Settings::Tag> _value;
|
||||
Settings::Tag,
|
||||
PollKey> _value;
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -60,6 +68,7 @@ public:
|
|||
Members,
|
||||
//Channels, // #feed
|
||||
Settings,
|
||||
PollResults,
|
||||
};
|
||||
using SettingsType = ::Settings::Type;
|
||||
using MediaType = Storage::SharedMediaType;
|
||||
|
|
@ -113,6 +122,10 @@ public:
|
|||
UserData *settingsSelf() const {
|
||||
return key().settingsSelf();
|
||||
}
|
||||
PollData *poll() const;
|
||||
FullMsgId pollContextId() const {
|
||||
return key().pollContextId();
|
||||
}
|
||||
|
||||
virtual void setSearchEnabledByContent(bool enabled) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "info/common_groups/info_common_groups_widget.h"
|
||||
//#include "info/feed/info_feed_profile_widget.h" // #feed
|
||||
#include "info/settings/info_settings_widget.h"
|
||||
#include "info/polls/info_polls_results_widget.h"
|
||||
#include "info/info_section_widget.h"
|
||||
#include "info/info_layer_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
|
|
@ -42,6 +43,10 @@ Memento::Memento(Settings::Tag settings, Section section)
|
|||
: Memento(DefaultStack(settings, section)) {
|
||||
}
|
||||
|
||||
Memento::Memento(not_null<PollData*> poll, FullMsgId contextId)
|
||||
: Memento(DefaultStack(poll, contextId)) {
|
||||
}
|
||||
|
||||
Memento::Memento(std::vector<std::unique_ptr<ContentMemento>> stack)
|
||||
: _stack(std::move(stack)) {
|
||||
}
|
||||
|
|
@ -72,6 +77,14 @@ std::vector<std::unique_ptr<ContentMemento>> Memento::DefaultStack(
|
|||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<ContentMemento>> Memento::DefaultStack(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId contextId) {
|
||||
auto result = std::vector<std::unique_ptr<ContentMemento>>();
|
||||
result.push_back(std::make_unique<Polls::Memento>(poll, contextId));
|
||||
return result;
|
||||
}
|
||||
|
||||
Section Memento::DefaultSection(not_null<PeerData*> peer) {
|
||||
if (peer->isSelf()) {
|
||||
return Section(Section::MediaType::Photo);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public:
|
|||
Memento(PeerId peerId, Section section);
|
||||
//Memento(not_null<Data::Feed*> feed, Section section); // #feed
|
||||
Memento(Settings::Tag settings, Section section);
|
||||
Memento(not_null<PollData*> poll, FullMsgId contextId);
|
||||
explicit Memento(std::vector<std::unique_ptr<ContentMemento>> stack);
|
||||
|
||||
object_ptr<Window::SectionWidget> createWidget(
|
||||
|
|
@ -76,10 +77,13 @@ private:
|
|||
static std::vector<std::unique_ptr<ContentMemento>> DefaultStack(
|
||||
Settings::Tag settings,
|
||||
Section section);
|
||||
static std::vector<std::unique_ptr<ContentMemento>> DefaultStack(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId contextId);
|
||||
|
||||
//static std::unique_ptr<ContentMemento> DefaultContent( // #feed
|
||||
// not_null<Data::Feed*> feed,
|
||||
// Section section);
|
||||
|
||||
static std::unique_ptr<ContentMemento> DefaultContent(
|
||||
PeerId peerId,
|
||||
Section section);
|
||||
|
|
|
|||
|
|
@ -635,6 +635,11 @@ rpl::producer<QString> TitleValue(
|
|||
return tr::ktg_settings_kotato();
|
||||
}
|
||||
Unexpected("Bad settings type in Info::TitleValue()");
|
||||
|
||||
case Section::Type::PollResults:
|
||||
return key.poll()->quiz()
|
||||
? tr::lng_polls_quiz_results_title()
|
||||
: tr::lng_polls_poll_results_title();
|
||||
}
|
||||
Unexpected("Bad section type in Info::TitleValue()");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const {
|
|||
return Dialogs::RowDescriptor(peer->owner().history(peer), FullMsgId());
|
||||
//} else if (const auto feed = key().feed()) { // #feed
|
||||
// return Dialogs::RowDescriptor(feed, FullMsgId());
|
||||
} else if (key().settingsSelf()) {
|
||||
} else if (key().settingsSelf() || key().poll()) {
|
||||
return Dialogs::RowDescriptor();
|
||||
}
|
||||
Unexpected("Owner in WrapWidget::activeChat().");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
111
Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp
Normal 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
|
||||
70
Telegram/SourceFiles/info/polls/info_polls_results_widget.h
Normal 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
|
||||
|
|
@ -20,7 +20,6 @@ namespace Settings {
|
|||
using Type = Section::SettingsType;
|
||||
|
||||
struct Tag;
|
||||
class InnerWidget;
|
||||
|
||||
class Memento final : public ContentMemento {
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ void File::Context::readNextPacket() {
|
|||
if (i->second.size() == kMaxQueuedPackets) {
|
||||
processQueuedPackets(SleepPolicy::Allowed);
|
||||
}
|
||||
Assert(i->second.size() < kMaxQueuedPackets);
|
||||
} else {
|
||||
// Still trying to read by drain.
|
||||
Assert(result.is<FFmpeg::AvErrorWrap>());
|
||||
|
|
|
|||
|
|
@ -396,6 +396,8 @@ bool Player::fileProcessPackets(
|
|||
videoReceivedTill(till);
|
||||
});
|
||||
_video->process(base::take(list));
|
||||
} else {
|
||||
list.clear(); // Free non-needed packets.
|
||||
}
|
||||
}
|
||||
return fileReadMore();
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ namespace View {
|
|||
namespace {
|
||||
|
||||
constexpr auto kPreloadCount = 4;
|
||||
constexpr auto kMaxZoomLevel = 7; // x8
|
||||
|
||||
constexpr auto kOverlayLoaderPriority = 2;
|
||||
|
||||
|
|
@ -897,13 +898,13 @@ bool OverlayWidget::radialAnimationCallback(crl::time now) {
|
|||
void OverlayWidget::zoomIn() {
|
||||
int32 newZoom = _zoom;
|
||||
if (newZoom == ZoomToScreenLevel) {
|
||||
if (qCeil(_zoomToScreen) <= MaxZoomLevel) {
|
||||
if (qCeil(_zoomToScreen) <= kMaxZoomLevel) {
|
||||
newZoom = qCeil(_zoomToScreen);
|
||||
}
|
||||
} else {
|
||||
if (newZoom < _zoomToScreen && (newZoom + 1 > _zoomToScreen || (_zoomToScreen > MaxZoomLevel && newZoom == MaxZoomLevel))) {
|
||||
if (newZoom < _zoomToScreen && (newZoom + 1 > _zoomToScreen || (_zoomToScreen > kMaxZoomLevel && newZoom == kMaxZoomLevel))) {
|
||||
newZoom = ZoomToScreenLevel;
|
||||
} else if (newZoom < MaxZoomLevel) {
|
||||
} else if (newZoom < kMaxZoomLevel) {
|
||||
++newZoom;
|
||||
}
|
||||
}
|
||||
|
|
@ -913,13 +914,13 @@ void OverlayWidget::zoomIn() {
|
|||
void OverlayWidget::zoomOut() {
|
||||
int32 newZoom = _zoom;
|
||||
if (newZoom == ZoomToScreenLevel) {
|
||||
if (qFloor(_zoomToScreen) >= -MaxZoomLevel) {
|
||||
if (qFloor(_zoomToScreen) >= -kMaxZoomLevel) {
|
||||
newZoom = qFloor(_zoomToScreen);
|
||||
}
|
||||
} else {
|
||||
if (newZoom > _zoomToScreen && (newZoom - 1 < _zoomToScreen || (_zoomToScreen < -MaxZoomLevel && newZoom == -MaxZoomLevel))) {
|
||||
if (newZoom > _zoomToScreen && (newZoom - 1 < _zoomToScreen || (_zoomToScreen < -kMaxZoomLevel && newZoom == -kMaxZoomLevel))) {
|
||||
newZoom = ZoomToScreenLevel;
|
||||
} else if (newZoom > -MaxZoomLevel) {
|
||||
} else if (newZoom > -kMaxZoomLevel) {
|
||||
--newZoom;
|
||||
}
|
||||
}
|
||||
|
|
@ -929,7 +930,7 @@ void OverlayWidget::zoomOut() {
|
|||
void OverlayWidget::zoomReset() {
|
||||
int32 newZoom = _zoom;
|
||||
if (_zoom == 0) {
|
||||
if (qFloor(_zoomToScreen) == qCeil(_zoomToScreen) && qRound(_zoomToScreen) >= -MaxZoomLevel && qRound(_zoomToScreen) <= MaxZoomLevel) {
|
||||
if (qFloor(_zoomToScreen) == qCeil(_zoomToScreen) && qRound(_zoomToScreen) >= -kMaxZoomLevel && qRound(_zoomToScreen) <= kMaxZoomLevel) {
|
||||
newZoom = qRound(_zoomToScreen);
|
||||
} else {
|
||||
newZoom = ZoomToScreenLevel;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "platform/linux/linux_desktop_environment.h"
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
#include <QDBusInterface>
|
||||
#endif
|
||||
|
||||
namespace Platform {
|
||||
namespace DesktopEnvironment {
|
||||
|
|
@ -117,11 +119,11 @@ bool TryQtTrayIcon() {
|
|||
|
||||
bool PreferAppIndicatorTrayIcon() {
|
||||
return IsXFCE() || IsUnity() || IsUbuntu() ||
|
||||
(IsGnome() && QDBusInterface("org.kde.StatusNotifierWatcher", "/").isValid());
|
||||
}
|
||||
|
||||
bool TryUnityCounter() {
|
||||
return IsUnity() || IsPantheon() || IsUbuntu() || IsKDE5();
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
(IsGnome() && QDBusInterface("org.kde.StatusNotifierWatcher", "/").isValid());
|
||||
#else
|
||||
IsGnome();
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace DesktopEnvironment
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ inline bool IsAwesome() {
|
|||
|
||||
bool TryQtTrayIcon();
|
||||
bool PreferAppIndicatorTrayIcon();
|
||||
bool TryUnityCounter();
|
||||
|
||||
} // namespace DesktopEnvironment
|
||||
} // namespace Platform
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "facades.h"
|
||||
#include "app.h"
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
#include <QtDBus>
|
||||
#endif
|
||||
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QAction>
|
||||
|
||||
|
|
@ -43,7 +46,10 @@ bool _trayIconMuted = true;
|
|||
int32 _trayIconCount = 0;
|
||||
QImage _trayIconImageBack, _trayIconImage;
|
||||
QString _desktopFile;
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
QString _dbusPath = "/";
|
||||
#endif
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
|
||||
void _trayIconPopup(GtkStatusIcon *status_icon, guint button, guint32 activate_time, gpointer popup_menu) {
|
||||
|
|
@ -331,6 +337,7 @@ void MainWindow::updateIconCounters() {
|
|||
|
||||
const auto counter = Core::App().unreadBadge();
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
if (useUnityCount) {
|
||||
QVariantMap dbusUnityProperties;
|
||||
if (counter > 0) {
|
||||
|
|
@ -345,6 +352,7 @@ void MainWindow::updateIconCounters() {
|
|||
signal << dbusUnityProperties;
|
||||
QDBusConnection::sessionBus().send(signal);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (noQtTrayIcon) {
|
||||
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
|
||||
|
|
@ -534,14 +542,12 @@ void MainWindow::psCreateTrayIcon() {
|
|||
void MainWindow::psFirstShow() {
|
||||
psCreateTrayIcon();
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
if (QDBusInterface("com.canonical.Unity", "/").isValid()) {
|
||||
auto snapName = QString::fromLatin1(qgetenv("SNAP_NAME"));
|
||||
if(snapName.isEmpty()) {
|
||||
std::vector<QString> possibleDesktopFiles = {
|
||||
#ifdef TDESKTOP_LAUNCHER_FILENAME
|
||||
MACRO_TO_STRING(TDESKTOP_LAUNCHER_FILENAME),
|
||||
#endif // TDESKTOP_LAUNCHER_FILENAME
|
||||
"kotatogramdesktop.desktop",
|
||||
MACRO_TO_STRING(TDESKTOP_LAUNCHER_BASENAME) ".desktop",
|
||||
"Kotatogram.desktop"
|
||||
};
|
||||
|
||||
|
|
@ -565,6 +571,7 @@ void MainWindow::psFirstShow() {
|
|||
} else {
|
||||
LOG(("Not using Unity Launcher count."));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool showShadows = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "facades.h"
|
||||
|
||||
#include <QtCore/QBuffer>
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
#include <QtDBus/QDBusConnection>
|
||||
#include <QtDBus/QDBusReply>
|
||||
#include <QtDBus/QDBusMetaType>
|
||||
#endif
|
||||
|
||||
namespace Platform {
|
||||
namespace Notifications {
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
namespace {
|
||||
|
||||
constexpr auto kService = str_const("org.freedesktop.Notifications");
|
||||
|
|
@ -52,19 +57,19 @@ std::vector<QString> GetServerInformation(
|
|||
return serverInformation;
|
||||
}
|
||||
|
||||
std::vector<QString> GetCapabilities(
|
||||
QStringList GetCapabilities(
|
||||
const std::shared_ptr<QDBusInterface> ¬ificationInterface) {
|
||||
QDBusReply<QStringList> capabilitiesReply = notificationInterface
|
||||
->call(qsl("GetCapabilities"));
|
||||
|
||||
if (capabilitiesReply.isValid()) {
|
||||
return capabilitiesReply.value().toVector().toStdVector();
|
||||
return capabilitiesReply.value();
|
||||
} else {
|
||||
LOG(("Native notification error: %1")
|
||||
.arg(capabilitiesReply.error().message()));
|
||||
}
|
||||
|
||||
return std::vector<QString>();
|
||||
return {};
|
||||
}
|
||||
|
||||
QVersionNumber ParseSpecificationVersion(
|
||||
|
|
@ -92,9 +97,8 @@ NotificationData::NotificationData(
|
|||
, _peerId(peerId)
|
||||
, _msgId(msgId) {
|
||||
auto capabilities = GetCapabilities(_notificationInterface);
|
||||
auto capabilitiesEnd = capabilities.end();
|
||||
|
||||
if (ranges::find(capabilities, qsl("body-markup")) != capabilitiesEnd) {
|
||||
if (capabilities.contains(qsl("body-markup"))) {
|
||||
_body = subtitle.isEmpty()
|
||||
? msg.toHtmlEscaped()
|
||||
: qsl("<b>%1</b>\n%2").arg(subtitle.toHtmlEscaped())
|
||||
|
|
@ -105,24 +109,33 @@ NotificationData::NotificationData(
|
|||
: qsl("%1\n%2").arg(subtitle).arg(msg);
|
||||
}
|
||||
|
||||
if (ranges::find(capabilities, qsl("actions")) != capabilitiesEnd) {
|
||||
if (capabilities.contains(qsl("actions"))) {
|
||||
_actions << qsl("default") << QString();
|
||||
|
||||
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
|
||||
_actions << qsl("mail-reply-sender")
|
||||
<< tr::lng_notification_reply(tr::now);
|
||||
|
||||
connect(_notificationInterface.get(),
|
||||
SIGNAL(ActionInvoked(uint, QString)),
|
||||
this, SLOT(notificationClicked(uint)));
|
||||
|
||||
if (capabilities.contains(qsl("inline-reply"))) {
|
||||
_actions << qsl("inline-reply")
|
||||
<< tr::lng_notification_reply(tr::now);
|
||||
|
||||
connect(_notificationInterface.get(),
|
||||
SIGNAL(NotificationReplied(uint,QString)),
|
||||
this, SLOT(notificationReplied(uint,QString)));
|
||||
} else {
|
||||
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
|
||||
_actions << qsl("mail-reply-sender")
|
||||
<< tr::lng_notification_reply(tr::now);
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges::find(capabilities, qsl("action-icons")) != capabilitiesEnd) {
|
||||
if (capabilities.contains(qsl("action-icons"))) {
|
||||
_hints["action-icons"] = true;
|
||||
}
|
||||
|
||||
// suppress system sound if telegram sound activated, otherwise use system sound
|
||||
if (ranges::find(capabilities, qsl("sound")) != capabilitiesEnd) {
|
||||
if (capabilities.contains(qsl("sound"))) {
|
||||
if (Global::SoundNotify()) {
|
||||
_hints["suppress-sound"] = true;
|
||||
} else {
|
||||
|
|
@ -131,20 +144,14 @@ NotificationData::NotificationData(
|
|||
}
|
||||
}
|
||||
|
||||
if (ranges::find(capabilities, qsl("x-canonical-append"))
|
||||
!= capabilitiesEnd) {
|
||||
if (capabilities.contains(qsl("x-canonical-append"))) {
|
||||
_hints["x-canonical-append"] = qsl("true");
|
||||
}
|
||||
|
||||
_hints["category"] = qsl("im.received");
|
||||
|
||||
#ifdef TDESKTOP_LAUNCHER_FILENAME
|
||||
_hints["desktop-entry"] =
|
||||
qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_FILENAME))
|
||||
.remove(QRegExp(qsl("\\.desktop$"), Qt::CaseInsensitive));
|
||||
#else
|
||||
_hints["desktop-entry"] = qsl("kotatogramdesktop");
|
||||
#endif
|
||||
qsl(MACRO_TO_STRING(TDESKTOP_LAUNCHER_BASENAME));
|
||||
|
||||
connect(_notificationInterface.get(),
|
||||
SIGNAL(NotificationClosed(uint, uint)),
|
||||
|
|
@ -238,6 +245,15 @@ void NotificationData::notificationClicked(uint id) {
|
|||
}
|
||||
}
|
||||
|
||||
void NotificationData::notificationReplied(uint id, const QString &text) {
|
||||
if (id == _notificationId) {
|
||||
const auto manager = _manager;
|
||||
crl::on_main(manager, [=] {
|
||||
manager->notificationReplied(_peerId, _msgId, { text, {} });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QDBusArgument &operator<<(QDBusArgument &argument,
|
||||
const NotificationData::ImageData &imageData) {
|
||||
argument.beginStructure();
|
||||
|
|
@ -265,24 +281,32 @@ const QDBusArgument &operator>>(const QDBusArgument &argument,
|
|||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Supported() {
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
static auto Available = QDBusInterface(
|
||||
str_const_toString(kService),
|
||||
str_const_toString(kObjectPath),
|
||||
str_const_toString(kInterface)).isValid();
|
||||
|
||||
return Available;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<Window::Notifications::Manager> Create(
|
||||
Window::Notifications::System *system) {
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
if (Global::NativeNotifications() && Supported()) {
|
||||
return std::make_unique<Manager>(system);
|
||||
}
|
||||
#endif
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
Manager::Private::Private(Manager *manager, Type type)
|
||||
: _cachedUserpics(type)
|
||||
, _manager(manager)
|
||||
|
|
@ -303,14 +327,7 @@ Manager::Private::Private(Manager *manager, Type type)
|
|||
}
|
||||
|
||||
if (!capabilities.empty()) {
|
||||
const auto capabilitiesString = std::accumulate(
|
||||
capabilities.begin(),
|
||||
capabilities.end(),
|
||||
QString{},
|
||||
[](auto &s, auto &p) {
|
||||
return s + (p + qstr(", "));
|
||||
}).chopped(2);
|
||||
|
||||
const auto capabilitiesString = capabilities.join(", ");
|
||||
LOG(("Notification daemon capabilities: %1").arg(capabilitiesString));
|
||||
}
|
||||
}
|
||||
|
|
@ -431,6 +448,7 @@ void Manager::doClearAllFast() {
|
|||
void Manager::doClearFromHistory(not_null<History*> history) {
|
||||
_private->clearFromHistory(history);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace Notifications
|
||||
} // namespace Platform
|
||||
|
|
|
|||