diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f79ceb35bb..0c88d5d498 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -231,6 +231,8 @@ PRIVATE api/api_peer_colors.h api/api_peer_photo.cpp api/api_peer_photo.h + api/api_peer_search.cpp + api/api_peer_search.h api/api_polls.cpp api/api_polls.h api/api_premium.cpp @@ -764,6 +766,8 @@ PRIVATE dialogs/dialogs_main_list.h dialogs/dialogs_pinned_list.cpp dialogs/dialogs_pinned_list.h + dialogs/dialogs_quick_action.cpp + dialogs/dialogs_quick_action.h dialogs/dialogs_row.cpp dialogs/dialogs_row.h dialogs/dialogs_search_from_controllers.cpp @@ -1001,6 +1005,8 @@ PRIVATE history/history_unread_things.h history/history_view_highlight_manager.cpp history/history_view_highlight_manager.h + history/history_view_swipe_back_session.cpp + history/history_view_swipe_back_session.h history/history_widget.cpp history/history_widget.h info/bot/earn/info_bot_earn_list.cpp @@ -2143,14 +2149,16 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED) configure_file("../lib/xdg/com.ayugram.desktop.metainfo.xml" "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.metainfo.xml" @ONLY) generate_appdata_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.metainfo.xml") 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 "ayugram.png") - install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "ayugram.png") - install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "ayugram.png") - install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "ayugram.png") - install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "ayugram.png") - install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "ayugram.png") - install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "ayugram.png") - install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "ayugram-symbolic.svg") + install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "com.ayugram.desktop.png") + install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "com.ayugram.desktop.png") + install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "com.ayugram.desktop.png") + install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "com.ayugram.desktop.png") + install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "com.ayugram.desktop.png") + install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "com.ayugram.desktop.png") + install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "com.ayugram.desktop.png") + install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-symbolic.svg") + install(FILES "Resources/icons/tray_monochrome_attention.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-attention-symbolic.svg") + install(FILES "Resources/icons/tray_monochrome_mute.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-mute-symbolic.svg") install(FILES "../lib/xdg/com.ayugram.desktop.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.service" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.metainfo.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") diff --git a/Telegram/Resources/animations/media_forbidden.tgs b/Telegram/Resources/animations/media_forbidden.tgs new file mode 100644 index 0000000000..b1846cd5db --- /dev/null +++ b/Telegram/Resources/animations/media_forbidden.tgs @@ -0,0 +1 @@ +{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"_051_GHSTBST_OUT","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"mouth","parent":4,"sr":1,"ks":{"p":{"a":0,"k":[0.333,12.11,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,-0.528],[7.067,0],[0,0.528],[-7.067,0]],"o":[[0,0.528],[-7.067,0],[0,-0.528],[7.067,0]],"v":[[33.711,-12.993],[-3.656,-7.631],[-37.522,-9.604],[-3.656,-9.545]],"c":true}]},{"i":{"x":0.677,"y":1},"o":{"x":1,"y":0},"t":74,"s":[{"i":[[-8.771,-8.472],[15.699,0],[-13.434,18.726],[-15.699,0]],"o":[[16.228,15.674],[-15.699,0],[6.52,-9.088],[15.699,0]],"v":[[43.825,-6.263],[-1.98,-0.006],[-47.357,-3.407],[-1.871,-13.433]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":92,"s":[{"i":[[0,-0.528],[7.067,0],[0,0.528],[-7.067,0]],"o":[[0,0.528],[-7.067,0],[0,-0.528],[7.067,0]],"v":[[33.711,-12.993],[-3.656,-12.631],[-37.522,-9.604],[-3.656,-14.545]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":106,"s":[{"i":[[0,-0.528],[7.067,0],[0,0.528],[-7.067,0]],"o":[[0,0.528],[-7.067,0],[0,-0.528],[7.067,0]],"v":[[40.66,-12.018],[6.297,-12.548],[-33.768,-8.034],[6.297,-14.462]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.7,"y":0},"t":136,"s":[{"i":[[0,-0.528],[7.067,0],[0,0.528],[-7.067,0]],"o":[[0,0.528],[-7.067,0],[0,-0.528],[7.067,0]],"v":[[40.66,-12.018],[6.297,-12.548],[-33.768,-8.034],[6.297,-14.462]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":150,"s":[{"i":[[0,-0.528],[5.996,0],[0,0.528],[-5.996,0]],"o":[[0,0.528],[-5.996,0],[0,-0.528],[5.996,0]],"v":[[39.448,-12.413],[-1.678,-12.848],[-33.87,-9.837],[-1.678,-14.762]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":186,"s":[{"i":[[0,-0.528],[5.996,0],[0,0.528],[-5.996,0]],"o":[[0,0.528],[-5.996,0],[0,-0.528],[5.996,0]],"v":[[39.448,-12.413],[-1.678,-12.848],[-33.87,-9.837],[-1.678,-14.762]],"c":true}]},{"t":212,"s":[{"i":[[0,-8.033],[7.067,0],[0,8.033],[-7.067,0]],"o":[[0,8.033],[-7.067,0],[0,-8.033],[7.067,0]],"v":[[12.796,0],[0,14.546],[-12.796,0],[0,-14.546]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.490196079016,0.035294119269,0.035294119269,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.490196079016,0.035294119269,0.035294119269,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"beak_bl","parent":4,"sr":1,"ks":{"o":{"a":0,"k":33},"p":{"a":0,"k":[-7.949,-24.179,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,0],[-4.412,4.147]],"o":[[5.678,-4.69],[0,0]],"v":[[-7.943,6.682],[3.424,-6.971]],"c":false}]},{"i":{"x":0.677,"y":1},"o":{"x":1,"y":0},"t":74,"s":[{"i":[[0,0],[-4.403,5.549]],"o":[[6.446,-4.392],[0,0]],"v":[[-10.628,6.975],[1.455,-6.971]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":92,"s":[{"i":[[0,0],[-4.403,5.549]],"o":[[6.446,-4.392],[0,0]],"v":[[-7.674,6.721],[3.424,-6.971]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":106,"s":[{"i":[[0,0],[-4.264,5.647]],"o":[[5.641,-5.851],[0,0]],"v":[[-8.051,8.282],[3.424,-6.971]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.7,"y":0},"t":136,"s":[{"i":[[0,0],[-4.264,5.647]],"o":[[5.641,-5.851],[0,0]],"v":[[-8.051,8.282],[3.424,-6.971]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":150,"s":[{"i":[[0,0],[-0.373,5.044]],"o":[[5.523,-2.968],[0,0]],"v":[[-5.282,12.038],[3.424,-6.971]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":186,"s":[{"i":[[0,0],[-0.373,5.044]],"o":[[5.523,-2.968],[0,0]],"v":[[-5.282,12.038],[3.424,-6.971]],"c":false}]},{"t":212,"s":[{"i":[[0,0],[-4.412,4.147]],"o":[[1.964,-7.603],[0,0]],"v":[[-5.443,10.182],[3.424,-6.971]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[5]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":216,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":228,"s":[5]},{"t":240,"s":[5]}]},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":216,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":228,"s":[100]},{"t":240,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","parent":18,"sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":0,"k":1.559},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":184,"s":[-6.826,76.052,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":194.666,"s":[18.173,76.217,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":205.334,"s":[-26.826,76.142,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":218.666,"s":[8.174,76.142,0],"to":[0,0,0],"ti":[0,0,0]},{"t":240,"s":[-6.826,76.052,0]}]},"a":{"a":0,"k":[60,60,0]}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"beak","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":60,"s":[-8.95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":74,"s":[-1.559]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":92,"s":[-1.559]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":106,"s":[-13.63]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":136,"s":[-13.63]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":150,"s":[-8.299]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":186,"s":[-8.299]},{"t":212,"s":[-1.559]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.789,"y":0},"t":60,"s":[76.503,30.265,0],"to":[-1.303,3.086,0],"ti":[2.223,-1.998,0]},{"i":{"x":0.419,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[68.242,9.778,0],"to":[-1.808,1.625,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":74,"s":[66.953,40.91,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":92,"s":[66.953,40.91,0],"to":[0,0,0],"ti":[-30.538,1.683,0]},{"t":106,"s":[120.906,38.44,0],"h":1},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[120.906,38.44,0],"to":[0,0,0],"ti":[38.176,-10.366,0]},{"t":150,"s":[29.143,75.951,0],"h":1},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":186,"s":[29.143,75.951,0],"to":[37.106,-3.862,0],"ti":[0,0,0]},{"t":212,"s":[66.953,40.91,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":67.5,"s":[110,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":76.5,"s":[90,130,100]},{"t":90,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[7.988,0.4],[16.048,-2.349],[27.593,-7.415],[-0.76,-4.01],[-8.507,-1.957],[-12.737,1.211],[-6.332,1.939],[-0.446,4.013]],"o":[[-29.32,-1.47],[-13.151,1.925],[-7.923,2.129],[0.679,3.582],[6.961,1.601],[12.334,-1.173],[8.449,-2.587],[0.684,-6.149]],"v":[[39.197,-13.901],[-6.251,-41.866],[-46.922,-6.336],[-57.964,3.906],[-44.397,12.952],[-1.355,16.043],[40.722,9.34],[53.214,-1.325]],"c":true}]},{"i":{"x":0.677,"y":1},"o":{"x":1,"y":0},"t":74,"s":[{"i":[[6.309,1.307],[16.048,-2.349],[36.607,-17.968],[-0.882,-5.709],[-5.328,-2.761],[-11.843,0.728],[-5.624,6.202],[-0.191,4.479]],"o":[[-33.997,-8.466],[-13.151,1.925],[-6.185,2.526],[0.788,5.1],[9.679,5.133],[13.616,-0.791],[4.681,-3.634],[0.292,-6.863]],"v":[[48.12,-8.237],[-6.251,-41.866],[-51.424,-6.236],[-62.689,14.421],[-53.5,27.125],[-0.365,29.968],[55.123,20.91],[62.025,8.224]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":92,"s":[{"i":[[7.988,0.4],[16.048,-2.349],[30.244,-4.765],[-0.567,-6.434],[-8.692,-0.813],[-12.737,1.211],[-6.332,1.939],[0.309,4.532]],"o":[[-29.32,-1.47],[-13.151,1.925],[-8.105,1.277],[0.507,5.748],[15.469,1.447],[12.334,-1.173],[8.449,-2.587],[-0.473,-6.944]],"v":[[39.197,-13.901],[-6.251,-41.866],[-46.922,-8.836],[-58.646,5.725],[-45.147,17.702],[-2.105,8.543],[44.722,12.09],[55.836,0.478]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":106,"s":[{"i":[[8.444,0.423],[16.048,-2.349],[30.244,-4.765],[-0.567,-6.434],[-8.692,-0.813],[-12.737,1.211],[-6.332,1.939],[4.732,3.233]],"o":[[-29.32,-1.47],[-13.151,1.925],[-8.105,1.277],[0.507,5.748],[15.469,1.447],[12.334,-1.173],[7.709,-2.361],[7.982,-3.767]],"v":[[39.197,-13.901],[-6.251,-41.866],[-46.922,-8.836],[-58.646,5.725],[-45.147,17.702],[2.145,12.293],[37.722,11.84],[43.34,0.666]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.7,"y":0},"t":136,"s":[{"i":[[8.444,0.423],[16.048,-2.349],[30.244,-4.765],[-0.567,-6.434],[-8.692,-0.813],[-12.737,1.211],[-6.332,1.939],[4.732,3.233]],"o":[[-29.32,-1.47],[-13.151,1.925],[-8.105,1.277],[0.507,5.748],[15.469,1.447],[12.334,-1.173],[7.709,-2.361],[7.982,-3.767]],"v":[[39.197,-13.901],[-6.251,-41.866],[-46.922,-8.836],[-58.646,5.725],[-45.147,17.702],[2.145,12.293],[37.722,11.84],[43.34,0.666]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":150,"s":[{"i":[[8.444,0.423],[16.048,-2.349],[30.244,-4.765],[-14.233,-1.737],[-8.352,-0.781],[-12.737,1.211],[-6.572,-0.817],[1.9,16.015]],"o":[[-29.32,-1.47],[-13.151,1.925],[-8.387,1.321],[-7.971,3.727],[15.469,1.447],[12.334,-1.173],[8.179,1.017],[-0.935,-7.882]],"v":[[35.422,-16.826],[-6.251,-41.866],[-35.806,-11.299],[-33.327,1.916],[-34.031,15.24],[2.145,12.293],[37.722,11.84],[56.931,-7.328]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":186,"s":[{"i":[[8.444,0.423],[16.048,-2.349],[30.244,-4.765],[-14.233,-1.737],[-8.352,-0.781],[-12.737,1.211],[-6.572,-0.817],[1.9,16.015]],"o":[[-29.32,-1.47],[-13.151,1.925],[-8.387,1.321],[-7.971,3.727],[15.469,1.447],[12.334,-1.173],[8.179,1.017],[-0.935,-7.882]],"v":[[35.422,-16.826],[-6.251,-41.866],[-35.806,-11.299],[-33.327,1.916],[-34.031,15.24],[2.145,12.293],[37.722,11.84],[56.931,-7.328]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":196,"s":[{"i":[[6.027,2.375],[16.048,-2.349],[19.954,-12.615],[-0.527,-5.248],[-5.678,-2.454],[-11.977,0.8],[-5.84,4.308],[0.609,9.716]],"o":[[-20.173,-7.491],[-13.151,1.925],[-5.495,3.434],[0.527,5.248],[10.548,4.58],[13.424,-0.848],[5.123,-1.836],[-0.033,-7.302]],"v":[[32.125,-10.211],[-6.251,-41.866],[-33.353,-4.497],[-38.305,11.207],[-30.361,23.484],[1.309,28.462],[33.416,19.473],[44.636,3.508]],"c":true}]},{"t":212,"s":[{"i":[[4.007,4.007],[16.048,-2.349],[11.351,-19.18],[-1.059,-5.302],[-3.442,-3.852],[-11.341,0.456],[-5.227,8.592],[-0.471,4.449]],"o":[[-12.525,-12.525],[-13.151,1.925],[-3.078,5.2],[0.946,4.737],[6.433,7.2],[14.335,-0.577],[2.568,-4.221],[0.721,-6.817]],"v":[[29.368,-4.68],[-6.251,-41.866],[-31.302,1.191],[-34.064,17.264],[-27.292,30.376],[0.611,41.98],[29.815,25.855],[34.357,12.568]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.811764717102,0.207843139768,0.007843137719,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.364705890417,0.121568627656,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"/_bl2","parent":9,"sr":1,"ks":{"p":{"a":0,"k":[-83.645,15.256,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.422,51.723],[16.715,33.851]],"c":false}]},{"t":80,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.081,8.657],[17.081,-8.657]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":82.133,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":92.268,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":102.4,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112.533,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":122.666,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132.801,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":142.934,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":153.066,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":163.199,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":173.334,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":183.467,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":193.6,"s":[5]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":203.732,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":213.867,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":224,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":234.133,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":244.266,"s":[0]},{"t":254.400390625,"s":[5]}]},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":82.133,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":92.268,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":102.4,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112.533,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":122.666,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132.801,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":142.934,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":153.066,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":163.199,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":173.334,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":183.467,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":193.6,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":203.732,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":213.867,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":224,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":234.133,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":244.266,"s":[95]},{"t":254.400390625,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":68,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"/_bl","parent":9,"sr":1,"ks":{"p":{"a":0,"k":[-96.129,22.065,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-48.709,69.279],[75.289,1.117]],"c":false}]},{"t":80,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-54.457,27.599],[54.457,-27.599]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":68,"op":300,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"/_sh","parent":9,"sr":1,"ks":{"o":{"a":0,"k":33},"p":{"a":0,"k":[11.345,21.386,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[133.255,-87.597],[-167.909,75.145]],"c":false}]},{"t":80,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[137.947,-69.913],[-137.947,69.913]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.639215707779,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":68,"op":300,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"/_l","parent":9,"sr":1,"ks":{"a":{"a":0,"k":[248.263,265.461,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[130.546,-90.927],[-169.425,71.392]],"c":false}]},{"t":80,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[144.191,-73.077],[-144.191,73.077]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.639215707779,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[262.772,294.09]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-143.63,123.931],[158.113,-36.23]],"c":false}]},{"t":80,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-144.191,73.077],[144.191,-73.077]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.639215707779,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[233.753,236.832]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false}],"ip":68,"op":300,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"/","parent":32,"sr":1,"ks":{"p":{"a":0,"k":[-7.737,9.461,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[165.769,-74.392],[-166.487,103.885],[-169.202,98.025],[163.103,-79.136]],"c":true}]},{"t":80,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[158.7,-44.448],[-129.681,101.706],[-158.7,44.448],[129.681,-101.706]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.800000011921,0,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":68,"op":300,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"eye_bl","parent":11,"sr":1,"ks":{"p":{"a":0,"k":[-5.809,-11.517,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":84,"s":[{"i":[[0.768,-4.443],[-4.443,-0.768],[-0.768,4.443],[4.443,0.768]],"o":[[-0.768,4.443],[4.443,0.768],[0.768,-4.443],[-4.443,-0.768]],"v":[[-8.044,-1.39],[-1.39,8.044],[8.044,1.39],[1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[{"i":[[0.677,-0.825],[-4.428,0.387],[-0.677,0.825],[4.428,-0.387]],"o":[[-0.677,0.825],[4.428,-0.387],[0.677,-0.825],[-4.428,0.387]],"v":[[-4.559,18.359],[2.233,19.151],[11.475,16.956],[4.684,16.163]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96,"s":[{"i":[[0.768,-4.443],[-4.443,-0.768],[-0.768,4.443],[4.443,0.768]],"o":[[-0.768,4.443],[4.443,0.768],[0.768,-4.443],[-4.443,-0.768]],"v":[[-8.044,-1.39],[-1.39,8.044],[8.044,1.39],[1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":128,"s":[{"i":[[0.768,-4.443],[-4.443,-0.768],[-0.768,4.443],[4.443,0.768]],"o":[[-0.768,4.443],[4.443,0.768],[0.768,-4.443],[-4.443,-0.768]],"v":[[-8.044,-1.39],[-1.39,8.044],[8.044,1.39],[1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":134,"s":[{"i":[[0.677,-0.825],[-4.428,0.387],[-0.677,0.825],[4.428,-0.387]],"o":[[-0.677,0.825],[4.428,-0.387],[0.677,-0.825],[-4.428,0.387]],"v":[[-4.559,18.359],[2.233,19.151],[11.475,16.956],[4.684,16.163]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":140,"s":[{"i":[[0.768,-4.443],[-4.443,-0.768],[-0.768,4.443],[4.443,0.768]],"o":[[-0.768,4.443],[4.443,0.768],[0.768,-4.443],[-4.443,-0.768]],"v":[[-8.044,-1.39],[-1.39,8.044],[8.044,1.39],[1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":178,"s":[{"i":[[0.768,-4.443],[-4.443,-0.768],[-0.768,4.443],[4.443,0.768]],"o":[[-0.768,4.443],[4.443,0.768],[0.768,-4.443],[-4.443,-0.768]],"v":[[-8.044,-1.39],[-1.39,8.044],[8.044,1.39],[1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":184,"s":[{"i":[[0.677,-0.825],[-4.428,0.387],[-0.677,0.825],[4.428,-0.387]],"o":[[-0.677,0.825],[4.428,-0.387],[0.677,-0.825],[-4.428,0.387]],"v":[[-4.559,18.359],[2.233,19.151],[11.475,16.956],[4.684,16.163]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":196,"s":[{"i":[[0.783,-0.725],[-4.439,-0.22],[-0.783,0.725],[4.439,0.22]],"o":[[-0.783,0.725],[4.439,0.22],[0.783,-0.725],[-4.439,-0.22]],"v":[[-4.792,11.216],[1.828,12.927],[11.284,12.012],[4.664,10.301]],"c":true}]},{"t":202,"s":[{"i":[[0.768,-4.443],[-4.443,-0.768],[-0.768,4.443],[4.443,0.768]],"o":[[-0.768,4.443],[4.443,0.768],[0.768,-4.443],[-4.443,-0.768]],"v":[[-8.044,-1.39],[-1.39,8.044],[8.044,1.39],[1.39,-8.044]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":84,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":90,"s":[0,0,0,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":96,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":128,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":134,"s":[0,0,0,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":140,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":178,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":184,"s":[0,0,0,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":196,"s":[0,0,0,1]},{"t":202,"s":[1,1,1,1]}]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"eye","parent":4,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":92,"s":[66.885,-49.27,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":106,"s":[51.584,-48.916,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[51.584,-48.916,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":150,"s":[73.565,-49.592,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":186,"s":[73.565,-49.592,0],"to":[0,0,0],"ti":[0,0,0]},{"t":212,"s":[66.885,-49.27,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":92,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":106,"s":[80,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":136,"s":[80,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":150,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":186,"s":[100,100,100]},{"t":212,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":84,"s":[{"i":[[-2.9,-14.385],[-12.613,2.543],[2.9,14.385],[12.613,-2.543]],"o":[[2.9,14.385],[12.613,-2.543],[-2.9,-14.385],[-12.613,2.543]],"v":[[-22.837,4.604],[5.251,26.046],[22.837,-4.604],[-5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[{"i":[[0.328,-1.751],[-12.613,2.543],[1.876,1.044],[12.613,-2.543]],"o":[[-0.59,3.146],[12.613,-2.543],[-1.858,-1.033],[-12.613,2.543]],"v":[[-22.695,6.615],[3.352,7.793],[24.513,-2.653],[2.167,3.644]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96,"s":[{"i":[[-2.9,-14.385],[-12.613,2.543],[2.9,14.385],[12.613,-2.543]],"o":[[2.9,14.385],[12.613,-2.543],[-2.9,-14.385],[-12.613,2.543]],"v":[[-22.837,4.604],[5.251,26.046],[22.837,-4.604],[-5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":128,"s":[{"i":[[-2.9,-14.385],[-12.613,2.543],[2.9,14.385],[12.613,-2.543]],"o":[[2.9,14.385],[12.613,-2.543],[-2.9,-14.385],[-12.613,2.543]],"v":[[-22.837,4.604],[5.251,26.046],[22.837,-4.604],[-5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":134,"s":[{"i":[[0.328,-1.751],[-12.613,2.543],[1.876,1.044],[12.613,-2.543]],"o":[[-0.59,3.146],[12.613,-2.543],[-1.858,-1.033],[-12.613,2.543]],"v":[[-22.695,6.615],[3.352,7.793],[24.513,-2.653],[2.167,3.644]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":140,"s":[{"i":[[-2.9,-14.385],[-12.613,2.543],[2.9,14.385],[12.613,-2.543]],"o":[[2.9,14.385],[12.613,-2.543],[-2.9,-14.385],[-12.613,2.543]],"v":[[-22.837,4.604],[5.251,26.046],[22.837,-4.604],[-5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":178,"s":[{"i":[[-2.9,-14.385],[-12.613,2.543],[2.9,14.385],[12.613,-2.543]],"o":[[2.9,14.385],[12.613,-2.543],[-2.9,-14.385],[-12.613,2.543]],"v":[[-22.837,4.604],[5.251,26.046],[22.837,-4.604],[-5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":184,"s":[{"i":[[0.328,-1.751],[-12.613,2.543],[1.876,1.044],[12.613,-2.543]],"o":[[-0.59,3.146],[12.613,-2.543],[-1.858,-1.033],[-12.613,2.543]],"v":[[-22.695,6.615],[3.352,7.793],[24.513,-2.653],[2.167,3.644]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":196,"s":[{"i":[[0.72,-1.63],[-12.844,0.763],[1.588,1.444],[13.508,-0.923]],"o":[[-1.293,2.928],[12.803,-0.761],[-1.573,-1.43],[-12.836,0.877]],"v":[[-23.438,1.208],[1.047,2.449],[23.404,-2.719],[0.841,-1.861]],"c":true}]},{"t":202,"s":[{"i":[[-2.9,-14.385],[-12.613,2.543],[2.9,14.385],[12.613,-2.543]],"o":[[2.9,14.385],[12.613,-2.543],[-2.9,-14.385],[-12.613,2.543]],"v":[[-22.837,4.604],[5.251,26.046],[22.837,-4.604],[-5.251,-26.046]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[8]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":128,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":134,"s":[8]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":140,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"t":178,"s":[0]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":184,"s":[8]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":196,"s":[8]},{"t":202,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"eyebr","parent":11,"sr":1,"ks":{"p":{"a":0,"k":[-2.814,-58.554,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[0,0],[-3.88,-4.065]],"o":[[4.779,0.417],[0,0]],"v":[[-2.884,25.451],[12.517,32.218]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":74,"s":[{"i":[[0,0],[-3.585,-0.326]],"o":[[3.585,3.585],[0,0]],"v":[[-7.497,-4.726],[7.497,4.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":84,"s":[{"i":[[0,0],[-3.585,-0.326]],"o":[[3.585,3.585],[0,0]],"v":[[-7.497,-4.726],[7.497,4.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[{"i":[[0,0],[-3.585,-0.049]],"o":[[3.585,0.541],[0,0]],"v":[[-4.497,33.3],[10.497,34.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96,"s":[{"i":[[0,0],[-3.585,-0.326]],"o":[[3.585,3.585],[0,0]],"v":[[-7.497,-4.726],[7.497,4.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":128,"s":[{"i":[[0,0],[-3.585,-0.326]],"o":[[3.585,3.585],[0,0]],"v":[[-7.497,-4.726],[7.497,4.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":134,"s":[{"i":[[0,0],[-3.585,-0.049]],"o":[[3.585,0.541],[0,0]],"v":[[-4.497,33.3],[10.497,34.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":140,"s":[{"i":[[0,0],[-3.585,-0.326]],"o":[[3.585,3.585],[0,0]],"v":[[-7.497,-4.726],[7.497,4.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":178,"s":[{"i":[[0,0],[-3.585,-0.326]],"o":[[3.585,3.585],[0,0]],"v":[[-7.497,-4.726],[7.497,4.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":184,"s":[{"i":[[0,0],[-3.585,-0.049]],"o":[[3.585,0.541],[0,0]],"v":[[-4.497,33.3],[10.497,34.726]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":196,"s":[{"i":[[0,0],[-3.585,-0.049]],"o":[[3.585,0.541],[0,0]],"v":[[-4.497,33.3],[10.497,34.726]],"c":false}]},{"t":202,"s":[{"i":[[0,0],[-3.585,-0.326]],"o":[[3.585,3.585],[0,0]],"v":[[-7.497,-4.726],[7.497,4.726]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"eye_bl","parent":14,"sr":1,"ks":{"p":{"a":0,"k":[-5.603,-8.455,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":84,"s":[{"i":[[-0.768,-4.443],[4.443,-0.768],[0.768,4.443],[-4.443,0.768]],"o":[[0.768,4.443],[-4.443,0.768],[-0.768,-4.443],[4.443,-0.768]],"v":[[8.044,-1.39],[1.39,8.044],[-8.044,1.39],[-1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[{"i":[[-0.694,-0.715],[4.428,0.375],[0.694,0.715],[-4.428,-0.375]],"o":[[0.694,0.715],[-4.428,-0.375],[-0.694,-0.715],[4.428,0.375]],"v":[[13.043,22.967],[6.281,23.583],[-2.994,21.61],[3.768,20.993]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96,"s":[{"i":[[-0.768,-4.443],[4.443,-0.768],[0.768,4.443],[-4.443,0.768]],"o":[[0.768,4.443],[-4.443,0.768],[-0.768,-4.443],[4.443,-0.768]],"v":[[8.044,-1.39],[1.39,8.044],[-8.044,1.39],[-1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":128,"s":[{"i":[[-0.768,-4.443],[4.443,-0.768],[0.768,4.443],[-4.443,0.768]],"o":[[0.768,4.443],[-4.443,0.768],[-0.768,-4.443],[4.443,-0.768]],"v":[[8.044,-1.39],[1.39,8.044],[-8.044,1.39],[-1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":134,"s":[{"i":[[-0.694,-0.715],[4.428,0.375],[0.694,0.715],[-4.428,-0.375]],"o":[[0.694,0.715],[-4.428,-0.375],[-0.694,-0.715],[4.428,0.375]],"v":[[13.043,22.967],[6.281,23.583],[-2.994,21.61],[3.768,20.993]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":140,"s":[{"i":[[-0.768,-4.443],[4.443,-0.768],[0.768,4.443],[-4.443,0.768]],"o":[[0.768,4.443],[-4.443,0.768],[-0.768,-4.443],[4.443,-0.768]],"v":[[8.044,-1.39],[1.39,8.044],[-8.044,1.39],[-1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":178,"s":[{"i":[[-0.768,-4.443],[4.443,-0.768],[0.768,4.443],[-4.443,0.768]],"o":[[0.768,4.443],[-4.443,0.768],[-0.768,-4.443],[4.443,-0.768]],"v":[[8.044,-1.39],[1.39,8.044],[-8.044,1.39],[-1.39,-8.044]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":184,"s":[{"i":[[-0.694,-0.715],[4.428,0.375],[0.694,0.715],[-4.428,-0.375]],"o":[[0.694,0.715],[-4.428,-0.375],[-0.694,-0.715],[4.428,0.375]],"v":[[13.043,22.967],[6.281,23.583],[-2.994,21.61],[3.768,20.993]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":196,"s":[{"i":[[-0.807,-0.585],[4.427,-0.393],[0.807,0.585],[-4.427,0.393]],"o":[[0.807,0.585],[-4.427,0.393],[-0.807,-0.585],[4.427,-0.393]],"v":[[13.361,18.34],[6.806,20.111],[-2.67,19.762],[3.885,17.991]],"c":true}]},{"t":202,"s":[{"i":[[-0.768,-4.443],[4.443,-0.768],[0.768,4.443],[-4.443,0.768]],"o":[[0.768,4.443],[-4.443,0.768],[-0.768,-4.443],[4.443,-0.768]],"v":[[8.044,-1.39],[1.39,8.044],[-8.044,1.39],[-1.39,-8.044]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":84,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":90,"s":[0,0,0,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":96,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":128,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":134,"s":[0,0,0,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":140,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":178,"s":[1,1,1,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":184,"s":[0,0,0,1]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":196,"s":[0,0,0,1]},{"t":202,"s":[1,1,1,1]}]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"eye","parent":4,"sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":92,"s":[-68.757,-49.27,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":106,"s":[-79.169,-47.871,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[-79.169,-47.871,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":150,"s":[-56.825,-45.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":186,"s":[-56.825,-45.23,0],"to":[0,0,0],"ti":[0,0,0]},{"t":212,"s":[-68.757,-49.27,0]}]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":92,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":106,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":136,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":150,"s":[80,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":186,"s":[80,100,100]},{"t":212,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":84,"s":[{"i":[[2.9,-14.385],[12.613,2.543],[-2.9,14.385],[-12.613,-2.543]],"o":[[-2.9,14.385],[-12.613,-2.543],[2.9,-14.385],[12.613,2.543]],"v":[[22.837,4.604],[-5.251,26.046],[-22.837,-4.604],[5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[{"i":[[-1.173,-0.668],[13.941,0.738],[-1.066,0.879],[-14.526,-0.228]],"o":[[0.855,0.567],[-14.499,-0.768],[1.181,-0.974],[15.349,0.241]],"v":[[26.489,9.254],[1.547,17.027],[-21.84,9.877],[2.114,11.919]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96,"s":[{"i":[[2.9,-14.385],[12.613,2.543],[-2.9,14.385],[-12.613,-2.543]],"o":[[-2.9,14.385],[-12.613,-2.543],[2.9,-14.385],[12.613,2.543]],"v":[[22.837,4.604],[-5.251,26.046],[-22.837,-4.604],[5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":128,"s":[{"i":[[2.9,-14.385],[12.613,2.543],[-2.9,14.385],[-12.613,-2.543]],"o":[[-2.9,14.385],[-12.613,-2.543],[2.9,-14.385],[12.613,2.543]],"v":[[22.837,4.604],[-5.251,26.046],[-22.837,-4.604],[5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":134,"s":[{"i":[[-1.173,-0.668],[13.941,0.738],[-1.066,0.879],[-14.526,-0.228]],"o":[[0.855,0.567],[-14.499,-0.768],[1.181,-0.974],[15.349,0.241]],"v":[[26.489,9.254],[1.547,17.027],[-21.84,9.877],[2.114,11.919]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":140,"s":[{"i":[[2.9,-14.385],[12.613,2.543],[-2.9,14.385],[-12.613,-2.543]],"o":[[-2.9,14.385],[-12.613,-2.543],[2.9,-14.385],[12.613,2.543]],"v":[[22.837,4.604],[-5.251,26.046],[-22.837,-4.604],[5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":178,"s":[{"i":[[2.9,-14.385],[12.613,2.543],[-2.9,14.385],[-12.613,-2.543]],"o":[[-2.9,14.385],[-12.613,-2.543],[2.9,-14.385],[12.613,2.543]],"v":[[22.837,4.604],[-5.251,26.046],[-22.837,-4.604],[5.251,-26.046]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":184,"s":[{"i":[[-1.173,-0.668],[13.941,0.738],[-1.066,0.879],[-14.526,-0.228]],"o":[[0.855,0.567],[-14.499,-0.768],[1.181,-0.974],[15.349,0.241]],"v":[[26.489,9.254],[1.547,17.027],[-21.84,9.877],[2.114,11.919]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":196,"s":[{"i":[[-1.22,-0.578],[13.956,-0.313],[-0.997,0.957],[-14.502,0.866]],"o":[[0.895,0.501],[-14.516,0.326],[1.104,-1.06],[15.324,-0.915]],"v":[[26.299,7.431],[0.816,12.526],[-21.847,11.691],[1.13,9.135]],"c":true}]},{"t":202,"s":[{"i":[[2.9,-14.385],[12.613,2.543],[-2.9,14.385],[-12.613,-2.543]],"o":[[-2.9,14.385],[-12.613,-2.543],[2.9,-14.385],[12.613,2.543]],"v":[[22.837,4.604],[-5.251,26.046],[-22.837,-4.604],[5.251,-26.046]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[8]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":128,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":134,"s":[8]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":140,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"t":178,"s":[0]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":184,"s":[8]},{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":196,"s":[8]},{"t":202,"s":[0]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"eyebr","parent":14,"sr":1,"ks":{"p":{"a":0,"k":[-6.516,-53.175,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[0,0],[-5.211,-1.201]],"o":[[5.825,-5.46],[0,0]],"v":[[-7.013,30.453],[11.642,22.829]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":74,"s":[{"i":[[0,0],[-2.608,2.934]],"o":[[8.801,-6.845],[0,0]],"v":[[-8.312,6.519],[8.312,-6.519]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":84,"s":[{"i":[[0,0],[-2.608,2.934]],"o":[[8.801,-6.845],[0,0]],"v":[[-8.312,6.519],[8.312,-6.519]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[{"i":[[0,0],[-2.608,0.339]],"o":[[8.801,-0.792],[0,0]],"v":[[-4.812,42.019],[11.812,40.511]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96,"s":[{"i":[[0,0],[-2.608,2.934]],"o":[[8.801,-6.845],[0,0]],"v":[[-8.312,6.519],[8.312,-6.519]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":128,"s":[{"i":[[0,0],[-2.608,2.934]],"o":[[8.801,-6.845],[0,0]],"v":[[-8.312,6.519],[8.312,-6.519]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":134,"s":[{"i":[[0,0],[-2.608,0.339]],"o":[[8.801,-0.792],[0,0]],"v":[[-4.812,42.019],[11.812,40.511]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":140,"s":[{"i":[[0,0],[-2.608,2.934]],"o":[[8.801,-6.845],[0,0]],"v":[[-8.312,6.519],[8.312,-6.519]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":178,"s":[{"i":[[0,0],[-2.608,2.934]],"o":[[8.801,-6.845],[0,0]],"v":[[-8.312,6.519],[8.312,-6.519]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":184,"s":[{"i":[[0,0],[-2.608,0.339]],"o":[[8.801,-0.792],[0,0]],"v":[[-4.812,42.019],[11.812,40.511]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":196,"s":[{"i":[[0,0],[-2.608,0.339]],"o":[[8.801,-0.792],[0,0]],"v":[[-4.812,42.019],[11.812,40.511]],"c":false}]},{"t":202,"s":[{"i":[[0,0],[-2.608,2.934]],"o":[[8.801,-6.845],[0,0]],"v":[[-8.312,6.519],[8.312,-6.519]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"head_bl","parent":18,"sr":1,"ks":{"p":{"a":0,"k":[108.248,-5.838,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,-10.353]],"o":[[10.353,14.335],[0,0]],"v":[[-6.371,-24.29],[6.371,24.29]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.988235294819,0.933333337307,0.129411771894,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[5]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":216,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":228,"s":[5]},{"t":240,"s":[5]}]},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":216,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":228,"s":[100]},{"t":240,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"head_bl","parent":18,"sr":1,"ks":{"p":{"a":0,"k":[-76.374,-39.35,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[7.802,-7.63],[3.918,-22.723]],"o":[[-9.941,5.192],[-17.455,17.071],[0,0]],"v":[[29.869,-40.695],[3.168,-21.261],[-29.869,40.695]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":73}},{"n":"g","nm":"gap","v":{"a":0,"k":20}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":60,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":216,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":228,"s":[5]},{"t":240,"s":[5]}]},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":60,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":216,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":228,"s":[100]},{"t":240,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"head","parent":28,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":60,"s":[-0.081]},{"t":74,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[-24.536,-43.019,0],"to":[0,0,0],"ti":[0,0,0]},{"t":74,"s":[-21.04,-46.185,0]}]},"a":{"a":0,"k":[16.675,97.867,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[7.055,29.915],[71.089,-16.765],[-14.987,-63.55],[-26.123,-12.949]],"o":[[17.827,-23.465],[-14.987,-63.55],[-71.089,16.765],[7.074,29.996],[0,0]],"v":[[111.229,68.451],[128.838,-13.461],[-29.62,-109.215],[-128.596,47.25],[-76.495,112.536]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.980392158031,0.564705908298,0.086274512112,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.835294127464,0.152941182256,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"wing","parent":28,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":60,"s":[30.551]},{"t":74,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[78.089,-46.43,0],"to":[0,0,0],"ti":[0,0,0]},{"t":74,"s":[85.442,-36.572,0]}]},"a":{"a":0,"k":[-38.5,0,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,0],[-3.186,-10.891],[31.234,-1.969]],"o":[[33.604,-18.059],[3.186,10.891],[0,0]],"v":[[-62.791,-22.947],[33.991,-4.39],[-15.725,10.233]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":74,"s":[{"i":[[0,0],[-3.186,-10.891],[32.263,-10.878]],"o":[[30.709,-3.412],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[42.907,-7.444],[-34.866,26.7]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":92,"s":[{"i":[[0,0],[-3.186,-10.891],[32.263,-10.878]],"o":[[30.709,-3.412],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[42.907,-7.444],[-34.866,26.7]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":100,"s":[{"i":[[0,0],[-3.186,-10.891],[34.926,-8.623]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[5.907,-20.444],[-10.866,28.2]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":110,"s":[{"i":[[0,0],[-3.186,-10.891],[30.926,-7.123]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[36.407,-30.444],[1.634,28.2]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":118,"s":[{"i":[[0,0],[-3.186,-10.891],[34.926,-8.623]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[5.907,-20.444],[-10.866,28.2]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":138,"s":[{"i":[[0,0],[-3.186,-10.891],[34.926,-8.623]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[5.907,-20.444],[-10.866,28.2]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":148,"s":[{"i":[[0,0],[-3.186,-10.891],[30.926,-7.123]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-38.627,-34.871],[48.759,-6.32],[-2.868,38.495]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":156,"s":[{"i":[[0,0],[-3.186,-10.891],[34.926,-8.623]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[5.907,-20.444],[-10.866,28.2]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":166,"s":[{"i":[[0,0],[-3.186,-10.891],[30.926,-7.123]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-38.627,-34.871],[37.474,-10.05],[-2.868,38.495]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":174,"s":[{"i":[[0,0],[-3.186,-10.891],[34.926,-8.623]],"o":[[24.615,33.652],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[5.907,-20.444],[-10.866,28.2]],"c":false}]},{"t":206,"s":[{"i":[[0,0],[-3.186,-10.891],[32.263,-10.878]],"o":[[30.709,-3.412],[3.186,10.891],[0,0]],"v":[[-43.056,-26.074],[42.907,-7.444],[-34.866,26.7]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.980392158031,0.564705908298,0.086274512112,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.835294127464,0.152941182256,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"wing_bl","parent":21,"sr":1,"ks":{"p":{"a":0,"k":[-18.164,4.659,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,0],[-7.962,3.64]],"o":[[10.236,-7.279],[0,0]],"v":[[7.853,-2.894],[36.676,-24.112]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":74,"s":[{"i":[[0,0],[-7.962,3.64]],"o":[[10.236,-7.279],[0,0]],"v":[[-16.037,10.236],[16.037,-10.236]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":92,"s":[{"i":[[0,0],[-7.962,3.64]],"o":[[10.236,-7.279],[0,0]],"v":[[-16.037,10.236],[16.037,-10.236]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":100,"s":[{"i":[[0,0],[-6.982,5.417]],"o":[[8.72,9.244],[0,0]],"v":[[-0.179,-9.033],[29.27,-5.606]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":110,"s":[{"i":[[0,0],[-11.698,3.051]],"o":[[9.552,1.392],[0,0]],"v":[[-13.278,-9.676],[17.485,-11.31]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":118,"s":[{"i":[[0,0],[-6.982,5.417]],"o":[[8.72,9.244],[0,0]],"v":[[-0.179,-9.033],[29.27,-5.606]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":138,"s":[{"i":[[0,0],[-6.982,5.417]],"o":[[8.72,9.244],[0,0]],"v":[[-0.179,-9.033],[29.27,-5.606]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":148,"s":[{"i":[[0,0],[-11.698,3.051]],"o":[[9.552,1.392],[0,0]],"v":[[-13.278,-9.676],[17.485,-11.31]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":156,"s":[{"i":[[0,0],[-6.982,5.417]],"o":[[8.72,9.244],[0,0]],"v":[[-0.179,-9.033],[29.27,-5.606]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":166,"s":[{"i":[[0,0],[-11.698,3.051]],"o":[[9.552,1.392],[0,0]],"v":[[-13.278,-9.676],[17.485,-11.31]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":174,"s":[{"i":[[0,0],[-6.982,5.417]],"o":[[8.72,9.244],[0,0]],"v":[[-0.179,-9.033],[29.27,-5.606]],"c":false}]},{"t":206,"s":[{"i":[[0,0],[-7.962,3.64]],"o":[[10.236,-7.279],[0,0]],"v":[[-16.037,10.236],[16.037,-10.236]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[5]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":216,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":228,"s":[5]},{"t":240,"s":[5]}]},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":84,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":216,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":228,"s":[100]},{"t":240,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"wing","parent":28,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":60,"s":[-36.067]},{"t":74,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[-122.157,-8.399,0],"to":[0,0,0],"ti":[0,0,0]},{"t":74,"s":[-112.784,-35.476,0]}]},"a":{"a":0,"k":[30.5,-12,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,0],[-2.958,-13.61],[-32.973,12.882]],"o":[[-45.283,2.228],[2.958,13.61],[0,0]],"v":[[49.746,-37.599],[-29.776,18.921],[21.241,13.628]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":74,"s":[{"i":[[0,0],[-2.958,-13.61],[-32.973,12.882]],"o":[[-38.103,24.016],[2.958,13.61],[0,0]],"v":[[13.129,-32.598],[-54.179,26.243],[54.368,16.381]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":92,"s":[{"i":[[0,0],[-2.958,-13.61],[-32.973,12.882]],"o":[[-38.103,24.016],[2.958,13.61],[0,0]],"v":[[13.129,-32.598],[-54.179,26.243],[54.368,16.381]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":100,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-1.344,54.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-26.679,-12.757],[54.368,16.381]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":110,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-6.844,30.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-43.179,-9.757],[54.368,16.381]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":118,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-1.344,54.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-26.679,-12.757],[54.368,16.381]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":138,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-1.344,54.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-26.679,-12.757],[54.368,16.381]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":148,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-6.844,30.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-43.179,-9.757],[54.368,16.381]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":156,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-1.344,54.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-26.679,-12.757],[54.368,16.381]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":166,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-6.844,30.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-43.179,-9.757],[54.368,16.381]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":174,"s":[{"i":[[0,0],[3.066,-13.586],[-57.583,27.6]],"o":[[-1.344,54.579],[-6.035,26.739],[0,0]],"v":[[13.129,-32.598],[-26.679,-12.757],[54.368,16.381]],"c":false}]},{"t":206,"s":[{"i":[[0,0],[-2.958,-13.61],[-32.973,12.882]],"o":[[-38.103,24.016],[2.958,13.61],[0,0]],"v":[[13.129,-32.598],[-54.179,26.243],[54.368,16.381]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.980392158031,0.564705908298,0.086274512112,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.835294127464,0.152941182256,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"O_bl4","parent":25,"sr":1,"ks":{"p":{"a":0,"k":[-30.789,17.913,0]},"a":{"a":0,"k":[225.211,273.913,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-2.677,2.747]],"o":[[2.871,-2.545],[0,0]],"v":[[-4.163,3.971],[4.163,-3.971]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[355.926,378.368]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-4.539,15.159]],"o":[[8.642,-12.85],[0,0]],"v":[[-9.983,21.103],[9.983,-21.103]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[387.942,331.3]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-9.945,17.23]],"o":[[4.477,-19.951],[0,0]],"v":[[-10.94,28.009],[10.94,-28.009]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[63.436,193.496]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":82.133,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":92.268,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":102.4,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112.533,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":122.666,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132.801,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":142.934,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":153.066,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":163.199,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":173.334,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":183.467,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":193.6,"s":[5]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":203.732,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":213.867,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":224,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":234.133,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":244.266,"s":[0]},{"t":254.400390625,"s":[5]}]},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":82.133,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":92.268,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":102.4,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112.533,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":122.666,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132.801,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":142.934,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":153.066,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":163.199,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":173.334,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":183.467,"s":[95]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":193.6,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":203.732,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":213.867,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":224,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":234.133,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":244.266,"s":[95]},{"t":254.400390625,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":72,"op":300,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"O_bl3","parent":25,"sr":1,"ks":{"p":{"a":0,"k":[-16.641,-12.754,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,86.122]],"o":[[86.123,0],[0,0]],"v":[[8.904,178.402],[165.093,22.215]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,29.563],[-110.621,0],[-31.544,-69.527]],"o":[[-11.531,-25.293],[0,-110.621],[81.186,0],[0,0]],"v":[[-173.753,105.162],[-191.713,22.215],[8.904,-178.402],[191.713,-60.4]],"c":false}},"nm":"Path 2","hd":false},{"ty":"st","c":{"a":0,"k":[1,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":72,"op":300,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"O_sh 2","parent":25,"sr":1,"ks":{"o":{"a":0,"k":33},"p":{"a":0,"k":[14.477,31.675,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,-86.123]],"o":[[-86.123,0],[0,0]],"v":[[-22.214,-178.402],[-178.402,-22.214]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,-31.026],[110.62,0],[34.692,28.642]],"o":[[12.629,26.251],[0,110.62],[-48.397,0],[0,0]],"v":[[158.694,-108.895],[178.402,-22.214],[-22.214,178.402],[-149.752,132.534]],"c":false}},"nm":"Path 2","hd":false},{"ty":"st","c":{"a":0,"k":[0.639215707779,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":72,"op":300,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"O 2","parent":32,"sr":1,"ks":{},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[82.394,0],[0,-82.394],[-82.394,0],[0,82.394]],"o":[[-82.394,0],[0,82.394],[82.394,0],[0,-82.394]],"v":[[-7.737,-139.727],[-156.925,9.461],[-7.737,158.649],[141.451,9.461]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-114.664],[114.664,0],[0,114.664],[-114.664,0]],"o":[[0,114.664],[-114.664,0],[0,-114.664],[114.664,0]],"v":[[199.88,9.461],[-7.737,217.078],[-215.354,9.461],[-7.737,-198.156]],"c":true}},"nm":"Path 2","hd":false},{"ty":"st","c":{"a":0,"k":[0.639215707779,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.800000011921,0,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":72,"op":300,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"body_bl","parent":28,"sr":1,"ks":{"p":{"a":0,"k":[13.403,71.866,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,0],[-12.996,10.203]],"o":[[22.355,-3.168],[0,0]],"v":[[0.585,-6.873],[82.893,-37.54]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":74,"s":[{"i":[[0,0],[-14.3,8.279]],"o":[[22.578,0],[0,0]],"v":[[-42.899,9.408],[42.899,-9.408]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":94,"s":[{"i":[[0,0],[-14.3,8.279]],"o":[[22.578,0],[0,0]],"v":[[-42.899,9.408],[42.899,-9.408]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":108,"s":[{"i":[[0,0],[-14.3,8.279]],"o":[[22.578,0],[0,0]],"v":[[-47.294,25.746],[38.503,6.93]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":118,"s":[{"i":[[0,0],[-14.3,8.279]],"o":[[22.578,0],[0,0]],"v":[[-42.899,9.408],[42.899,-9.408]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":142,"s":[{"i":[[0,0],[-14.3,8.279]],"o":[[22.578,0],[0,0]],"v":[[-42.899,9.408],[42.899,-9.408]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":158,"s":[{"i":[[0,0],[-14.3,8.279]],"o":[[22.578,0],[0,0]],"v":[[-23.208,22.695],[62.589,3.879]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":166,"s":[{"i":[[0,0],[-16.794,12.189]],"o":[[26.059,-8.878],[0,0]],"v":[[-19.971,26.91],[61.956,-9.689]],"c":false}]},{"t":204,"s":[{"i":[[0,0],[-14.3,8.279]],"o":[[22.578,0],[0,0]],"v":[[-42.899,9.408],[42.899,-9.408]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.988235294819,0.933333337307,0.129411771894,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[5]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":72,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":84,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":120,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":216,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":228,"s":[0]},{"t":240,"s":[5]}]},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":72,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":84,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":96,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":120,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":132,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":144,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":156,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":192,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":204,"s":[95]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":216,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":228,"s":[95]},{"t":240,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":27,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[255.148,296.54,0]},"a":{"a":0,"k":[60,60,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[75,75,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":72,"s":[105,105,100]},{"t":88,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"body","parent":27,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":60,"s":[10.154]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":74,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":92,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":106,"s":[4.619]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":116,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":138,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":152,"s":[-3.668]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":178,"s":[-3.668]},{"t":210,"s":[0]}]},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[59.148,100.54,0],"to":[0,0,0],"ti":[0,0,0]},{"t":74,"s":[60.148,51.54,0]}]},"a":{"a":0,"k":[-4.687,2.752,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":68,"s":[110,90,100]},{"t":76,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,0],[-15.556,-53.369],[15.033,63.171],[28.502,8.651]],"o":[[-27.016,8.344],[17.508,60.067],[-14.305,-60.113],[0,0]],"v":[[-105.574,-57.937],[-130.991,60.125],[127.469,10.691],[59.762,-82.547]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":74,"s":[{"i":[[0,0],[-34.956,-43.224],[33.715,79.992],[29.009,6.756]],"o":[[-26.408,10.105],[40.579,50.177],[-18.496,-43.883],[0,0]],"v":[[-99.606,-49.459],[-120.972,79.715],[133.703,-3.121],[22.062,-99.289]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":94,"s":[{"i":[[0,0],[-34.956,-43.224],[33.715,79.992],[29.009,6.756]],"o":[[-26.408,10.105],[40.579,50.177],[-18.496,-43.883],[0,0]],"v":[[-99.606,-49.459],[-120.972,79.715],[133.703,-3.121],[22.062,-99.289]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":108,"s":[{"i":[[0,0],[-19.531,-52.045],[-6.786,86.541],[29.009,6.756]],"o":[[-26.408,10.105],[23.865,63.592],[7.085,-90.358],[0,0]],"v":[[-99.606,-49.459],[-127.819,82.188],[130.216,19.168],[22.062,-99.289]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":118,"s":[{"i":[[0,0],[-34.956,-43.224],[33.715,79.992],[29.009,6.756]],"o":[[-26.408,10.105],[40.579,50.177],[-18.496,-43.883],[0,0]],"v":[[-99.606,-49.459],[-120.972,79.715],[133.703,-3.121],[22.062,-99.289]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":142,"s":[{"i":[[0,0],[-34.956,-43.224],[33.715,79.992],[29.009,6.756]],"o":[[-26.408,10.105],[40.579,50.177],[-18.496,-43.883],[0,0]],"v":[[-99.606,-49.459],[-120.972,79.715],[133.703,-3.121],[22.062,-99.289]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":158,"s":[{"i":[[0,0],[-19.531,-52.045],[-6.786,86.541],[29.009,6.756]],"o":[[-26.408,10.105],[23.865,63.592],[7.085,-90.358],[0,0]],"v":[[-99.606,-49.459],[-137.819,68.688],[137.216,33.169],[22.062,-99.289]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":166,"s":[{"i":[[0,0],[-35.72,-42.595],[33.715,79.992],[29.009,6.756]],"o":[[-26.408,10.105],[56.989,67.958],[-18.496,-43.883],[0,0]],"v":[[-99.606,-49.459],[-103.972,94.715],[134.703,-15.621],[22.062,-99.289]],"c":false}]},{"t":204,"s":[{"i":[[0,0],[-34.956,-43.224],[33.715,79.992],[29.009,6.756]],"o":[[-26.408,10.105],[40.579,50.177],[-18.496,-43.883],[0,0]],"v":[[-99.606,-49.459],[-120.972,79.715],[133.703,-3.121],[22.062,-99.289]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.980392158031,0.564705908298,0.086274512112,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.835294127464,0.152941182256,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"O_bl2","parent":32,"sr":1,"ks":{"p":{"a":0,"k":[-30.789,17.913,0]},"a":{"a":0,"k":[225.211,273.913,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-2.677,2.747]],"o":[[2.871,-2.545],[0,0]],"v":[[-4.163,3.971],[4.163,-3.971]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[355.926,378.368]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-4.539,15.159]],"o":[[8.642,-12.85],[0,0]],"v":[[-9.983,21.103],[9.983,-21.103]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[387.942,331.3]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-9.945,17.23]],"o":[[4.477,-19.951],[0,0]],"v":[[-10.94,28.009],[10.94,-28.009]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[63.436,193.496]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false}],"ip":0,"op":72,"st":0,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"O_bl1","parent":32,"sr":1,"ks":{"p":{"a":0,"k":[-16.641,-12.754,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,86.122]],"o":[[86.123,0],[0,0]],"v":[[8.904,178.402],[165.093,22.215]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,29.563],[-110.621,0],[-31.544,-69.527]],"o":[[-11.531,-25.293],[0,-110.621],[81.186,0],[0,0]],"v":[[-173.753,105.162],[-191.713,22.215],[8.904,-178.402],[191.713,-60.4]],"c":false}},"nm":"Path 2","hd":false},{"ty":"st","c":{"a":0,"k":[1,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":72,"st":0,"bm":0},{"ddd":0,"ind":31,"ty":4,"nm":"O_sh","parent":32,"sr":1,"ks":{"o":{"a":0,"k":33},"p":{"a":0,"k":[14.477,31.675,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,-86.123]],"o":[[-86.123,0],[0,0]],"v":[[-22.214,-178.402],[-178.402,-22.214]],"c":false}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,-31.026],[110.62,0],[34.692,28.642]],"o":[[12.629,26.251],[0,110.62],[-48.397,0],[0,0]],"v":[[158.694,-108.895],[178.402,-22.214],[-22.214,178.402],[-149.752,132.534]],"c":false}},"nm":"Path 2","hd":false},{"ty":"st","c":{"a":0,"k":[0.639215707779,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":72,"st":0,"bm":0},{"ddd":0,"ind":32,"ty":4,"nm":"O","parent":27,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.04],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[60]},{"t":72,"s":[0]}]},"p":{"a":0,"k":[60.852,19.46,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":60,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":72,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":86,"s":[94,94,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":94,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":100,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":110,"s":[108,95,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":118,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":138,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":148,"s":[108,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":156,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":166,"s":[108,95,100]},{"t":174,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[82.394,0],[0,-82.394],[-82.394,0],[0,82.394]],"o":[[-82.394,0],[0,82.394],[82.394,0],[0,-82.394]],"v":[[-7.737,-139.727],[-156.925,9.461],[-7.737,158.649],[141.451,9.461]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-114.664],[114.664,0],[0,114.664],[-114.664,0]],"o":[[0,114.664],[-114.664,0],[0,-114.664],[114.664,0]],"v":[[199.88,9.461],[-7.737,217.078],[-215.354,9.461],[-7.737,-198.156]],"c":true}},"nm":"Path 2","hd":false},{"ty":"st","c":{"a":0,"k":[0.639215707779,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.800000011921,0,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":72,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"_051_GHSTBST","refId":"comp_0","sr":1,"ks":{"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":-60,"bm":0}]} \ No newline at end of file diff --git a/Telegram/Resources/animations/swipe_action/archive.tgs b/Telegram/Resources/animations/swipe_action/archive.tgs new file mode 100644 index 0000000000..73e049e69c Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/archive.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/delete.tgs b/Telegram/Resources/animations/swipe_action/delete.tgs new file mode 100644 index 0000000000..daa3a01626 Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/delete.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/disabled.tgs b/Telegram/Resources/animations/swipe_action/disabled.tgs new file mode 100644 index 0000000000..a4fb31cb25 Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/disabled.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/mute.tgs b/Telegram/Resources/animations/swipe_action/mute.tgs new file mode 100644 index 0000000000..97d7bbf68c Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/mute.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/pin.tgs b/Telegram/Resources/animations/swipe_action/pin.tgs new file mode 100644 index 0000000000..5fbeedbc4f Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/pin.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/read.tgs b/Telegram/Resources/animations/swipe_action/read.tgs new file mode 100644 index 0000000000..59b258677c Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/read.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/unarchive.tgs b/Telegram/Resources/animations/swipe_action/unarchive.tgs new file mode 100644 index 0000000000..f8a4f2108f Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/unarchive.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/unmute.tgs b/Telegram/Resources/animations/swipe_action/unmute.tgs new file mode 100644 index 0000000000..563749f53f Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/unmute.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/unpin.tgs b/Telegram/Resources/animations/swipe_action/unpin.tgs new file mode 100644 index 0000000000..8e96e36ec9 Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/unpin.tgs differ diff --git a/Telegram/Resources/animations/swipe_action/unread.tgs b/Telegram/Resources/animations/swipe_action/unread.tgs new file mode 100644 index 0000000000..97ce96a05f Binary files /dev/null and b/Telegram/Resources/animations/swipe_action/unread.tgs differ diff --git a/Telegram/Resources/icons/chat/input_gift.png b/Telegram/Resources/icons/chat/input_gift.png new file mode 100644 index 0000000000..c1f42e5e84 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_gift.png differ diff --git a/Telegram/Resources/icons/chat/input_gift@2x.png b/Telegram/Resources/icons/chat/input_gift@2x.png new file mode 100644 index 0000000000..79bacc11a5 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_gift@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_gift@3x.png b/Telegram/Resources/icons/chat/input_gift@3x.png new file mode 100644 index 0000000000..83f17c0b99 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_gift@3x.png differ diff --git a/Telegram/Resources/icons/menu/hourglass.png b/Telegram/Resources/icons/menu/hourglass.png new file mode 100644 index 0000000000..e7389fb11a Binary files /dev/null and b/Telegram/Resources/icons/menu/hourglass.png differ diff --git a/Telegram/Resources/icons/menu/hourglass@2x.png b/Telegram/Resources/icons/menu/hourglass@2x.png new file mode 100644 index 0000000000..69a579183a Binary files /dev/null and b/Telegram/Resources/icons/menu/hourglass@2x.png differ diff --git a/Telegram/Resources/icons/menu/hourglass@3x.png b/Telegram/Resources/icons/menu/hourglass@3x.png new file mode 100644 index 0000000000..0fcfcfab9a Binary files /dev/null and b/Telegram/Resources/icons/menu/hourglass@3x.png differ diff --git a/Telegram/Resources/icons/settings/mini_gift.png b/Telegram/Resources/icons/settings/mini_gift.png new file mode 100644 index 0000000000..6e1f566065 Binary files /dev/null and b/Telegram/Resources/icons/settings/mini_gift.png differ diff --git a/Telegram/Resources/icons/settings/mini_gift@2x.png b/Telegram/Resources/icons/settings/mini_gift@2x.png new file mode 100644 index 0000000000..7a5e301e1a Binary files /dev/null and b/Telegram/Resources/icons/settings/mini_gift@2x.png differ diff --git a/Telegram/Resources/icons/settings/mini_gift@3x.png b/Telegram/Resources/icons/settings/mini_gift@3x.png new file mode 100644 index 0000000000..d34d1b6dea Binary files /dev/null and b/Telegram/Resources/icons/settings/mini_gift@3x.png differ diff --git a/Telegram/Resources/icons/tray_monochrome_attention.svg b/Telegram/Resources/icons/tray_monochrome_attention.svg new file mode 100644 index 0000000000..95b8c2cfaf --- /dev/null +++ b/Telegram/Resources/icons/tray_monochrome_attention.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Telegram/Resources/icons/tray_monochrome_mute.svg b/Telegram/Resources/icons/tray_monochrome_mute.svg new file mode 100644 index 0000000000..448f6990b2 --- /dev/null +++ b/Telegram/Resources/icons/tray_monochrome_mute.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index dfff5dd622..8f3ca87072 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -681,6 +681,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_shortcuts_archive_chat" = "Archive chat"; "lng_shortcuts_media_fullscreen" = "Toggle video fullscreen"; "lng_shortcuts_show_chat_menu" = "Show chat menu"; +"lng_shortcuts_show_chat_preview" = "Show chat preview"; "lng_settings_chat_reactions_title" = "Quick Reaction"; "lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction"; @@ -1179,6 +1180,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_restart_now" = "Restart"; "lng_settings_restart_later" = "Later"; +"lng_settings_quick_dialog_action_title" = "Chat list quick action"; +"lng_settings_quick_dialog_action_about" = "Choose the action you want to perform when you middle-click or swipe left in the chat list."; +"lng_settings_quick_dialog_action_both" = "Swipe left and Middle-click"; +"lng_settings_quick_dialog_action_swipe" = "Swipe left"; +"lng_settings_quick_dialog_action_mute" = "Mute"; +"lng_settings_quick_dialog_action_unmute" = "Unmute"; +"lng_settings_quick_dialog_action_pin" = "Pin"; +"lng_settings_quick_dialog_action_unpin" = "Unpin"; +"lng_settings_quick_dialog_action_read" = "Read"; +"lng_settings_quick_dialog_action_unread" = "Unread"; +"lng_settings_quick_dialog_action_archive" = "Archive"; +"lng_settings_quick_dialog_action_unarchive" = "Unarchive"; +"lng_settings_quick_dialog_action_delete" = "Delete"; +"lng_settings_quick_dialog_action_disabled" = "Change folder"; + +"lng_settings_generic_subscribe" = "Subscribe to {link} to use this setting."; +"lng_settings_generic_subscribe_link" = "Telegram Premium"; + "lng_sessions_header" = "This device"; "lng_sessions_other_header" = "Active Devices"; "lng_sessions_other_desc" = "You can log in to Telegram from other mobile, tablet and desktop devices, using the same phone number. All your data will be instantly synchronized."; @@ -1294,6 +1313,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_gifts_always_title" = "Always allow"; "lng_edit_privacy_gifts_never_title" = "Never allow"; +"lng_edit_privacy_gifts_types" = "Accepted Gift Types"; +"lng_edit_privacy_gifts_premium" = "Premium Subscriptions"; +"lng_edit_privacy_gifts_unlimited" = "Unlimited"; +"lng_edit_privacy_gifts_limited" = "Limited-Edition"; +"lng_edit_privacy_gifts_unique" = "Unique"; +"lng_edit_privacy_gifts_types_about" = "Choose the types of gifts that you accept."; +"lng_edit_privacy_gifts_show_icon" = "Show Gift Icon in Chats"; +"lng_edit_privacy_gifts_show_icon_about" = "Display the {emoji}Gift icon in the message input field for both participants in all chats."; +"lng_edit_privacy_gifts_restricted" = "This user doesn't accept gifts."; + "lng_edit_privacy_calls_title" = "Calls"; "lng_edit_privacy_calls_header" = "Who can call me"; "lng_edit_privacy_calls_always_empty" = "Always allow"; @@ -2173,6 +2202,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_paid_message_some#other" = "send {count} messages"; "lng_action_paid_message_got#one" = "You received {count} Star from {name}"; "lng_action_paid_message_got#other" = "You received {count} Stars from {name}"; +"lng_action_paid_message_refund#one" = "{from} refunded {count} Star to you"; +"lng_action_paid_message_refund#other" = "{from} refunded {count} Stars to you"; +"lng_action_paid_message_refund_self#one" = "You refunded {count} Star to {name}"; +"lng_action_paid_message_refund_self#other" = "You refunded {count} Stars to {name}"; +"lng_action_message_price_free" = "Messages are now free in this group."; +"lng_action_message_price_paid#one" = "Messages now cost {count} Star each in this group."; +"lng_action_message_price_paid#other" = "Messages now cost {count} Stars each in this group."; "lng_you_paid_stars#one" = "You paid {count} Star."; "lng_you_paid_stars#other" = "You paid {count} Stars."; @@ -2906,8 +2942,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbots_include_button" = "Select Chats"; "lng_chatbots_exclude_about" = "Select chats or entire chat categories which the bot will not have access to."; "lng_chatbots_permissions_title" = "Bot permissions"; + +"lng_chatbots_manage_messages" = "Manage Messages"; +"lng_chatbots_read" = "Read Messages"; "lng_chatbots_reply" = "Reply to Messages"; -"lng_chatbots_reply_about" = "The bot can only reply on your behalf in chats that were active during the last 24h."; +"lng_chatbots_mark_as_read" = "Mark Messages as Read"; +"lng_chatbots_delete_sent" = "Delete Sent Messages"; +"lng_chatbots_delete_received" = "Delete Received Messages"; + +"lng_chatbots_manage_profile" = "Manage Profile"; +"lng_chatbots_edit_name" = "Edit Name"; +"lng_chatbots_edit_bio" = "Edit Bio"; +"lng_chatbots_edit_userpic" = "Edit Profile Picture"; +"lng_chatbots_edit_username" = "Edit Username"; + +"lng_chatbots_manage_gifts" = "Manage Gifts and Stars"; +"lng_chatbots_view_gifts" = "View Gifts"; +"lng_chatbots_sell_gifts" = "Sell Gifts"; +"lng_chatbots_gift_settings" = "Change Gift Settings"; +"lng_chatbots_transfer_gifts" = "Transfer and Upgrade Gifts"; +"lng_chatbots_transfer_stars" = "Transfer Stars"; + +"lng_chatbots_manage_stories" = "Manage Stories"; + "lng_chatbots_remove" = "Remove Bot"; "lng_chatbots_not_found" = "Chatbot not found."; "lng_chatbots_not_supported" = "This bot doesn't support Telegram Business yet."; @@ -3342,6 +3399,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_stars_limited" = "limited"; "lng_gift_stars_sold_out" = "sold out"; "lng_gift_stars_tabs_all" = "All Gifts"; +"lng_gift_stars_tabs_my" = "My Gifts"; "lng_gift_stars_tabs_limited" = "Limited"; "lng_gift_stars_tabs_in_stock" = "In Stock"; "lng_gift_send_title" = "Send a Gift"; @@ -3371,7 +3429,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_limited_of_one" = "unique"; "lng_gift_limited_of_count" = "1 of {amount}"; "lng_gift_collectible_tag" = "gift"; -"lng_gift_price_unique" = "Unique"; "lng_gift_view_unpack" = "Unpack"; "lng_gift_anonymous_hint" = "Only you can see the sender's name."; "lng_gift_anonymous_hint_channel" = "Only admins of this channel can see the sender's name."; @@ -3428,7 +3485,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_display_done_channel" = "The gift is now shown in channel's Gifts."; "lng_gift_display_done_hide" = "The gift is now hidden from your profile page."; "lng_gift_display_done_hide_channel" = "The gift is now hidden from channel's Gifts."; +"lng_gift_pinned_done_title" = "{gift} pinned"; "lng_gift_pinned_done" = "The gift will always be shown on top."; +"lng_gift_pinned_done_replaced" = "replacing {gift}"; "lng_gift_got_stars#one" = "You got **{count} Star** for this gift."; "lng_gift_got_stars#other" = "You got **{count} Stars** for this gift."; "lng_gift_channel_got#one" = "Channel got **{count} Star** for this gift."; @@ -3501,6 +3560,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_wear_subscribe" = "Subscribe to {link} to wear collectibles."; "lng_gift_wear_start_toast" = "You put on {name}"; "lng_gift_wear_end_toast" = "You took off {name}"; +"lng_gift_many_pinned_title" = "Too Many Pinned Gifts"; +"lng_gift_many_pinned_choose" = "Select a gift to unpin below"; "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; @@ -4331,6 +4392,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_search_filter_private" = "Private chats"; "lng_search_filter_group" = "Group chats"; "lng_search_filter_channel" = "Channels"; +"lng_search_sponsored_button" = "Ad â‹®"; "lng_media_save_progress" = "{ready} of {total} {mb}"; "lng_mediaview_save_as" = "Save As..."; @@ -5745,6 +5807,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sponsored_revenued_info1_title" = "Respect Your Privacy"; "lng_sponsored_revenued_info1_description" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them."; "lng_sponsored_revenued_info1_bot_description" = "Ads on Telegram do not use your personal information and are based on the mini app in which you see them."; +"lng_sponsored_revenued_info1_search_description" = "Ads on Telegram do not use your personal information and are based on the search query you entered."; "lng_sponsored_revenued_info2_title" = "Help the Channel Creator"; "lng_sponsored_revenued_info2_bot_title" = "Help the Bot Developer"; "lng_sponsored_revenued_info2_description" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed."; @@ -5753,9 +5816,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sponsored_revenued_info3_description#one" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers."; "lng_sponsored_revenued_info3_description#other" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers."; "lng_sponsored_revenued_info3_bot_description" = "You can turn off ads in mini apps by subscribing to {link}."; +"lng_sponsored_revenued_info3_search_description" = "You can turn off ads by subscribing to Telegram Premium. {link}"; +"lng_sponsored_revenued_info3_search_link" = "Subscribe {arrow}"; "lng_sponsored_revenued_footer_title" = "Can I Launch an Ad?"; "lng_sponsored_revenued_footer_description" = "Anyone can create an ad to display in this channel — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}"; "lng_sponsored_revenued_footer_bot_description" = "Anyone can create an ad to display in this bot — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}"; +"lng_sponsored_revenued_footer_search_description" = "Anyone can create an ad to display in search results for any query. Check out the **Telegram Ad Platform** for details. {link}"; "lng_sponsored_top_bar_hide" = "remove"; "lng_telegram_features_url" = "https://t.me/TelegramTips"; @@ -6241,6 +6307,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_qr_box_transparent_background" = "Transparent Background"; "lng_qr_box_font_size" = "Font size"; +"lng_frozen_bar_title" = "Your account is frozen!"; +"lng_frozen_bar_text" = "Click to view details {arrow}"; +"lng_frozen_restrict_title" = "Your account is frozen"; +"lng_frozen_restrict_text" = "Click to view details"; +"lng_frozen_title" = "Your Account is Frozen"; +"lng_frozen_subtitle1" = "Violation of Terms"; +"lng_frozen_text1" = "Your account was frozen for breaking Telegram's Terms and Conditions."; +"lng_frozen_subtitle2" = "Read-Only Mode"; +"lng_frozen_text2" = "You can access your account but can't send messages or take actions."; +"lng_frozen_subtitle3" = "Appeal Before Deactivation"; +"lng_frozen_text3" = "Appeal via {link} before {date}, or your account will be deleted."; +"lng_frozen_appeal_button" = "Submit an Appeal"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index e66d847b6a..5feeeacbc6 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -30,6 +30,7 @@ ../../animations/noresults.tgs ../../animations/hello_status.tgs ../../animations/starref_link.tgs + ../../animations/media_forbidden.tgs ../../animations/dice/dice_idle.tgs ../../animations/dice/dart_idle.tgs @@ -49,5 +50,16 @@ ../../animations/star_reaction/effect1.tgs ../../animations/star_reaction/effect2.tgs ../../animations/star_reaction/effect3.tgs + + ../../animations/swipe_action/archive.tgs + ../../animations/swipe_action/unarchive.tgs + ../../animations/swipe_action/delete.tgs + ../../animations/swipe_action/disabled.tgs + ../../animations/swipe_action/mute.tgs + ../../animations/swipe_action/unmute.tgs + ../../animations/swipe_action/pin.tgs + ../../animations/swipe_action/unpin.tgs + ../../animations/swipe_action/read.tgs + ../../animations/swipe_action/unread.tgs diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 7ca5f84b6f..8fcea19be9 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -24,6 +24,8 @@ ../../icons/settings/star.svg ../../icons/settings/starmini.svg ../../icons/tray_monochrome.svg + ../../icons/tray_monochrome_attention.svg + ../../icons/tray_monochrome_mute.svg ../../art/topic_icons/blue.svg ../../art/topic_icons/yellow.svg ../../art/topic_icons/violet.svg diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 990e326bf2..8fa9ad8dc6 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.13.1.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 26296ddc66..e0136007f9 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,12,3,0 - PRODUCTVERSION 5,12,3,0 + FILEVERSION 5,13,1,0 + PRODUCTVERSION 5,13,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "5.12.3.0" + VALUE "FileVersion", "5.13.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.12.3.0" + VALUE "ProductVersion", "5.13.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index a52f9c8416..cb637e054d 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,12,3,0 - PRODUCTVERSION 5,12,3,0 + FILEVERSION 5,13,1,0 + PRODUCTVERSION 5,13,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "5.12.3.0" + VALUE "FileVersion", "5.13.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.12.3.0" + VALUE "ProductVersion", "5.13.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_confirm_phone.cpp b/Telegram/SourceFiles/api/api_confirm_phone.cpp index 3a42a793df..a3b41fefc9 100644 --- a/Telegram/SourceFiles/api/api_confirm_phone.cpp +++ b/Telegram/SourceFiles/api/api_confirm_phone.cpp @@ -150,6 +150,9 @@ void ConfirmPhone::resolve( }, [](const MTPDauth_sentCodeSuccess &) { LOG(("API Error: Unexpected auth.sentCodeSuccess " "(Api::ConfirmPhone).")); + }, [](const MTPDauth_sentCodePaymentRequired &) { + LOG(("API Error: Unexpected auth.sentCodePaymentRequired " + "(Api::ConfirmPhone).")); }); }).fail([=](const MTP::Error &error) { _sendRequestId = 0; diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index 90c56ae4e5..2a2def3c5a 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_global_privacy.h" #include "apiwrap.h" +#include "data/data_user.h" #include "main/main_session.h" #include "main/main_app_config.h" @@ -115,7 +116,8 @@ void GlobalPrivacy::updateHideReadTime(bool hide) { unarchiveOnNewMessageCurrent(), hide, newRequirePremiumCurrent(), - newChargeStarsCurrent()); + newChargeStarsCurrent(), + disallowedGiftTypesCurrent()); } bool GlobalPrivacy::hideReadTimeCurrent() const { @@ -150,7 +152,27 @@ void GlobalPrivacy::updateMessagesPrivacy( unarchiveOnNewMessageCurrent(), hideReadTimeCurrent(), requirePremium, - chargeStars); + chargeStars, + disallowedGiftTypesCurrent()); +} + +DisallowedGiftTypes GlobalPrivacy::disallowedGiftTypesCurrent() const { + return _disallowedGiftTypes.current(); +} + +auto GlobalPrivacy::disallowedGiftTypes() const + -> rpl::producer { + return _disallowedGiftTypes.value(); +} + +void GlobalPrivacy::updateDisallowedGiftTypes(DisallowedGiftTypes types) { + update( + archiveAndMuteCurrent(), + unarchiveOnNewMessageCurrent(), + hideReadTimeCurrent(), + newRequirePremiumCurrent(), + newChargeStarsCurrent(), + types); } void GlobalPrivacy::loadPaidReactionShownPeer() { @@ -182,7 +204,8 @@ void GlobalPrivacy::updateArchiveAndMute(bool value) { unarchiveOnNewMessageCurrent(), hideReadTimeCurrent(), newRequirePremiumCurrent(), - newChargeStarsCurrent()); + newChargeStarsCurrent(), + disallowedGiftTypesCurrent()); } void GlobalPrivacy::updateUnarchiveOnNewMessage( @@ -192,7 +215,8 @@ void GlobalPrivacy::updateUnarchiveOnNewMessage( value, hideReadTimeCurrent(), newRequirePremiumCurrent(), - newChargeStarsCurrent()); + newChargeStarsCurrent(), + disallowedGiftTypesCurrent()); } void GlobalPrivacy::update( @@ -200,12 +224,16 @@ void GlobalPrivacy::update( UnarchiveOnNewMessage unarchiveOnNewMessage, bool hideReadTime, bool newRequirePremium, - int newChargeStars) { + int newChargeStars, + DisallowedGiftTypes disallowedGiftTypes) { using Flag = MTPDglobalPrivacySettings::Flag; + using DisallowedFlag = MTPDdisallowedGiftsSettings::Flag; _api.request(_requestId).cancel(); const auto newRequirePremiumAllowed = _session->premium() || _session->appConfig().newRequirePremiumFree(); + const auto showGiftIcon + = (disallowedGiftTypes & DisallowedGiftType::SendHide); const auto flags = Flag() | (archiveAndMute ? Flag::f_archive_and_mute_new_noncontact_peers @@ -220,14 +248,35 @@ void GlobalPrivacy::update( | ((newRequirePremium && newRequirePremiumAllowed) ? Flag::f_new_noncontact_peers_require_premium : Flag()) - | Flag::f_noncontact_peers_paid_stars; + | Flag::f_noncontact_peers_paid_stars + | (showGiftIcon ? Flag::f_display_gifts_button : Flag()) + | Flag::f_disallowed_gifts; + const auto disallowedFlags = DisallowedFlag() + | ((disallowedGiftTypes & DisallowedGiftType::Premium) + ? DisallowedFlag::f_disallow_premium_gifts + : DisallowedFlag()) + | ((disallowedGiftTypes & DisallowedGiftType::Unlimited) + ? DisallowedFlag::f_disallow_unlimited_stargifts + : DisallowedFlag()) + | ((disallowedGiftTypes & DisallowedGiftType::Limited) + ? DisallowedFlag::f_disallow_limited_stargifts + : DisallowedFlag()) + | ((disallowedGiftTypes & DisallowedGiftType::Unique) + ? DisallowedFlag::f_disallow_unique_stargifts + : DisallowedFlag()); + const auto typesWas = _disallowedGiftTypes.current(); + const auto typesChanged = (typesWas != disallowedGiftTypes); _requestId = _api.request(MTPaccount_SetGlobalPrivacySettings( MTP_globalPrivacySettings( MTP_flags(flags), - MTP_long(newChargeStars)) + MTP_long(newChargeStars), + MTP_disallowedGiftsSettings(MTP_flags(disallowedFlags))) )).done([=](const MTPGlobalPrivacySettings &result) { _requestId = 0; apply(result); + if (typesChanged) { + _session->user()->updateFullForced(); + } }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) { @@ -236,7 +285,8 @@ void GlobalPrivacy::update( unarchiveOnNewMessage, hideReadTime, false, - 0); + 0, + DisallowedGiftTypes()); } }).send(); _archiveAndMute = archiveAndMute; @@ -244,6 +294,7 @@ void GlobalPrivacy::update( _hideReadTime = hideReadTime; _newRequirePremium = newRequirePremium; _newChargeStars = newChargeStars; + _disallowedGiftTypes = disallowedGiftTypes; } void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) { @@ -257,6 +308,29 @@ void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) { _hideReadTime = data.is_hide_read_marks(); _newRequirePremium = data.is_new_noncontact_peers_require_premium(); _newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty(); + if (const auto gifts = data.vdisallowed_gifts()) { + const auto &disallow = gifts->data(); + _disallowedGiftTypes = DisallowedGiftType() + | (disallow.is_disallow_unlimited_stargifts() + ? DisallowedGiftType::Unlimited + : DisallowedGiftType()) + | (disallow.is_disallow_limited_stargifts() + ? DisallowedGiftType::Limited + : DisallowedGiftType()) + | (disallow.is_disallow_unique_stargifts() + ? DisallowedGiftType::Unique + : DisallowedGiftType()) + | (disallow.is_disallow_premium_gifts() + ? DisallowedGiftType::Premium + : DisallowedGiftType()) + | (data.is_display_gifts_button() + ? DisallowedGiftType::SendHide + : DisallowedGiftType()); + } else { + _disallowedGiftTypes = data.is_display_gifts_button() + ? DisallowedGiftType::SendHide + : DisallowedGiftType(); + } } } // namespace Api diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index 6b0fc064b3..7346f2ff00 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/flags.h" #include "mtproto/sender.h" class ApiWrap; @@ -23,6 +24,17 @@ enum class UnarchiveOnNewMessage { AnyUnmuted, }; +enum class DisallowedGiftType : uchar { + Limited = 0x01, + Unlimited = 0x02, + Unique = 0x04, + Premium = 0x08, + SendHide = 0x10, +}; +inline constexpr bool is_flag_type(DisallowedGiftType) { return true; } + +using DisallowedGiftTypes = base::flags; + [[nodiscard]] PeerId ParsePaidReactionShownPeer( not_null session, const MTPPaidReactionPrivacy &value); @@ -57,6 +69,11 @@ public: void updateMessagesPrivacy(bool requirePremium, int chargeStars); + [[nodiscard]] DisallowedGiftTypes disallowedGiftTypesCurrent() const; + [[nodiscard]] auto disallowedGiftTypes() const + -> rpl::producer; + void updateDisallowedGiftTypes(DisallowedGiftTypes types); + void loadPaidReactionShownPeer(); void updatePaidReactionShownPeer(PeerId shownPeer); [[nodiscard]] PeerId paidReactionShownPeerCurrent() const; @@ -70,7 +87,8 @@ private: UnarchiveOnNewMessage unarchiveOnNewMessage, bool hideReadTime, bool newRequirePremium, - int newChargeStars); + int newChargeStars, + DisallowedGiftTypes disallowedGiftTypes); const not_null _session; MTP::Sender _api; @@ -82,6 +100,7 @@ private: rpl::variable _hideReadTime = false; rpl::variable _newRequirePremium = false; rpl::variable _newChargeStars = 0; + rpl::variable _disallowedGiftTypes; rpl::variable _paidReactionShownPeer = false; std::vector> _callbacks; bool _paidReactionShownPeerLoaded = false; diff --git a/Telegram/SourceFiles/api/api_peer_search.cpp b/Telegram/SourceFiles/api/api_peer_search.cpp new file mode 100644 index 0000000000..789334681e --- /dev/null +++ b/Telegram/SourceFiles/api/api_peer_search.cpp @@ -0,0 +1,170 @@ +/* +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 "api/api_peer_search.h" + +#include "api/api_single_message_search.h" +#include "apiwrap.h" +#include "data/data_session.h" +#include "dialogs/ui/chat_search_in.h" // IsHashOrCashtagSearchQuery +#include "main/main_session.h" + +namespace Api { +namespace { + +constexpr auto kMinSponsoredQueryLength = 4; + +} // namespace + +PeerSearch::PeerSearch(not_null session, Type type) +: _session(session) +, _type(type) { +} + +PeerSearch::~PeerSearch() { + clear(); +} + +void PeerSearch::request( + const QString &query, + Fn callback, + RequestType type) { + using namespace Dialogs; + _query = Api::ConvertPeerSearchQuery(query); + _callback = callback; + if (_query.isEmpty() + || IsHashOrCashtagSearchQuery(_query) != HashOrCashtag::None) { + finish(PeerSearchResult{}); + return; + } + auto &cache = _cache[_query]; + if (cache.peersReady && cache.sponsoredReady) { + finish(cache.result); + return; + } else if (type == RequestType::CacheOnly) { + _callback = nullptr; + return; + } else if (cache.requested) { + return; + } + cache.requested = true; + cache.result.query = _query; + if (_query.size() < kMinSponsoredQueryLength) { + cache.sponsoredReady = true; + } else if (_type == Type::WithSponsored) { + requestSponsored(); + } + requestPeers(); +} + +void PeerSearch::requestPeers() { + const auto requestId = _session->api().request(MTPcontacts_Search( + MTP_string(_query), + MTP_int(SearchPeopleLimit) + )).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) { + const auto &data = result.data(); + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + auto parsed = PeerSearchResult(); + parsed.my.reserve(data.vmy_results().v.size()); + for (const auto &id : data.vmy_results().v) { + const auto peerId = peerFromMTP(id); + parsed.my.push_back(_session->data().peer(peerId)); + } + parsed.peers.reserve(data.vresults().v.size()); + for (const auto &id : data.vresults().v) { + const auto peerId = peerFromMTP(id); + parsed.peers.push_back(_session->data().peer(peerId)); + } + finishPeers(requestId, std::move(parsed)); + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + finishPeers(requestId, PeerSearchResult{}); + }).send(); + _peerRequests.emplace(requestId, _query); +} + +void PeerSearch::requestSponsored() { + const auto requestId = _session->api().request( + MTPcontacts_GetSponsoredPeers(MTP_string(_query)) + ).done([=]( + const MTPcontacts_SponsoredPeers &result, + mtpRequestId requestId) { + result.match([&](const MTPDcontacts_sponsoredPeersEmpty &) { + finishSponsored(requestId, PeerSearchResult{}); + }, [&](const MTPDcontacts_sponsoredPeers &data) { + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + auto parsed = PeerSearchResult(); + parsed.sponsored.reserve(data.vpeers().v.size()); + for (const auto &peer : data.vpeers().v) { + const auto &data = peer.data(); + const auto peerId = peerFromMTP(data.vpeer()); + parsed.sponsored.push_back({ + .peer = _session->data().peer(peerId), + .randomId = data.vrandom_id().v, + .sponsorInfo = TextWithEntities::Simple( + qs(data.vsponsor_info().value_or_empty())), + .additionalInfo = TextWithEntities::Simple( + qs(data.vadditional_info().value_or_empty())), + }); + } + finishSponsored(requestId, std::move(parsed)); + }); + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + finishSponsored(requestId, PeerSearchResult{}); + }).send(); + _sponsoredRequests.emplace(requestId, _query); +} + +void PeerSearch::finishPeers( + mtpRequestId requestId, + PeerSearchResult result) { + const auto query = _peerRequests.take(requestId); + Assert(query.has_value()); + + auto &cache = _cache[*query]; + cache.peersReady = true; + cache.result.my = std::move(result.my); + cache.result.peers = std::move(result.peers); + if (cache.sponsoredReady && _query == *query) { + finish(cache.result); + } +} + +void PeerSearch::finishSponsored( + mtpRequestId requestId, + PeerSearchResult result) { + const auto query = _sponsoredRequests.take(requestId); + Assert(query.has_value()); + + auto &cache = _cache[*query]; + cache.sponsoredReady = true; + cache.result.sponsored = std::move(result.sponsored); + if (cache.peersReady && _query == *query) { + finish(cache.result); + } +} + +void PeerSearch::finish(PeerSearchResult result) { + if (const auto onstack = base::take(_callback)) { + onstack(std::move(result)); + } +} + +void PeerSearch::clear() { + _query = QString(); + _callback = nullptr; + _cache.clear(); + for (const auto &[requestId, query] : base::take(_peerRequests)) { + _session->api().request(requestId).cancel(); + } + for (const auto &[requestId, query] : base::take(_sponsoredRequests)) { + _session->api().request(requestId).cancel(); + } +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_peer_search.h b/Telegram/SourceFiles/api/api_peer_search.h new file mode 100644 index 0000000000..c706429521 --- /dev/null +++ b/Telegram/SourceFiles/api/api_peer_search.h @@ -0,0 +1,76 @@ +/* +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 + +namespace Main { +class Session; +} // namespace Main + +namespace Api { + +struct SponsoredSearchResult { + not_null peer; + QByteArray randomId; + TextWithEntities sponsorInfo; + TextWithEntities additionalInfo; +}; + +struct PeerSearchResult { + QString query; + std::vector> my; + std::vector> peers; + std::vector sponsored; +}; + +class PeerSearch final { +public: + enum class Type { + WithSponsored, + JustPeers, + }; + PeerSearch(not_null session, Type type); + ~PeerSearch(); + + enum class RequestType { + CacheOnly, + CacheOrRemote, + }; + void request( + const QString &query, + Fn callback, + RequestType type = RequestType::CacheOrRemote); + void clear(); + +private: + struct CacheEntry { + PeerSearchResult result; + bool requested = false; + bool peersReady = false; + bool sponsoredReady = false; + }; + + void requestPeers(); + void requestSponsored(); + + void finish(PeerSearchResult result); + void finishPeers(mtpRequestId requestId, PeerSearchResult result); + void finishSponsored(mtpRequestId requestId, PeerSearchResult result); + + const not_null _session; + const Type _type; + + QString _query; + Fn _callback; + + base::flat_map _cache; + base::flat_map _peerRequests; + base::flat_map _sponsoredRequests; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index d7dbcd37f1..be3ead5afc 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -816,6 +816,7 @@ std::optional FromTL( .lastSaleDate = data.vlast_sale_date().value_or_empty(), .upgradable = data.vupgrade_stars().has_value(), .birthday = data.is_birthday(), + .soldOut = data.is_sold_out(), }); }, [&](const MTPDstarGiftUnique &data) { const auto total = data.vavailability_total().v; @@ -882,6 +883,7 @@ std::optional FromTL( unique->exportAt = data.vcan_export_at().value_or_empty(); } using Id = Data::SavedStarGiftId; + const auto hasUnique = parsed->unique != nullptr; return Data::SavedStarGift{ .info = std::move(*parsed), .manageId = (to->isUser() @@ -904,7 +906,7 @@ std::optional FromTL( .date = data.vdate().v, .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), - .pinned = data.is_pinned_to_top(), + .pinned = data.is_pinned_to_top() && hasUnique, .hidden = data.is_unsaved(), .mine = to->isSelf(), }; diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 93164126bd..6630c0428b 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -295,6 +295,7 @@ void FillChooseFilterMenu( contains ? &st::mediaPlayerMenuCheck : nullptr); item->setMarkedText(title.text, QString(), Core::TextContext({ .session = &history->session(), + .repaint = [raw = item.get()] { raw->update(); }, .customEmojiLoopLimit = title.isStatic ? -1 : 0, })); diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp index 247cad699a..d880ab5860 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp @@ -271,7 +271,8 @@ void DeleteMessagesBox::prepare() { appendDetails({ tr::lng_delete_for_me_chat_hint(tr::now, lt_count, count) }); - } else if (!peer->isSelf()) { + } else if (!peer->isSelf() + && (!peer->isUser() || !peer->asUser()->isInaccessible())) { if (const auto user = peer->asUser(); user && user->isBot()) { _revokeForBot = true; } diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 5c74632618..c3c92325a0 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -1054,7 +1054,7 @@ void EditMessagesPrivacyBox( tr::now, lt_count, always) - : QString(); + : tr::lng_edit_privacy_exceptions_add(tr::now); }); const auto exceptions = Settings::AddButtonWithLabel( diff --git a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp index 14e00151b4..e3cf2a2dd5 100644 --- a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp @@ -111,6 +111,9 @@ void AddBotToGroupBoxController::Start( Scope scope, const QString &token, ChatAdminRights requestedRights) { + if (controller->showFrozenError()) { + return; + } auto initBox = [=](not_null box) { box->addButton(tr::lng_cancel(), [box] { box->closeBox(); }); }; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index c630f603be..31f22a4f24 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/admin_log/history_admin_log_filter.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" +#include "data/business/data_business_chatbots.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_session.h" @@ -63,6 +64,11 @@ constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000); return std::vector>{}; } +[[nodiscard]] auto Dependencies(Data::ChatbotsPermissions) { + using Flag = Data::ChatbotsPermission; + return std::vector>{}; +} + [[nodiscard]] auto NestedRestrictionLabelsList( Data::RestrictionsSetOptions options) -> std::vector> { @@ -680,7 +686,7 @@ template checkView->setChecked(false, anim::type::instant); } else if (locked.has_value()) { if (checked != toggled) { - if (!state->toast) { + if (!state->toast && !locked->isEmpty()) { state->toast = Ui::Toast::Show(container, { .text = { *locked }, .duration = kForceDisableTooltipDuration, @@ -1490,3 +1496,20 @@ EditFlagsControl CreateEditAdminLogFilter( return result; } + +EditFlagsControl CreateEditChatbotPermissions( + QWidget *parent, + Data::ChatbotsPermissions flags) { + auto widget = object_ptr(parent); + auto descriptor = Data::ChatbotsPermissionsLabels(); + descriptor.disabledMessages.emplace( + Data::ChatbotsPermission::ViewMessages, + QString()); + auto result = CreateEditFlags( + widget.data(), + flags | Data::ChatbotsPermission::ViewMessages, + std::move(descriptor)); + result.widget = std::move(widget); + + return result; +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h index 6c4863b7bf..018b5523dc 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.h @@ -27,6 +27,11 @@ enum Flag : uint32; using Flags = base::flags; } // namespace PowerSaving +namespace Data { +enum class ChatbotsPermission; +using ChatbotsPermissions = base::flags; +} // namespace Data + template class object_ptr; @@ -120,3 +125,8 @@ using AdminRightLabel = EditFlagsLabel; AdminLog::FilterValue::Flags flags, bool isChannel ) -> EditFlagsControl; + +[[nodiscard]] auto CreateEditChatbotPermissions( + QWidget *parent, + Data::ChatbotsPermissions flags +) -> EditFlagsControl; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index bc15e90593..f02ea735c8 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_credits.h" +#include "api/api_global_privacy.h" #include "api/api_premium.h" #include "base/event_filter.h" #include "base/random.h" @@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "boxes/premium_preview_box.h" #include "boxes/send_credits_box.h" +#include "boxes/transfer_gift_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "chat_helpers/stickers_gift_box_pack.h" @@ -101,8 +103,10 @@ namespace Ui { namespace { constexpr auto kPriceTabAll = 0; -constexpr auto kPriceTabLimited = -2; constexpr auto kPriceTabInStock = -1; +constexpr auto kPriceTabLimited = -2; +constexpr auto kPriceTabMy = -3; +constexpr auto kMyGiftsPerPage = 50; constexpr auto kGiftMessageLimit = 255; constexpr auto kSentToastDuration = 3 * crl::time(1000); constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); @@ -118,6 +122,11 @@ struct PremiumGiftsDescriptor { std::shared_ptr api; }; +struct MyGiftsDescriptor { + std::vector list; + QString offset; +}; + struct GiftsDescriptor { std::vector list; std::shared_ptr api; @@ -360,6 +369,30 @@ auto GenerateGiftMedia( return Images::Round(std::move(result), mask, RectPart::FullTop); } +struct VisibleRange { + int top = 0; + int bottom = 0; + + friend inline bool operator==(VisibleRange, VisibleRange) = default; +}; +class WidgetWithRange final : public RpWidget { +public: + using RpWidget::RpWidget; + + [[nodiscard]] rpl::producer visibleRange() const { + return _visibleRange.value(); + } +private: + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override { + _visibleRange = VisibleRange{ visibleTop, visibleBottom }; + } + + rpl::variable _visibleRange; + +}; + void PrepareImage( QImage &image, not_null emoji, @@ -673,7 +706,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { } ranges::sort(list, ranges::less(), &GiftTypePremium::months); auto &map = Map[session]; - if (map.last.list != list) { + if (map.last.list != list || list.empty()) { map.last = PremiumGiftsDescriptor{ std::move(list), api, @@ -686,6 +719,24 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { }; } +[[nodiscard]] bool AllowedToSend( + const GiftTypeStars &gift, + not_null peer) { + using Type = Api::DisallowedGiftType; + const auto user = peer->asUser(); + if (!user || user->isSelf()) { + return true; + } + const auto disallowedTypes = user ? user->disallowedGiftTypes() : Type(); + const auto allowLimited = !(disallowedTypes & Type::Limited); + const auto allowUnlimited = !(disallowedTypes & Type::Unlimited); + const auto allowUnique = !(disallowedTypes & Type::Unique); + if (!gift.info.limitedCount) { + return allowUnlimited; + } + return allowLimited || (gift.info.starsToUpgrade && allowUnique); +} + [[nodiscard]] rpl::producer> GiftsStars( not_null session, not_null peer) { @@ -694,6 +745,12 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { }; static auto Map = base::flat_map, Session>(); + const auto filtered = [=](std::vector list) { + list.erase(ranges::remove_if(list, [&](const GiftTypeStars &gift) { + return !AllowedToSend(gift, peer); + }), end(list)); + return list; + }; return [=](auto consumer) { auto lifetime = rpl::lifetime(); @@ -703,7 +760,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { session->lifetime().add([=] { Map.remove(session); }); } if (!i->second.last.empty()) { - consumer.put_next_copy(i->second.last); + consumer.put_next(filtered(i->second.last)); } using namespace Api; @@ -718,25 +775,14 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { for (auto &gift : gifts) { list.push_back({ .info = gift }); } - ranges::sort(list, []( - const GiftTypeStars &a, - const GiftTypeStars &b) { - if (!a.info.limitedCount && !b.info.limitedCount) { - return a.info.stars <= b.info.stars; - } else if (!a.info.limitedCount) { - return true; - } else if (!b.info.limitedCount) { - return false; - } else if (a.info.limitedLeft != b.info.limitedLeft) { - return a.info.limitedLeft > b.info.limitedLeft; - } - return a.info.stars <= b.info.stars; + ranges::stable_sort(list, [](const auto &a, const auto &b) { + return a.info.soldOut < b.info.soldOut; }); auto &map = Map[session]; - if (map.last != list) { + if (map.last != list || list.empty()) { map.last = list; - consumer.put_next_copy(list); + consumer.put_next(filtered(std::move(list))); } }, lifetime); @@ -744,6 +790,48 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { }; } +[[nodiscard]] rpl::producer UniqueGiftsSlice( + not_null session, + QString offset = QString()) { + return [=](auto consumer) { + using Flag = MTPpayments_GetSavedStarGifts::Flag; + const auto user = session->user(); + const auto requestId = session->api().request( + MTPpayments_GetSavedStarGifts( + MTP_flags(Flag::f_exclude_limited | Flag::f_exclude_unlimited), + user->input, + MTP_string(offset), + MTP_int(kMyGiftsPerPage) + )).done([=](const MTPpayments_SavedStarGifts &result) { + auto gifts = MyGiftsDescriptor(); + const auto &data = result.data(); + if (const auto next = data.vnext_offset()) { + gifts.offset = qs(*next); + } + + const auto owner = &session->data(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + + gifts.list.reserve(data.vgifts().v.size()); + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(user, gift)) { + gifts.list.push_back(std::move(*parsed)); + } + } + consumer.put_next(std::move(gifts)); + consumer.put_done(); + }).fail([=] { + consumer.put_next({}); + consumer.put_done(); + }).send(); + + auto lifetime = rpl::lifetime(); + lifetime.add([=] { session->api().request(requestId).cancel(); }); + return lifetime; + }; +} + [[nodiscard]] Text::String TabTextForPrice( not_null session, int price) { @@ -752,6 +840,8 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { }; if (price == kPriceTabAll) { return simple(tr::lng_gift_stars_tabs_all(tr::now)); + } else if (price == kPriceTabMy) { + return simple(tr::lng_gift_stars_tabs_my(tr::now)); } else if (price == kPriceTabLimited) { return simple(tr::lng_gift_stars_tabs_limited(tr::now)); } else if (price == kPriceTabInStock) { @@ -774,7 +864,8 @@ struct GiftPriceTabs { [[nodiscard]] GiftPriceTabs MakeGiftsPriceTabs( not_null window, not_null peer, - rpl::producer> gifts) { + rpl::producer> gifts, + bool hasMyUnique) { auto widget = object_ptr((QWidget*)nullptr); const auto raw = widget.data(); @@ -798,6 +889,13 @@ struct GiftPriceTabs { int pressed = -1; int active = -1; }; + const auto user = peer->asUser(); + const auto disallowed = user + ? user->disallowedGiftTypes() + : Api::DisallowedGiftType(); + if (disallowed & Api::DisallowedGiftType::Unique) { + hasMyUnique = false; + } const auto state = raw->lifetime().make_state(); const auto scroll = [=] { return QPoint(int(base::SafeRound(state->scroll)), 0); @@ -805,25 +903,14 @@ struct GiftPriceTabs { state->prices = std::move( gifts - ) | rpl::map([](const std::vector &gifts) { + ) | rpl::map([=](const std::vector &gifts) { auto result = std::vector(); result.push_back(kPriceTabAll); - auto special = 1; - auto same = true; - auto sameKey = 0; auto hasNonSoldOut = false; auto hasSoldOut = false; auto hasLimited = false; + auto hasNonLimited = false; for (const auto &gift : gifts) { - if (same) { - const auto key = gift.info.stars - * (gift.info.limitedCount ? -1 : 1); - if (!sameKey) { - sameKey = key; - } else if (sameKey != key) { - same = false; - } - } if (IsSoldOut(gift.info)) { hasSoldOut = true; } else { @@ -831,19 +918,21 @@ struct GiftPriceTabs { } if (gift.info.limitedCount) { hasLimited = true; + } else { + hasNonLimited = true; } if (!ranges::contains(result, gift.info.stars)) { result.push_back(gift.info.stars); } } - if (same) { - return std::vector(); + if (hasMyUnique && !gifts.empty()) { + result.push_back(kPriceTabMy); } if (hasSoldOut && hasNonSoldOut) { - result.insert(begin(result) + (special++), kPriceTabInStock); + result.push_back(kPriceTabInStock); } - if (hasLimited) { - result.insert(begin(result) + (special++), kPriceTabLimited); + if (hasLimited && hasNonLimited) { + result.push_back(kPriceTabLimited); } ranges::sort(begin(result) + 1, end(result)); return result; @@ -1446,6 +1535,13 @@ void SendGiftBox( const auto limited = stars && (stars->info.limitedCount > stars->info.limitedLeft) && (stars->info.limitedLeft > 0); + const auto costToUpgrade = stars ? stars->info.starsToUpgrade : 0; + const auto user = peer->asUser(); + const auto disallowed = user + ? user->disallowedGiftTypes() + : Api::DisallowedGiftTypes(); + const auto disallowLimited = !peer->isSelf() + && (disallowed & Api::DisallowedGiftType::Limited); box->setStyle(limited ? st::giftLimitedBox : st::giftBox); box->setWidth(st::boxWideWidth); box->setTitle(tr::lng_gift_send_title()); @@ -1465,6 +1561,7 @@ void SendGiftBox( state->details = GiftDetails{ .descriptor = descriptor, .randomId = base::RandomValue(), + .upgraded = disallowLimited && (costToUpgrade > 0), }; peer->updateFull(); state->messageAllowed = peer->session().changes().peerFlagsValue( @@ -1563,13 +1660,13 @@ void SendGiftBox( session, { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); if (stars) { - const auto cost = stars->info.starsToUpgrade; - if (cost > 0 && !peer->isSelf()) { + if (costToUpgrade > 0 && !peer->isSelf() && !disallowLimited) { const auto id = stars->info.id; const auto showing = std::make_shared(); AddDivider(container); AddSkip(container); - AddUpgradeButton(container, session, cost, peer, [=](bool on) { + AddUpgradeButton(container, session, costToUpgrade, peer, [=]( + bool on) { auto now = state->details.current(); now.upgraded = on; state->details = std::move(now); @@ -1583,7 +1680,7 @@ void SendGiftBox( .stargiftId = id, .ready = [=](bool) { *showing = false; }, .peer = peer, - .cost = int(cost), + .cost = int(costToUpgrade), }); }); } else { @@ -1726,17 +1823,24 @@ void SendGiftBox( [[nodiscard]] object_ptr MakeGiftsList( not_null window, not_null peer, - rpl::producer gifts) { - auto result = object_ptr((QWidget*)nullptr); + rpl::producer gifts, + Fn loadMore) { + auto result = object_ptr((QWidget*)nullptr); const auto raw = result.data(); struct State { Delegate delegate; + std::vector order; + std::vector validated; + std::vector list; std::vector> buttons; + std::shared_ptr api; + rpl::variable visibleRange; bool sending = false; + int perRow = 1; }; const auto state = raw->lifetime().make_state(State{ - .delegate = Delegate(window, GiftButtonMode::Full), + .delegate = Delegate(&window->session(), GiftButtonMode::Full), }); const auto single = state->delegate.buttonSize(); const auto shadow = st::defaultDropdownMenu.wrap.shadow; @@ -1745,74 +1849,165 @@ void SendGiftBox( auto &packs = window->session().giftBoxStickersPacks(); packs.updated() | rpl::start_with_next([=] { for (const auto &button : state->buttons) { - button->update(); + if (const auto raw = button.get()) { + raw->update(); + } } }, raw->lifetime()); + const auto rebuild = [=] { + const auto width = st::boxWideWidth; + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + const auto range = state->visibleRange.current(); + const auto count = int(state->list.size()); + + auto &buttons = state->buttons; + if (buttons.size() < count) { + buttons.resize(count); + } + auto &validated = state->validated; + validated.resize(count); + + auto x = padding.left(); + auto y = padding.top(); + const auto perRow = state->perRow; + const auto singlew = single.width() + st::giftBoxGiftSkip.x(); + const auto singleh = single.height() + st::giftBoxGiftSkip.y(); + const auto rowFrom = std::max(range.top - y, 0) / singleh; + const auto rowTill = (std::max(range.bottom - y + st::giftBoxGiftSkip.y(), 0) + singleh - 1) + / singleh; + Assert(rowTill >= rowFrom); + const auto first = rowFrom * perRow; + const auto last = std::min(rowTill * perRow, count); + auto checkedFrom = 0; + auto checkedTill = int(buttons.size()); + const auto ensureButton = [&](int index) { + auto &button = buttons[index]; + if (!button) { + validated[index] = false; + for (; checkedFrom != first; ++checkedFrom) { + if (buttons[checkedFrom]) { + button = std::move(buttons[checkedFrom]); + break; + } + } + } + if (!button) { + for (; checkedTill != last; ) { + --checkedTill; + if (buttons[checkedTill]) { + button = std::move(buttons[checkedTill]); + break; + } + } + } + if (!button) { + button = std::make_unique( + raw, + &state->delegate); + } + if (validated[index]) { + return; + } + button->show(); + validated[index] = true; + const auto &descriptor = state->list[state->order[index]]; + button->setDescriptor(descriptor, GiftButton::Mode::Full); + button->setClickedCallback([=] { + const auto star = std::get_if(&descriptor); + if (star && star->info.unique) { + const auto done = [=] { + window->session().credits().load(true); + window->showPeerHistory(peer); + }; + ShowTransferToBox( + window, + peer, + star->info.unique, + star->transferId, + done); + } else if (star && IsSoldOut(star->info)) { + window->show(Box(SoldOutBox, window, *star)); + } else { + window->show(Box( + SendGiftBox, + window, + peer, + state->api, + descriptor)); + } + }); + button->setGeometry(QRect(QPoint(x, y), single), extend); + }; + y += rowFrom * singleh; + for (auto row = rowFrom; row != rowTill; ++row) { + for (auto col = 0; col != perRow; ++col) { + const auto index = row * perRow + col; + if (index >= count) { + break; + } + const auto last = !((col + 1) % perRow); + if (last) { + x = padding.left() + available - single.width(); + } + ensureButton(index); + if (last) { + x = padding.left(); + y += singleh; + } else { + x += singlew; + } + } + } + const auto till = std::min(int(buttons.size()), rowTill * perRow); + for (auto i = count; i < till; ++i) { + if (const auto button = buttons[i].get()) { + button->hide(); + } + } + + const auto page = range.bottom - range.top; + if (loadMore && page > 0 && range.bottom + page > raw->height()) { + loadMore(); + } + }; + + state->visibleRange = raw->visibleRange(); + state->visibleRange.value( + ) | rpl::start_with_next(rebuild, raw->lifetime()); + std::move( gifts ) | rpl::start_with_next([=](const GiftsDescriptor &gifts) { const auto width = st::boxWideWidth; const auto padding = st::giftBoxPadding; const auto available = width - padding.left() - padding.right(); - const auto perRow = available / single.width(); - const auto count = int(gifts.list.size()); + state->perRow = available / single.width(); + state->list = std::move(gifts.list); + state->api = gifts.api; - auto order = ranges::views::ints + const auto count = int(state->list.size()); + state->order = ranges::views::ints | ranges::views::take(count) | ranges::to_vector; + state->validated.clear(); if (SortForBirthday(peer)) { - ranges::stable_partition(order, [&](int i) { - const auto &gift = gifts.list[i]; + ranges::stable_partition(state->order, [&](int i) { + const auto &gift = state->list[i]; const auto stars = std::get_if(&gift); - return stars && stars->info.birthday; + return stars && stars->info.birthday && !stars->info.unique; }); } - auto x = padding.left(); - auto y = padding.top(); - state->buttons.resize(count); - for (auto &button : state->buttons) { - if (!button) { - button = std::make_unique(raw, &state->delegate); - button->show(); - } - } - const auto api = gifts.api; - for (auto i = 0; i != count; ++i) { - const auto button = state->buttons[i].get(); - const auto &descriptor = gifts.list[order[i]]; - button->setDescriptor(descriptor, GiftButton::Mode::Full); - - const auto last = !((i + 1) % perRow); - if (last) { - x = padding.left() + available - single.width(); - } - button->setGeometry(QRect(QPoint(x, y), single), extend); - if (last) { - x = padding.left(); - y += single.height() + st::giftBoxGiftSkip.y(); - } else { - x += single.width() + st::giftBoxGiftSkip.x(); - } - - button->setClickedCallback([=] { - const auto star = std::get_if(&descriptor); - if (star && IsSoldOut(star->info)) { - window->show(Box(SoldOutBox, window, *star)); - } else { - window->show( - Box(SendGiftBox, window, peer, api, descriptor)); - } - }); - } - if (count % perRow) { - y += padding.bottom() + single.height(); - } else { - y += padding.bottom() - st::giftBoxGiftSkip.y(); - } - raw->resize(raw->width(), count ? y : 0); + const auto rows = (count + state->perRow - 1) / state->perRow; + const auto height = padding.top() + + (rows * single.height()) + + ((rows - 1) * st::giftBoxGiftSkip.y()) + + padding.bottom(); + raw->resize(raw->width(), height); + rebuild(); }, raw->lifetime()); return result; @@ -1876,42 +2071,83 @@ void AddBlock( gifts.list | ranges::to>, gifts.api, }; - })); + }), nullptr); result->lifetime().add([state = std::move(state)] {}); return result; } [[nodiscard]] object_ptr MakeStarsGifts( not_null window, - not_null peer) { + not_null peer, + MyGiftsDescriptor my) { auto result = object_ptr((QWidget*)nullptr); struct State { rpl::variable> gifts; rpl::variable priceTab = kPriceTabAll; + rpl::event_stream<> myUpdated; + MyGiftsDescriptor my; + rpl::lifetime myLoading; }; const auto state = result->lifetime().make_state(); + state->my = std::move(my); state->gifts = GiftsStars(&window->session(), peer); - auto tabs = MakeGiftsPriceTabs(window, peer, state->gifts.value()); + auto tabs = MakeGiftsPriceTabs( + window, + peer, + state->gifts.value(), + !state->my.list.empty()); state->priceTab = std::move(tabs.priceTab); result->add(std::move(tabs.widget)); result->add(MakeGiftsList(window, peer, rpl::combine( state->gifts.value(), - state->priceTab.value() - ) | rpl::map([=](std::vector &&gifts, int price) { - gifts.erase(ranges::remove_if(gifts, [&](const GiftTypeStars &gift) { - return (price == kPriceTabLimited) - ? (!gift.info.limitedCount) - : (price == kPriceTabInStock) - ? IsSoldOut(gift.info) - : (price && gift.info.stars != price); - }), end(gifts)); + state->priceTab.value(), + rpl::single(rpl::empty) | rpl::then(state->myUpdated.events()) + ) | rpl::map([=](std::vector &&gifts, int price, auto) { + if (price == kPriceTabMy) { + gifts.clear(); + for (const auto &gift : state->my.list) { + gifts.push_back({ + .transferId = gift.manageId, + .info = gift.info, + .mine = true, + }); + } + } else { + const auto pred = [&](const GiftTypeStars &gift) { + return (price == kPriceTabLimited) + ? (!gift.info.limitedCount) + : (price == kPriceTabInStock) + ? IsSoldOut(gift.info) + : (price && gift.info.stars != price); + }; + gifts.erase(ranges::remove_if(gifts, pred), end(gifts)); + } return GiftsDescriptor{ gifts | ranges::to>(), }; - }))); + }), [=] { + if (state->priceTab.current() == kPriceTabMy + && !state->my.offset.isEmpty() + && !state->myLoading) { + state->myLoading = UniqueGiftsSlice( + &peer->session(), + state->my.offset + ) | rpl::start_with_next([=](MyGiftsDescriptor &&descriptor) { + state->myLoading.destroy(); + state->my.offset = descriptor.list.empty() + ? QString() + : descriptor.offset; + state->my.list.insert( + end(state->my.list), + std::make_move_iterator(begin(descriptor.list)), + std::make_move_iterator(end(descriptor.list))); + state->myUpdated.fire({}); + }); + } + })); return result; } @@ -1919,7 +2155,8 @@ void AddBlock( void GiftBox( not_null box, not_null window, - not_null peer) { + not_null peer, + MyGiftsDescriptor my) { box->setWidth(st::boxWideWidth); box->setStyle(st::creditsGiftBox); box->setNoContentMargin(true); @@ -1935,6 +2172,24 @@ void GiftBox( AddSkip(content, st::defaultVerticalListSkip * 5); + // Check disallowed gift types + const auto user = peer->asUser(); + using Type = Api::DisallowedGiftType; + const auto disallowedTypes = user + ? user->disallowedGiftTypes() + : Type::Premium; + const auto premiumDisallowed = peer->isSelf() + || (disallowedTypes & Type::Premium); + const auto limitedDisallowed = !peer->isSelf() + && (disallowedTypes & Type::Limited); + const auto unlimitedDisallowed = !peer->isSelf() + && (disallowedTypes & Type::Unlimited); + const auto uniqueDisallowed = !peer->isSelf() + && (disallowedTypes & Type::Unique); + const auto allStarsDisallowed = limitedDisallowed + && unlimitedDisallowed + && uniqueDisallowed; + content->add( object_ptr>( content, @@ -1956,11 +2211,12 @@ void GiftBox( window->showSettings(Settings::CreditsId()); return false; }; - if (peer->isUser() && !peer->isSelf()) { + if (peer->isUser() && !peer->isSelf() && !premiumDisallowed) { const auto premiumClickHandlerFilter = [=](const auto &...) { Settings::ShowPremium(window, u"gift_send"_q); return false; }; + AddBlock(content, window, { .subtitle = tr::lng_gift_premium_subtitle(), .about = tr::lng_gift_premium_about( @@ -1973,28 +2229,32 @@ void GiftBox( .content = MakePremiumGifts(window, peer), }); } - AddBlock(content, window, { - .subtitle = (peer->isSelf() - ? tr::lng_gift_self_title() - : peer->isBroadcast() - ? tr::lng_gift_channel_title() - : tr::lng_gift_stars_subtitle()), - .about = (peer->isSelf() - ? tr::lng_gift_self_about(Text::WithEntities) - : peer->isBroadcast() - ? tr::lng_gift_channel_about( - lt_name, - rpl::single(Text::Bold(peer->name())), - Text::WithEntities) - : tr::lng_gift_stars_about( - lt_name, - rpl::single(Text::Bold(peer->shortName())), - lt_link, - tr::lng_gift_stars_link() | Text::ToLink(), - Text::WithEntities)), - .aboutFilter = starsClickHandlerFilter, - .content = MakeStarsGifts(window, peer), - }); + + // Only add star gifts if at least one type is allowed + if (!allStarsDisallowed) { + AddBlock(content, window, { + .subtitle = (peer->isSelf() + ? tr::lng_gift_self_title() + : peer->isBroadcast() + ? tr::lng_gift_channel_title() + : tr::lng_gift_stars_subtitle()), + .about = (peer->isSelf() + ? tr::lng_gift_self_about(Text::WithEntities) + : peer->isBroadcast() + ? tr::lng_gift_channel_about( + lt_name, + rpl::single(Text::Bold(peer->name())), + Text::WithEntities) + : tr::lng_gift_stars_about( + lt_name, + rpl::single(Text::Bold(peer->shortName())), + lt_link, + tr::lng_gift_stars_link() | Text::ToLink(), + Text::WithEntities)), + .aboutFilter = starsClickHandlerFilter, + .content = MakeStarsGifts(window, peer, std::move(my)), + }); + } } struct SelfOption { @@ -2184,11 +2444,31 @@ void ChooseStarGiftRecipient( void ShowStarGiftBox( not_null controller, not_null peer) { + if (controller->showFrozenError()) { + return; + } + struct Session { PeerData *peer = nullptr; + MyGiftsDescriptor my; bool premiumGiftsReady = false; bool starsGiftsReady = false; + bool fullReady = false; + bool myReady = false; + + bool hasPremium = false; + bool hasUpgradable = false; + bool hasLimited = false; + bool hasUnlimited = false; + rpl::lifetime lifetime; + + [[nodiscard]] bool ready() const { + return premiumGiftsReady + && starsGiftsReady + && fullReady + && myReady; + } }; static auto Map = base::flat_map, Session>(); @@ -2203,46 +2483,91 @@ void ShowStarGiftBox( i->second = Session{ .peer = peer }; const auto weak = base::make_weak(controller); - const auto show = [=] { - Map[session] = Session(); + const auto checkReady = [=] { + auto &entry = Map[session]; + if (!entry.ready()) { + return; + } + auto was = std::move(entry); + entry = Session(); if (const auto strong = weak.get()) { - strong->show(Box(GiftBox, strong, peer)); + if (const auto user = peer->asUser()) { + using Type = Api::DisallowedGiftType; + const auto disallowedTypes = user->disallowedGiftTypes(); + const auto premium = (disallowedTypes & Type::Premium) + || peer->isSelf(); + const auto limited = (disallowedTypes & Type::Limited); + const auto unlimited = (disallowedTypes & Type::Unlimited); + const auto unique = (disallowedTypes & Type::Unique); + if ((unique || (!was.hasUpgradable && was.my.list.empty())) + && (premium || !was.hasPremium) + && (limited || !was.hasLimited) + && (unlimited || !was.hasUnlimited)) { + strong->showToast( + tr::lng_edit_privacy_gifts_restricted(tr::now)); + return; + } + } + strong->show(Box(GiftBox, strong, peer, std::move(was.my))); } }; - base::timer_once( - kGiftsPreloadTimeout - ) | rpl::start_with_next(show, i->second.lifetime); - const auto user = peer->asUser(); if (user && !user->isSelf()) { GiftsPremium( session, peer ) | rpl::start_with_next([=](PremiumGiftsDescriptor &&gifts) { - if (!gifts.list.empty()) { - auto &entry = Map[session]; - entry.premiumGiftsReady = true; - if (entry.starsGiftsReady) { - show(); - } - } + auto &entry = Map[session]; + entry.premiumGiftsReady = true; + entry.hasPremium = !gifts.list.empty(); + checkReady(); }, i->second.lifetime); } else { + i->second.hasPremium = false; i->second.premiumGiftsReady = true; } + if (peer->isFullLoaded()) { + i->second.fullReady = true; + } else { + peer->updateFull(); + peer->session().changes().peerUpdates( + peer, + Data::PeerUpdate::Flag::FullInfo + ) | rpl::take(1) | rpl::start_with_next([=] { + auto &entry = Map[session]; + entry.fullReady = true; + checkReady(); + }, i->second.lifetime); + } + GiftsStars( session, peer ) | rpl::start_with_next([=](std::vector &&gifts) { - if (!gifts.empty()) { - auto &entry = Map[session]; - entry.starsGiftsReady = true; - if (entry.premiumGiftsReady) { - show(); + auto &entry = Map[session]; + entry.starsGiftsReady = true; + for (const auto &gift : gifts) { + if (gift.info.limitedCount) { + entry.hasLimited = true; + if (gift.info.starsToUpgrade) { + entry.hasUpgradable = true; + } + } else { + entry.hasUnlimited = true; } } + checkReady(); + }, i->second.lifetime); + + UniqueGiftsSlice( + session + ) | rpl::start_with_next([=](MyGiftsDescriptor &&gifts) { + auto &entry = Map[session]; + entry.my = std::move(gifts); + entry.myReady = true; + checkReady(); }, i->second.lifetime); } diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp index 8d297bb27f..f0f52c1855 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp @@ -463,6 +463,8 @@ void TransferGift( std::move(formDone)); } +} // namespace + void ShowTransferToBox( not_null controller, not_null peer, @@ -539,8 +541,6 @@ void ShowTransferToBox( })); } -} // namespace - void ShowTransferGiftBox( not_null window, std::shared_ptr gift, diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.h b/Telegram/SourceFiles/boxes/transfer_gift_box.h index e058e88c3c..5cb051a892 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.h +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.h @@ -16,6 +16,13 @@ struct UniqueGift; class SavedStarGiftId; } // namespace Data +void ShowTransferToBox( + not_null controller, + not_null peer, + std::shared_ptr gift, + Data::SavedStarGiftId savedId, + Fn closeParentBox); + void ShowTransferGiftBox( not_null window, std::shared_ptr gift, diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 673216650d..e5af06c171 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -715,18 +715,21 @@ void Panel::createRemoteLowBattery() { }, _remoteLowBattery->lifetime()); constexpr auto kBatterySize = QSize(29, 13); + const auto scaledBatterySize = QSize( + style::ConvertScale(kBatterySize.width()), + style::ConvertScale(kBatterySize.height())); const auto icon = [&] { auto svg = QSvgRenderer( BatterySvg(kBatterySize, st::videoPlayIconFg->c)); auto image = QImage( - kBatterySize * style::DevicePixelRatio(), + scaledBatterySize * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(style::DevicePixelRatio()); image.fill(Qt::transparent); { auto p = QPainter(&image); - svg.render(&p, Rect(kBatterySize)); + svg.render(&p, Rect(scaledBatterySize)); } return image; }(); @@ -745,7 +748,7 @@ void Panel::createRemoteLowBattery() { p.drawImage( st::callTooltipMutedIconPosition.x(), - (r.height() - kBatterySize.height()) / 2, + (r.height() - scaledBatterySize.height()) / 2, icon); }, _remoteLowBattery->lifetime()); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index b5f24927da..1660c27585 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1131,6 +1131,10 @@ historyScheduledToggle: IconButton(historyAttach) { { "chat/input_scheduled_dot", attentionButtonFg } }; } +historyGiftToUser: IconButton(historyAttach) { + icon: icon {{ "chat/input_gift", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_gift", historyComposeIconFgOver }}; +} historyAttachEmojiInner: IconButton(historyAttach) { icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; @@ -1566,3 +1570,27 @@ processingVideoView: RoundButton(defaultActiveButton) { textBgOver: transparent; ripple: emptyRippleAnimation; } + +frozenBarTitle: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: attentionButtonFg; +} +frozenRestrictionTitle: FlatLabel(frozenBarTitle) { + align: align(top); +} +frozenBarSubtitle: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} +frozenRestrictionSubtitle: FlatLabel(frozenBarSubtitle) { + align: align(top); +} +frozenInfoBox: Box(defaultBox) { + buttonPadding: margins(16px, 11px, 16px, 16px); + buttonHeight: 42px; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + style: semiboldTextStyle; + } + shadowIgnoreTopSkip: true; +} diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index de43168a3a..bc7f46d696 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1265,7 +1265,7 @@ void EmojiListWidget::fillEmojiStatusMenu( int section, int index) { const auto chosen = lookupCustomEmoji(index, section); - if (!chosen || chosen.collectible) { + if (!chosen) { return; } const auto selectWith = [=](TimeId scheduled) { diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index a352674552..61d27c3408 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -11,38 +11,48 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" // History::session #include "history/history_item.h" // HistoryItem::originalText #include "history/history_item_helpers.h" // DropDisallowedCustomEmoji +#include "base/unixtime.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" #include "base/event_filter.h" #include "ui/chat/chat_style.h" #include "ui/layers/generic_box.h" +#include "ui/basic_click_handlers.h" #include "ui/rect.h" #include "core/shortcuts.h" #include "core/application.h" #include "core/core_settings.h" #include "core/ui_integration.h" +#include "lottie/lottie_icon.h" +#include "info/profile/info_profile_icon.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/shadow.h" #include "ui/power_saving.h" +#include "ui/vertical_list.h" #include "ui/ui_utility.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_document.h" #include "data/stickers/data_custom_emoji.h" #include "chat_helpers/emoji_suggestions_widget.h" +#include "history/view/controls/compose_controls_common.h" #include "window/window_session_controller.h" #include "lang/lang_keys.h" #include "mainwindow.h" #include "main/main_session.h" +#include "settings/settings_common.h" #include "settings/settings_premium.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" +#include "styles/style_dialogs.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" #include "base/qt/qt_common_adapters.h" @@ -1187,10 +1197,10 @@ base::unique_qptr CreateDisabledFieldView( return result; } -base::unique_qptr TextErrorSendRestriction( +std::unique_ptr TextErrorSendRestriction( QWidget *parent, const QString &text) { - auto result = base::make_unique_q(parent); + auto result = std::make_unique(parent); const auto raw = result.get(); const auto label = CreateChild( result.get(), @@ -1215,11 +1225,11 @@ base::unique_qptr TextErrorSendRestriction( return result; } -base::unique_qptr PremiumRequiredSendRestriction( +std::unique_ptr PremiumRequiredSendRestriction( QWidget *parent, not_null user, not_null controller) { - auto result = base::make_unique_q(parent); + auto result = std::make_unique(parent); const auto raw = result.get(); const auto label = CreateChild( result.get(), @@ -1254,6 +1264,102 @@ base::unique_qptr PremiumRequiredSendRestriction( return result; } +std::unique_ptr BoostsToLiftWriteRestriction( + not_null parent, + std::shared_ptr show, + not_null peer, + int boosts) { + auto result = std::make_unique( + parent, + tr::lng_restricted_boost_group(tr::now), + st::historyComposeButton); + result->setClickedCallback([=] { + const auto window = show->resolveWindow(); + window->resolveBoostState(peer->asChannel(), boosts); + }); + return result; +} + +std::unique_ptr FrozenWriteRestriction( + not_null parent, + std::shared_ptr show, + FrozenWriteRestrictionType type, + FreezeInfoStyleOverride st) { + using namespace Ui; + + auto result = std::make_unique( + parent, + QString(), + st::historyComposeButton); + const auto raw = result.get(); + + const auto bar = (type == FrozenWriteRestrictionType::DialogsList); + const auto title = CreateChild( + raw, + (bar ? tr::lng_frozen_bar_title : tr::lng_frozen_restrict_title)( + tr::now), + bar ? st::frozenBarTitle : st::frozenRestrictionTitle); + title->setAttribute(Qt::WA_TransparentForMouseEvents); + title->show(); + const auto subtitle = CreateChild( + raw, + (bar + ? tr::lng_frozen_bar_text( + lt_arrow, + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + Ui::Text::WithEntities) + : tr::lng_frozen_restrict_text(Ui::Text::WithEntities)), + bar ? st::frozenBarSubtitle : st::frozenRestrictionSubtitle); + subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + subtitle->show(); + + const auto shadow = bar ? CreateChild(raw) : nullptr; + const auto icon = bar ? CreateChild(raw) : nullptr; + if (icon) { + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + st::menuIconDisableAttention.paintInCenter(p, icon->rect()); + }, icon->lifetime()); + icon->show(); + } + + raw->sizeValue() | rpl::start_with_next([=](QSize size) { + if (bar) { + const auto toggle = [&](auto &&widget, bool shown) { + if (widget->isHidden() == shown) { + widget->setVisible(shown); + } + }; + const auto small = 2 * st::defaultDialogRow.photoSize; + const auto shown = (size.width() > small); + toggle(icon, !shown); + toggle(title, shown); + toggle(subtitle, shown); + icon->setGeometry(0, 0, size.width(), size.height()); + } + const auto skip = bar + ? st::defaultDialogRow.padding.left() + : 2 * st::normalFont->spacew; + const auto available = size.width() - skip * 2; + title->resizeToWidth(available); + subtitle->resizeToWidth(available); + const auto height = title->height() + subtitle->height(); + const auto top = (size.height() - height) / 2; + title->moveToLeft(skip, top, size.width()); + subtitle->moveToLeft(skip, top + title->height(), size.width()); + + const auto line = st::lineWidth; + if (shadow) { + shadow->setGeometry(0, size.height() - line, size.width(), line); + } + }, title->lifetime()); + + raw->setClickedCallback([=] { + show->show(Box(FrozenInfoBox, &show->session(), st)); + }); + return result; +} + void SelectTextInFieldWithMargins( not_null field, const TextSelection &selection) { @@ -1301,3 +1407,101 @@ rpl::producer PaidSendButtonText( return PaidSendButtonText(tr::now, count); }); } + +void FrozenInfoBox( + not_null box, + not_null session, + FreezeInfoStyleOverride st) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::frozenInfoBox); + box->setNoContentMargin(true); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + const auto info = session->frozen(); + const auto content = box->verticalLayout(); + auto icon = Settings::CreateLottieIcon( + content, + { + .name = u"media_forbidden"_q, + .sizeOverride = { + st::changePhoneIconSize, + st::changePhoneIconSize, + }, + }, + st::settingLocalPasscodeIconPadding); + content->add(std::move(icon.widget)); + box->setShowFinishedCallback([animate = std::move(icon.animate)] { + animate(anim::repeat::once); + }); + + Ui::AddSkip(content); + + const auto infoRow = [&]( + rpl::producer title, + rpl::producer text, + not_null icon) { + auto raw = content->add( + object_ptr(content)); + raw->add( + object_ptr( + raw, + std::move(title) | Ui::Text::ToBold(), + st.infoTitle ? *st.infoTitle : st::defaultFlatLabel), + st::settingsPremiumRowTitlePadding); + raw->add( + object_ptr( + raw, + std::move(text), + st.infoAbout ? *st.infoAbout : st::upgradeGiftSubtext), + st::settingsPremiumRowAboutPadding); + object_ptr( + raw, + *icon, + st::starrefInfoIconPosition); + }; + + content->add( + object_ptr( + content, + tr::lng_frozen_title(), + st.title ? *st.title : st::uniqueGiftTitle), + st::settingsPremiumRowTitlePadding); + + Ui::AddSkip(content, st::defaultVerticalListSkip * 3); + + infoRow( + tr::lng_frozen_subtitle1(), + tr::lng_frozen_text1(Ui::Text::WithEntities), + st.violationIcon ? st.violationIcon : &st::menuIconBlock); + infoRow( + tr::lng_frozen_subtitle2(), + tr::lng_frozen_text2(Ui::Text::WithEntities), + st.readOnlyIcon ? st.readOnlyIcon : &st::menuIconLock); + infoRow( + tr::lng_frozen_subtitle3(), + tr::lng_frozen_text3( + lt_link, + rpl::single(Ui::Text::Link(u"@SpamBot"_q, info.appealUrl)), + lt_date, + rpl::single(TextWithEntities{ + langDayOfMonthFull( + base::unixtime::parse(info.until).date()), + }), + Ui::Text::WithEntities), + st.appealIcon ? st.appealIcon : &st::menuIconHourglass); + + const auto button = box->addButton( + tr::lng_frozen_appeal_button(), + [url = info.appealUrl] { UrlClickHandler::Open(url); }); + const auto buttonPadding = st::frozenInfoBox.buttonPadding; + const auto buttonWidth = st::boxWideWidth + - buttonPadding.left() + - buttonPadding.right(); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); +} diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 7d97d93321..4d7ef52b48 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -37,7 +37,12 @@ enum class PauseReason; class Show; } // namespace ChatHelpers +namespace HistoryView::Controls { +struct WriteRestriction; +} // namespace HistoryView::Controls + namespace Ui { +class GenericBox; class PopupMenu; class Show; } // namespace Ui @@ -162,13 +167,41 @@ private: [[nodiscard]] base::unique_qptr CreateDisabledFieldView( QWidget *parent, not_null peer); -[[nodiscard]] base::unique_qptr TextErrorSendRestriction( +[[nodiscard]] std::unique_ptr TextErrorSendRestriction( QWidget *parent, const QString &text); -[[nodiscard]] base::unique_qptr PremiumRequiredSendRestriction( +[[nodiscard]] std::unique_ptr PremiumRequiredSendRestriction( QWidget *parent, not_null user, not_null controller); +[[nodiscard]] auto BoostsToLiftWriteRestriction( + not_null parent, + std::shared_ptr show, + not_null peer, + int boosts) +-> std::unique_ptr; + +struct FreezeInfoStyleOverride { + const style::Box *box = nullptr; + const style::FlatLabel *title = nullptr; + const style::FlatLabel *subtitle = nullptr; + const style::icon *violationIcon = nullptr; + const style::icon *readOnlyIcon = nullptr; + const style::icon *appealIcon = nullptr; + const style::FlatLabel *infoTitle = nullptr; + const style::FlatLabel *infoAbout = nullptr; +}; +[[nodiscard]] FreezeInfoStyleOverride DarkFreezeInfoStyle(); + +enum class FrozenWriteRestrictionType { + MessageField, + DialogsList, +}; +[[nodiscard]] std::unique_ptr FrozenWriteRestriction( + not_null parent, + std::shared_ptr show, + FrozenWriteRestrictionType type, + FreezeInfoStyleOverride st = {}); void SelectTextInFieldWithMargins( not_null field, @@ -178,3 +211,8 @@ void SelectTextInFieldWithMargins( [[nodiscard]] rpl::producer PaidSendButtonText( rpl::producer stars, rpl::producer fallback = nullptr); + +void FrozenInfoBox( + not_null box, + not_null session, + FreezeInfoStyleOverride st); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index bca5ae7ebc..33ec7ef78d 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/stickers_list_widget.h" #include "chat_helpers/gifs_list_widget.h" #include "menu/menu_send.h" +#include "ui/controls/swipe_handler.h" #include "ui/controls/tabbed_search.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -522,7 +523,6 @@ TabbedSelector::TabbedSelector( if (hasEmojiTab()) { emoji()->refreshEmoji(); } - //setAttribute(Qt::WA_AcceptTouchEvents); setAttribute(Qt::WA_OpaquePaintEvent, false); showAll(); hide(); @@ -530,6 +530,60 @@ TabbedSelector::TabbedSelector( TabbedSelector::~TabbedSelector() = default; +void TabbedSelector::reinstallSwipe(not_null widget) { + _swipeLifetime.destroy(); + + auto update = [=](Ui::Controls::SwipeContextData data) { + if (data.translation != 0) { + if (!_swipeBackData.callback) { + _swipeBackData = Ui::Controls::SetupSwipeBack( + this, + [=]() -> std::pair { + return { + st::historyForwardChooseBg->c, + st::historyForwardChooseFg->c, + }; + }, + data.translation < 0); + } + _swipeBackData.callback(data); + return; + } else if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } + }; + + auto init = [=](int, Qt::LayoutDirection direction) { + if (!_tabsSlider) { + return Ui::Controls::SwipeHandlerFinishData(); + } + const auto activeSection = _tabsSlider->activeSection(); + const auto isToLeft = direction == Qt::RightToLeft; + if ((isToLeft && activeSection > 0) + || (!isToLeft && activeSection < _tabs.size() - 1)) { + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + if (_tabsSlider + && _tabsSlider->activeSection() == activeSection) { + _swipeBackData = {}; + _tabsSlider->setActiveSection(isToLeft + ? activeSection - 1 + : activeSection + 1); + } + }); + } + return Ui::Controls::SwipeHandlerFinishData(); + }; + + Ui::Controls::SetupSwipeHandler({ + .widget = widget, + .scroll = _scroll.data(), + .update = std::move(update), + .init = std::move(init), + .dontStart = nullptr, + .onLifetime = &_swipeLifetime, + }); +} + const style::EmojiPan &TabbedSelector::st() const { return _st; } @@ -1302,6 +1356,10 @@ void TabbedSelector::setWidgetToScrollArea() { inner->moveToLeft(0, 0); inner->show(); + if (_tabs.size() > 1) { + reinstallSwipe(inner); + } + _scroll->disableScroll(false); scrollToY(currentTab()->getScrollTop()); handleScroll(); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index ad8821d846..47e8c4b649 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "chat_helpers/compose/compose_features.h" #include "ui/rp_widget.h" +#include "ui/controls/swipe_handler_data.h" #include "ui/effects/animations.h" #include "ui/effects/message_sending_animation_common.h" #include "ui/effects/panel_animation.h" @@ -287,12 +288,16 @@ private: not_null gifs() const; not_null masks() const; + void reinstallSwipe(not_null widget); + const style::EmojiPan &_st; const ComposeFeatures _features; const std::shared_ptr _show; const PauseReason _level = {}; const Fn _customTextColor; + Ui::Controls::SwipeBackResult _swipeBackData; + Mode _mode = Mode::Full; int _roundRadius = 0; int _footerTop = 0; @@ -329,6 +334,8 @@ private: rpl::event_stream<> _showRequests; rpl::event_stream<> _slideFinished; + rpl::lifetime _swipeLifetime; + }; class TabbedSelector::Inner : public Ui::RpWidget { diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index e3ceadc853..c8134da03f 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -93,6 +93,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include + // AyuGram includes #include "ayu/ayu_infra.h" #include "ayu/features/streamer_mode/streamer_mode.h" diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index f655093ef7..1db0a3a47b 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -244,7 +244,7 @@ QByteArray Settings::serialize() const { + Serialize::stringSize(_customFontFamily) + sizeof(qint32) * 3 + Serialize::bytearraySize(_tonsiteStorageToken) - + sizeof(qint32) * 7; + + sizeof(qint32) * 8; auto result = QByteArray(); result.reserve(size); @@ -405,7 +405,8 @@ QByteArray Settings::serialize() const { << qint32(_recordVideoMessages ? 1 : 0) << SerializeVideoQuality(_videoQuality) << qint32(_ivZoom.current()) - << qint32(_systemDarkModeEnabled.current() ? 1 : 0); + << qint32(_systemDarkModeEnabled.current() ? 1 : 0) + << qint32(_quickDialogAction); } Ensures(result.size() == size); @@ -536,6 +537,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0; quint32 videoQuality = SerializeVideoQuality(_videoQuality); quint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0; + quint32 quickDialogAction = quint32(_quickDialogAction); stream >> themesAccentColors; if (!stream.atEnd()) { @@ -864,6 +866,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> systemDarkModeEnabled; } + if (!stream.atEnd()) { + stream >> quickDialogAction; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -1086,6 +1091,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _recordVideoMessages = (recordVideoMessages == 1); _videoQuality = DeserializeVideoQuality(videoQuality); _chatFiltersHorizontal = (chatFiltersHorizontal == 1); + _quickDialogAction = Dialogs::Ui::QuickDialogAction(quickDialogAction); } QString Settings::getSoundPath(const QString &key) const { @@ -1477,6 +1483,7 @@ void Settings::resetOnLastLogout() { _recordVideoMessages = false; _videoQuality = {}; _chatFiltersHorizontal = false; + _quickDialogAction = Dialogs::Ui::QuickDialogAction::Disabled; _recentEmojiPreload.clear(); _recentEmoji.clear(); @@ -1664,4 +1671,12 @@ void Settings::setChatFiltersHorizontal(bool value) { _chatFiltersHorizontal = value; } +Dialogs::Ui::QuickDialogAction Settings::quickDialogAction() const { + return _quickDialogAction; +} + +void Settings::setQuickDialogAction(Dialogs::Ui::QuickDialogAction action) { + _quickDialogAction = action; +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 4f4711b234..f8dc8a188c 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings_proxy.h" #include "media/media_common.h" +#include "dialogs/ui/dialogs_quick_action.h" #include "window/themes/window_themes_embedded.h" #include "ui/chat/attach/attach_send_files_way.h" #include "base/flags.h" @@ -952,6 +953,9 @@ public: [[nodiscard]] static PlaybackSpeed DeserializePlaybackSpeed( qint32 speed); + [[nodiscard]] Dialogs::Ui::QuickDialogAction quickDialogAction() const; + void setQuickDialogAction(Dialogs::Ui::QuickDialogAction); + void resetOnLastLogout(); private: @@ -1092,6 +1096,9 @@ private: bool _recordVideoMessages = false; + Dialogs::Ui::QuickDialogAction _quickDialogAction + = Dialogs::Ui::QuickDialogAction::Disabled; + QByteArray _photoEditorBrush; }; diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index 5334eb97ef..538f78dd25 100644 --- a/Telegram/SourceFiles/core/launcher.cpp +++ b/Telegram/SourceFiles/core/launcher.cpp @@ -504,6 +504,22 @@ uint64 Launcher::installationTag() const { return InstallationTag; } +QByteArray Launcher::instanceHash() const { + static const auto Result = [&] { + QByteArray h(32, 0); + if (customWorkingDir()) { + const auto d = QFile::encodeName( + QDir(cWorkingDir()).absolutePath()); + hashMd5Hex(d.constData(), d.size(), h.data()); + } else { + const auto f = QFile::encodeName(cExeDir() + cExeName()); + hashMd5Hex(f.constData(), f.size(), h.data()); + } + return h; + }(); + return Result; +} + void Launcher::processArguments() { enum class KeyFormat { NoValues, diff --git a/Telegram/SourceFiles/core/launcher.h b/Telegram/SourceFiles/core/launcher.h index ea5b1c97f3..e4de3c31d2 100644 --- a/Telegram/SourceFiles/core/launcher.h +++ b/Telegram/SourceFiles/core/launcher.h @@ -33,6 +33,7 @@ public: bool customWorkingDir() const; uint64 installationTag() const; + QByteArray instanceHash() const; bool checkPortableVersionFolder(); bool validateCustomWorkingDir(); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index fbb5fbfb0a..0c8899cf7a 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -306,6 +306,42 @@ void ShowLanguagesBox(Window::SessionController *controller) { Guard = LanguageBox::Show(controller); } +void ShowPhonePrivacyBox(Window::SessionController *controller) { + static auto Guard = base::binary_guard(); + auto guard = base::binary_guard(); + + using Privacy = Api::UserPrivacy; + const auto key = Privacy::Key::PhoneNumber; + controller->session().api().userPrivacy().reload(key); + + const auto weak = base::make_weak(controller); + auto shared = std::make_shared( + guard.make_guard()); + auto lifetime = std::make_shared(); + controller->session().api().userPrivacy().value( + key + ) | rpl::take( + 1 + ) | rpl::start_with_next([=](const Privacy::Rule &value) mutable { + using namespace ::Settings; + const auto show = shared->alive(); + if (lifetime) { + base::take(lifetime)->destroy(); + } + if (show) { + if (const auto controller = weak.get()) { + controller->show(Box( + controller, + std::make_unique( + controller), + value)); + } + } + }, *lifetime); + + Guard = std::move(guard); +} + bool SetLanguage( Window::SessionController *controller, const Match &match, @@ -722,6 +758,9 @@ bool ResolveSettings( if (section == u"language"_q) { ShowLanguagesBox(controller); return {}; + } else if (section == u"phone_privacy"_q) { + ShowPhonePrivacyBox(controller); + return {}; } else if (section == u"devices"_q) { return ::Settings::Sessions::Id(); } else if (section == u"folders"_q) { @@ -876,6 +915,8 @@ bool ShowEditBirthday( const QVariant &context) { if (!controller) { return false; + } else if (controller->showFrozenError()) { + return true; } const auto user = controller->session().user(); const auto save = [=](Data::Birthday result) { @@ -932,6 +973,8 @@ bool ShowEditPersonalChannel( const QVariant &context) { if (!controller) { return false; + } else if (controller->showFrozenError()) { + return true; } auto listController = std::make_unique( @@ -1466,7 +1509,7 @@ const std::vector &LocalUrlHandlers() { ResolvePrivatePost }, { - u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile)?$"_q, + u"^settings(/language|/devices|/folders|/privacy|/themes|/change_number|/auto_delete|/information|/edit_profile|/phone_privacy)?$"_q, ResolveSettings }, { diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp index 667ab43aed..9a3a09a444 100644 --- a/Telegram/SourceFiles/core/shortcuts.cpp +++ b/Telegram/SourceFiles/core/shortcuts.cpp @@ -110,6 +110,7 @@ const auto CommandByName = base::flat_map{ { u"read_chat"_q , Command::ReadChat }, { u"show_chat_menu"_q , Command::ShowChatMenu }, + { u"show_chat_preview"_q , Command::ShowChatPreview }, // Shortcuts that have no default values. { u"message"_q , Command::JustSendMessage }, @@ -506,6 +507,7 @@ void Manager::fillDefaults() { set(u"ctrl+r"_q, Command::ReadChat); set(u"ctrl+\\"_q, Command::ShowChatMenu); + set(u"ctrl+]"_q, Command::ShowChatPreview); _defaults = keysCurrents(); } diff --git a/Telegram/SourceFiles/core/shortcuts.h b/Telegram/SourceFiles/core/shortcuts.h index 70c8eb63af..7be77d4eca 100644 --- a/Telegram/SourceFiles/core/shortcuts.h +++ b/Telegram/SourceFiles/core/shortcuts.h @@ -72,6 +72,7 @@ enum class Command { MediaViewerFullscreen, ShowChatMenu, + ShowChatPreview, SupportReloadTemplates, SupportToggleMuted, diff --git a/Telegram/SourceFiles/core/utils.h b/Telegram/SourceFiles/core/utils.h index 325f6ccc43..9d8a3ee622 100644 --- a/Telegram/SourceFiles/core/utils.h +++ b/Telegram/SourceFiles/core/utils.h @@ -21,10 +21,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#if __has_include() -#include -#endif - #define qsl(s) QStringLiteral(s) namespace base { @@ -34,15 +30,6 @@ inline bool in_range(Value &&value, From &&from, Till &&till) { return (value >= from) && (value < till); } -#if __has_include() -inline QString IconName() { - static const auto Result = KSandbox::isFlatpak() - ? qEnvironmentVariable("FLATPAK_ID") - : u"telegram"_q; - return Result; -} -#endif - inline bool CanReadDirectory(const QString &path) { #ifndef Q_OS_MAC // directory_iterator since 10.15 std::error_code error; diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index a2770d5708..2122ec9e8e 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 5012003; -constexpr auto AppVersionStr = "5.12.3"; +constexpr auto AppVersion = 5013001; +constexpr auto AppVersionStr = "5.13.1"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp index ca894acf55..126b7043bb 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -8,10 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/business/data_business_chatbots.h" #include "apiwrap.h" +#include "boxes/peers/edit_peer_permissions_box.h" #include "data/business/data_business_common.h" #include "data/business/data_business_info.h" #include "data/data_session.h" #include "data/data_user.h" +#include "lang/lang_keys.h" #include "main/main_session.h" namespace Data { @@ -41,7 +43,7 @@ void Chatbots::preload() { _settings = ChatbotsSettings{ .bot = _owner->session().data().user(botId), .recipients = FromMTP(_owner, bot.vrecipients()), - .repliesAllowed = bot.is_can_reply(), + .permissions = FromMTP(bot.vrights()), }; } else { _settings.force_assign(ChatbotsSettings()); @@ -81,11 +83,8 @@ void Chatbots::save( using Flag = MTPaccount_UpdateConnectedBot::Flag; const auto api = &_owner->session().api(); api->request(MTPaccount_UpdateConnectedBot( - MTP_flags(!settings.bot - ? Flag::f_deleted - : settings.repliesAllowed - ? Flag::f_can_reply - : Flag()), + MTP_flags(!settings.bot ? Flag::f_deleted : Flag::f_rights), + ToMTP(settings.permissions), (settings.bot ? settings.bot : was.bot)->inputUser, ForBotsToMTP(settings.recipients) )).done([=](const MTPUpdates &result) { @@ -180,4 +179,39 @@ void Chatbots::reload() { preload(); } +EditFlagsDescriptor ChatbotsPermissionsLabels() { + using Flag = ChatbotsPermission; + + using PermissionLabel = EditFlagsLabel; + auto messages = std::vector{ + { Flag::ViewMessages, tr::lng_chatbots_read(tr::now) }, + { Flag::ReplyToMessages, tr::lng_chatbots_reply(tr::now) }, + { Flag::MarkAsRead, tr::lng_chatbots_mark_as_read(tr::now) }, + { Flag::DeleteSent, tr::lng_chatbots_delete_sent(tr::now) }, + { Flag::DeleteReceived, tr::lng_chatbots_delete_received(tr::now) }, + }; + auto manage = std::vector{ + { Flag::EditName, tr::lng_chatbots_edit_name(tr::now) }, + { Flag::EditBio, tr::lng_chatbots_edit_bio(tr::now) }, + { Flag::EditUserpic, tr::lng_chatbots_edit_userpic(tr::now) }, + { Flag::EditUsername, tr::lng_chatbots_edit_username(tr::now) }, + }; + auto gifts = std::vector{ + { Flag::ViewGifts, tr::lng_chatbots_view_gifts(tr::now) }, + { Flag::SellGifts, tr::lng_chatbots_sell_gifts(tr::now) }, + { Flag::GiftSettings, tr::lng_chatbots_gift_settings(tr::now) }, + { Flag::TransferGifts, tr::lng_chatbots_transfer_gifts(tr::now) }, + { Flag::TransferStars, tr::lng_chatbots_transfer_stars(tr::now) }, + }; + auto stories = std::vector{ + { Flag::ManageStories, tr::lng_chatbots_manage_stories(tr::now) }, + }; + return { .labels = { + { tr::lng_chatbots_manage_messages(), std::move(messages) }, + { tr::lng_chatbots_manage_profile(), std::move(manage) }, + { tr::lng_chatbots_manage_gifts(), std::move(gifts) }, + { std::nullopt, std::move(stories) }, + }, .st = nullptr }; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h index 902cdd4206..97a034845e 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.h +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -11,6 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class UserData; +template +struct EditFlagsDescriptor; + namespace Data { class Session; @@ -18,7 +21,7 @@ class Session; struct ChatbotsSettings { UserData *bot = nullptr; BusinessRecipients recipients; - bool repliesAllowed = false; + ChatbotsPermissions permissions; friend inline bool operator==( const ChatbotsSettings &, @@ -67,4 +70,7 @@ private: }; +[[nodiscard]] auto ChatbotsPermissionsLabels() +-> EditFlagsDescriptor; + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 149a3ee136..dc6619f5e7 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -159,6 +159,47 @@ BusinessRecipients FromMTP( return result; } +ChatbotsPermissions FromMTP(const MTPBusinessBotRights &rights) { + using Flag = ChatbotsPermission; + const auto &data = rights.data(); + + return Flag::ViewMessages + | (data.is_reply() ? Flag::ReplyToMessages : Flag()) + | (data.is_read_messages() ? Flag::MarkAsRead : Flag()) + | (data.is_delete_sent_messages() ? Flag::DeleteSent : Flag()) + | (data.is_delete_received_messages() ? Flag::DeleteReceived : Flag()) + | (data.is_edit_name() ? Flag::EditName : Flag()) + | (data.is_edit_bio() ? Flag::EditBio : Flag()) + | (data.is_edit_profile_photo() ? Flag::EditUserpic : Flag()) + | (data.is_edit_username() ? Flag::EditUsername : Flag()) + | (data.is_view_gifts() ? Flag::ViewGifts : Flag()) + | (data.is_sell_gifts() ? Flag::SellGifts : Flag()) + | (data.is_change_gift_settings() ? Flag::GiftSettings : Flag()) + | (data.is_transfer_and_upgrade_gifts() ? Flag::TransferGifts : Flag()) + | (data.is_transfer_stars() ? Flag::TransferStars : Flag()) + | (data.is_manage_stories() ? Flag::ManageStories : Flag()); +} + +MTPBusinessBotRights ToMTP(ChatbotsPermissions rights) { + using Flag = MTPDbusinessBotRights::Flag; + using Right = ChatbotsPermission; + return MTP_businessBotRights(MTP_flags(Flag() + | ((rights & Right::ReplyToMessages) ? Flag::f_reply : Flag()) + | ((rights & Right::MarkAsRead) ? Flag::f_read_messages : Flag()) + | ((rights & Right::DeleteSent) ? Flag::f_delete_sent_messages : Flag()) + | ((rights & Right::DeleteReceived) ? Flag::f_delete_received_messages : Flag()) + | ((rights & Right::EditName) ? Flag::f_edit_name : Flag()) + | ((rights & Right::EditBio) ? Flag::f_edit_bio : Flag()) + | ((rights & Right::EditUserpic) ? Flag::f_edit_profile_photo : Flag()) + | ((rights & Right::EditUsername) ? Flag::f_edit_username : Flag()) + | ((rights & Right::ViewGifts) ? Flag::f_view_gifts : Flag()) + | ((rights & Right::SellGifts) ? Flag::f_sell_gifts : Flag()) + | ((rights & Right::GiftSettings) ? Flag::f_change_gift_settings : Flag()) + | ((rights & Right::TransferGifts) ? Flag::f_transfer_and_upgrade_gifts : Flag()) + | ((rights & Right::TransferStars) ? Flag::f_transfer_stars : Flag()) + | ((rights & Right::ManageStories) ? Flag::f_manage_stories : Flag()))); +} + BusinessDetails FromMTP( not_null owner, const tl::conditional &hours, diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index 600d5ff79d..90ae82b074 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -57,6 +57,26 @@ enum class BusinessRecipientsType : uchar { Bots, }; +enum class ChatbotsPermission { + ViewMessages = 0x0001, + ReplyToMessages = 0x0002, + MarkAsRead = 0x0004, + DeleteSent = 0x0008, + DeleteReceived = 0x0010, + EditName = 0x0020, + EditBio = 0x0040, + EditUserpic = 0x0080, + EditUsername = 0x0100, + ViewGifts = 0x0200, + SellGifts = 0x0400, + GiftSettings = 0x0800, + TransferGifts = 0x1000, + TransferStars = 0x2000, + ManageStories = 0x4000, +}; +inline constexpr bool is_flag_type(ChatbotsPermission) { return true; } +using ChatbotsPermissions = base::flags; + [[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP( const BusinessRecipients &data); [[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP( @@ -67,6 +87,9 @@ enum class BusinessRecipientsType : uchar { [[nodiscard]] BusinessRecipients FromMTP( not_null owner, const MTPBusinessBotRecipients &recipients); +[[nodiscard]] ChatbotsPermissions FromMTP( + const MTPBusinessBotRights &rights); +[[nodiscard]] MTPBusinessBotRights ToMTP(ChatbotsPermissions rights); struct Timezone { QString id; diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index c039247326..6fc7cbd5a1 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/sponsored_messages.h" #include "api/api_text_entities.h" +#include "api/api_peer_search.h" // SponsoredSearchResult #include "apiwrap.h" #include "core/click_handler_types.h" #include "data/data_channel.h" @@ -37,6 +38,19 @@ constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000); return (received > 0) && (received + kRequestTimeLimit > crl::now()); } +template +[[nodiscard]] std::vector Prepare(const Fields &fields) { + using InfoList = std::vector; + return (!fields.sponsorInfo.text.isEmpty() + && !fields.additionalInfo.text.isEmpty()) + ? InfoList{ fields.sponsorInfo, fields.additionalInfo } + : !fields.sponsorInfo.text.isEmpty() + ? InfoList{ fields.sponsorInfo } + : !fields.additionalInfo.text.isEmpty() + ? InfoList{ fields.additionalInfo } + : InfoList{}; +} + } // namespace SponsoredMessages::SponsoredMessages(not_null session) @@ -523,17 +537,16 @@ void SponsoredMessages::view(const FullMsgId &fullId) { if (!entryPtr) { return; } - const auto randomId = entryPtr->sponsored.randomId; + view(entryPtr->sponsored.randomId); +} + +void SponsoredMessages::view(const QByteArray &randomId) { auto &request = _viewRequests[randomId]; if (request.requestId || TooEarlyForRequest(request.lastReceived)) { return; } request.requestId = _session->api().request( - MTPmessages_ViewSponsoredMessage( - entryPtr->item - ? entryPtr->item->history()->peer->input - : _session->data().peer(fullId.peer)->input, - MTP_bytes(randomId)) + MTPmessages_ViewSponsoredMessage(MTP_bytes(randomId)) ).done([=] { auto &request = _viewRequests[randomId]; request.lastReceived = crl::now(); @@ -550,18 +563,8 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails( return {}; } const auto &data = entryPtr->sponsored; - - using InfoList = std::vector; - auto info = (!data.sponsorInfo.text.isEmpty() - && !data.additionalInfo.text.isEmpty()) - ? InfoList{ data.sponsorInfo, data.additionalInfo } - : !data.sponsorInfo.text.isEmpty() - ? InfoList{ data.sponsorInfo } - : !data.additionalInfo.text.isEmpty() - ? InfoList{ data.additionalInfo } - : InfoList{}; return { - .info = std::move(info), + .info = Prepare(data), .link = data.link, .buttonText = data.from.buttonText, .photoId = data.from.photoId, @@ -574,6 +577,14 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails( }; } +SponsoredMessages::Details SponsoredMessages::lookupDetails( + const Api::SponsoredSearchResult &data) const { + return { + .info = Prepare(data), + .canReport = true, + }; +} + void SponsoredMessages::clicked( const FullMsgId &fullId, bool isMedia, @@ -582,22 +593,45 @@ void SponsoredMessages::clicked( if (!entryPtr) { return; } - const auto randomId = entryPtr->sponsored.randomId; + clicked(entryPtr->sponsored.randomId, isMedia, isFullscreen); +} + +void SponsoredMessages::clicked( + const QByteArray &randomId, + bool isMedia, + bool isFullscreen) { using Flag = MTPmessages_ClickSponsoredMessage::Flag; _session->api().request(MTPmessages_ClickSponsoredMessage( MTP_flags(Flag(0) | (isMedia ? Flag::f_media : Flag(0)) | (isFullscreen ? Flag::f_fullscreen : Flag(0))), - entryPtr->item - ? entryPtr->item->history()->peer->input - : _session->data().peer(fullId.peer)->input, MTP_bytes(randomId) )).send(); } +SponsoredReportAction SponsoredMessages::createReportCallback( + const FullMsgId &fullId) { + const auto entry = find(fullId); + if (!entry) { + return { .callback = [=](const auto &...) {} }; + } + const auto history = _session->data().history(fullId.peer); + const auto erase = [=] { + const auto it = _data.find(history); + if (it != end(_data)) { + auto &list = it->second.entries; + const auto proj = [&](const Entry &e) { + return e.itemFullId == fullId; + }; + list.erase(ranges::remove_if(list, proj), end(list)); + } + }; + return createReportCallback(entry->sponsored.randomId, erase); +} -auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) --> Fn)> { +SponsoredReportAction SponsoredMessages::createReportCallback( + const QByteArray &randomId, + Fn erase) { using TLChoose = MTPDchannels_sponsoredMessageReportResultChooseOption; using TLAdsHidden = MTPDchannels_sponsoredMessageReportResultAdsHidden; using TLReported = MTPDchannels_sponsoredMessageReportResultReported; @@ -613,25 +647,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) }; const auto state = std::make_shared(); - return [=](Result::Id optionId, Fn done) { - const auto entry = find(fullId); - if (!entry) { - return; - } - - const auto history = _session->data().history(fullId.peer); - - const auto erase = [=] { - const auto it = _data.find(history); - if (it != end(_data)) { - auto &list = it->second.entries; - const auto proj = [&](const Entry &e) { - return e.itemFullId == fullId; - }; - list.erase(ranges::remove_if(list, proj), end(list)); - } - }; - + return { .callback = [=](Result::Id optionId, Fn done) { if (optionId == Result::Id("-1")) { erase(); return; @@ -639,8 +655,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) state->requestId = _session->api().request( MTPmessages_ReportSponsoredMessage( - history->peer->input, - MTP_bytes(entry->sponsored.randomId), + MTP_bytes(randomId), MTP_bytes(optionId)) ).done([=]( const MTPchannels_SponsoredMessageReportResult &result, @@ -677,7 +692,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) done({ .error = error.type() }); } }).send(); - }; + } }; } SponsoredMessages::State SponsoredMessages::state( diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.h b/Telegram/SourceFiles/data/components/sponsored_messages.h index ebcefb9e20..6c875c081c 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.h +++ b/Telegram/SourceFiles/data/components/sponsored_messages.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class History; +namespace Api { +struct SponsoredSearchResult; +} // namespace Api + namespace Main { class Session; } // namespace Main @@ -69,6 +73,25 @@ struct SponsoredMessage { TextWithEntities additionalInfo; }; +struct SponsoredMessageDetails { + std::vector info; + QString link; + QString buttonText; + PhotoId photoId = PhotoId(0); + PhotoId mediaPhotoId = PhotoId(0); + DocumentId mediaDocumentId = DocumentId(0); + uint64 backgroundEmojiId = 0; + uint8 colorIndex : 6 = 0; + bool isLinkInternal = false; + bool canReport = false; +}; + +struct SponsoredReportAction { + Fn)> callback; +}; + class SponsoredMessages final { public: enum class AppendResult { @@ -82,18 +105,7 @@ public: InjectToMiddle, AppendToTopBar, }; - struct Details { - std::vector info; - QString link; - QString buttonText; - PhotoId photoId = PhotoId(0); - PhotoId mediaPhotoId = PhotoId(0); - DocumentId mediaDocumentId = DocumentId(0); - uint64 backgroundEmojiId = 0; - uint8 colorIndex : 6 = 0; - bool isLinkInternal = false; - bool canReport = false; - }; + using Details = SponsoredMessageDetails; using RandomId = QByteArray; explicit SponsoredMessages(not_null session); ~SponsoredMessages(); @@ -103,7 +115,13 @@ public: void request(not_null history, Fn done); void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; + [[nodiscard]] Details lookupDetails( + const Api::SponsoredSearchResult &data) const; void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen); + void clicked( + const QByteArray &randomId, + bool isMedia, + bool isFullscreen); [[nodiscard]] FullMsgId fillTopBar( not_null history, not_null widget); @@ -117,11 +135,15 @@ public: int fallbackWidth); void view(const FullMsgId &fullId); + void view(const QByteArray &randomId); [[nodiscard]] State state(not_null history) const; - [[nodiscard]] auto createReportCallback(const FullMsgId &fullId) - -> Fn)>; + [[nodiscard]] SponsoredReportAction createReportCallback( + const FullMsgId &fullId); + [[nodiscard]] SponsoredReportAction createReportCallback( + const QByteArray &randomId, + Fn erase); void clear(); diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index f44ae72bbf..39a654d3c0 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -96,27 +96,28 @@ struct PeerUpdate { PersonalChannel = (1ULL << 34), StarRefProgram = (1ULL << 35), PaysPerMessage = (1ULL << 36), + GiftSettings = (1ULL << 37), // For chats and channels - InviteLinks = (1ULL << 37), - Members = (1ULL << 38), - Admins = (1ULL << 39), - BannedUsers = (1ULL << 40), - Rights = (1ULL << 41), - PendingRequests = (1ULL << 42), - Reactions = (1ULL << 43), + InviteLinks = (1ULL << 38), + Members = (1ULL << 39), + Admins = (1ULL << 40), + BannedUsers = (1ULL << 41), + Rights = (1ULL << 42), + PendingRequests = (1ULL << 43), + Reactions = (1ULL << 44), // For channels - ChannelAmIn = (1ULL << 44), - StickersSet = (1ULL << 45), - EmojiSet = (1ULL << 46), - ChannelLinkedChat = (1ULL << 47), - ChannelLocation = (1ULL << 48), - Slowmode = (1ULL << 49), - GroupCall = (1ULL << 50), + ChannelAmIn = (1ULL << 45), + StickersSet = (1ULL << 46), + EmojiSet = (1ULL << 47), + ChannelLinkedChat = (1ULL << 48), + ChannelLocation = (1ULL << 49), + Slowmode = (1ULL << 50), + GroupCall = (1ULL << 51), // For iteration - LastUsedBit = (1ULL << 50), + LastUsedBit = (1ULL << 51), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 29b9cb451a..81b3330972 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -118,7 +118,10 @@ bool CanSendAnyOf( not_null peer, ChatRestrictions rights, bool forbidInForums) { - if (const auto user = peer->asUser()) { + if (peer->session().frozen() + && !peer->isFreezeAppealChat()) { + return false; + } else if (const auto user = peer->asUser()) { if (user->isInaccessible() || user->isRepliesChat() || user->isVerifyCodes()) { @@ -178,7 +181,13 @@ SendError RestrictionError( not_null peer, ChatRestriction restriction) { using Flag = ChatRestriction; - if (const auto restricted = peer->amRestricted(restriction)) { + if (peer->session().frozen() + && !peer->isFreezeAppealChat()) { + return SendError({ + .text = tr::lng_frozen_restrict_title(tr::now), + .frozen = true, + }); + } else if (const auto restricted = peer->amRestricted(restriction)) { if (const auto user = peer->asUser()) { if (user->requiresPremiumToWrite() && !user->session().premium()) { diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index bfa24fb869..b3db584a4e 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -191,16 +191,19 @@ struct SendError { QString text; int boostsToLift = 0; bool premiumToLift = false; + bool frozen = false; }; SendError(Args &&args) : text(std::move(args.text)) , boostsToLift(args.boostsToLift) - , premiumToLift(args.premiumToLift) { + , premiumToLift(args.premiumToLift) + , frozen(args.frozen) { } QString text; int boostsToLift = 0; bool premiumToLift = false; + bool frozen = false; [[nodiscard]] SendError value_or(SendError other) const { return *this ? *this : other; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 02c1299872..97ef0e6d65 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -171,8 +171,19 @@ struct AlbumCounts { } template -[[nodiscard]] uint64 CountCacheKey(not_null data, bool spoiler) { - return (reinterpret_cast(data.get()) & ~1) | (spoiler ? 1 : 0); +[[nodiscard]] uint64 CountCacheKey( + not_null data, + ImageRoundRadius radius, + bool spoiler) { + return (reinterpret_cast(data.get()) & ~3) + | ((radius == ImageRoundRadius::Ellipse) ? 2 : 0) + | (spoiler ? 1 : 0); +} + +[[nodiscard]] uint64 SimpleCacheKey(ImageRoundRadius radius, bool spoiler) { + return uint64() + | ((radius == ImageRoundRadius::Ellipse) ? 2 : 0) + | (spoiler ? 1 : 0); } [[nodiscard]] ItemPreviewImage PreparePhotoPreviewImage( @@ -181,7 +192,7 @@ template ImageRoundRadius radius, bool spoiler) { const auto photo = media->owner(); - const auto counted = CountCacheKey(photo, spoiler); + const auto counted = CountCacheKey(photo, radius, spoiler); if (const auto small = media->image(PhotoSize::Small)) { return { PreparePreviewImage(small, radius, spoiler), counted }; } else if (const auto thumbnail = media->image(PhotoSize::Thumbnail)) { @@ -191,15 +202,15 @@ template } const auto allowedToDownload = media->autoLoadThumbnailAllowed( item->history()->peer); - const auto spoilered = uint64(spoiler ? 1 : 0); - const auto cacheKey = allowedToDownload ? spoilered : counted; + const auto simple = SimpleCacheKey(radius, spoiler); + const auto cacheKey = allowedToDownload ? simple : counted; if (allowedToDownload) { media->owner()->load(PhotoSize::Small, item->fullId()); } if (const auto blurred = media->thumbnailInline()) { return { PreparePreviewImage(blurred, radius, spoiler), cacheKey }; } - return { QImage(), allowedToDownload ? spoilered : cacheKey }; + return { QImage(), allowedToDownload ? simple : cacheKey }; } [[nodiscard]] ItemPreviewImage PrepareFilePreviewImage( @@ -210,7 +221,7 @@ template Expects(media->owner()->hasThumbnail()); const auto document = media->owner(); - const auto readyCacheKey = CountCacheKey(document, spoiler); + const auto readyCacheKey = CountCacheKey(document, radius, spoiler); if (const auto thumbnail = media->thumbnail()) { return { PreparePreviewImage(thumbnail, radius, spoiler), @@ -218,11 +229,11 @@ template }; } document->loadThumbnail(item->fullId()); - const auto spoilered = uint64(spoiler ? 1 : 0); + const auto simple = SimpleCacheKey(radius, spoiler); if (const auto blurred = media->thumbnailInline()) { - return { PreparePreviewImage(blurred, radius, spoiler), spoilered }; + return { PreparePreviewImage(blurred, radius, spoiler), simple }; } - return { QImage(), spoilered }; + return { QImage(), simple }; } [[nodiscard]] QImage PutPlayIcon(QImage preview) { @@ -274,13 +285,14 @@ template [[nodiscard]] ItemPreviewImage FindCachedPreview( const std::vector *existing, not_null data, + ImageRoundRadius radius, bool spoiler) { if (!existing) { return {}; } const auto i = ranges::find( *existing, - CountCacheKey(data, spoiler), + CountCacheKey(data, radius, spoiler), &ItemPreviewImage::cacheKey); return (i != end(*existing)) ? *i : ItemPreviewImage(); } @@ -856,13 +868,17 @@ ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const { } auto images = std::vector(); auto context = std::any(); - if (auto found = FindCachedPreview(options.existing, _photo, _spoiler)) { + const auto radius = _chat + ? ImageRoundRadius::Ellipse + : ImageRoundRadius::Small; + if (auto found = FindCachedPreview( + options.existing, + _photo, + radius, + _spoiler)) { images.push_back(std::move(found)); } else { const auto media = _photo->createMediaView(); - const auto radius = _chat - ? ImageRoundRadius::Ellipse - : ImageRoundRadius::Small; if (auto prepared = PreparePhotoPreview( parent(), media, @@ -1101,18 +1117,24 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const { auto images = std::vector(); auto context = std::any(); const auto existing = options.existing; - if (auto found = FindCachedPreview(existing, _document, _spoiler)) { + const auto spoilered = _spoiler + || (_document->isVideoMessage() && ttlSeconds()); + const auto radius = _document->isVideoMessage() + ? ImageRoundRadius::Ellipse + : ImageRoundRadius::Small; + if (auto found = FindCachedPreview( + existing, + _document, + radius, + spoilered)) { images.push_back(std::move(found)); } else if (TryFilePreview(_document)) { const auto media = _document->createMediaView(); - const auto radius = _document->isVideoMessage() - ? ImageRoundRadius::Ellipse - : ImageRoundRadius::Small; if (auto prepared = PrepareFilePreview( parent(), media, radius, - _spoiler) + spoilered) ; prepared || !prepared.cacheKey) { images.push_back(std::move(prepared)); if (!prepared.cacheKey) { @@ -1809,13 +1831,17 @@ ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const { || _page->type == WebPageType::Video || _page->type == WebPageType::Document; if (pageTypeWithPreview || !_page->collage.items.empty()) { - if (auto found = FindCachedPreview(options.existing, _page, false)) { + const auto radius = ImageRoundRadius::Small; + if (auto found = FindCachedPreview( + options.existing, + _page, + radius, + false)) { return { .text = caption, .images = { std::move(found) } }; } auto context = std::any(); auto images = std::vector(); auto prepared = ItemPreviewImage(); - const auto radius = ImageRoundRadius::Small; if (const auto photo = MediaWebPage::photo()) { const auto media = photo->createMediaView(); prepared = PreparePhotoPreview(parent(), media, radius, false); @@ -2063,10 +2089,18 @@ ItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const { if (!photo && !document) { continue; } else if (images.size() < kMaxPreviewImages) { - auto found = photo - ? FindCachedPreview(existing, not_null(photo), spoiler) - : FindCachedPreview(existing, not_null(document), spoiler); const auto radius = ImageRoundRadius::Small; + auto found = photo + ? FindCachedPreview( + existing, + not_null(photo), + radius, + spoiler) + : FindCachedPreview( + existing, + not_null(document), + radius, + spoiler); if (found) { images.push_back(std::move(found)); } else if (photo) { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 19140fbaa5..ccc3eb53f1 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1319,6 +1319,10 @@ bool PeerData::isVerifyCodes() const { return (id == kVerifyCodesId); } +bool PeerData::isFreezeAppealChat() const { + return username().compare(u"spambot"_q, Qt::CaseInsensitive) == 0; +} + bool PeerData::sharedMediaInfo() const { return isSelf() || isRepliesChat(); } @@ -1453,6 +1457,7 @@ bool PeerData::canRevokeFullHistory() const { if (const auto user = asUser()) { return !isSelf() && (!user->isBot() || user->isSupport()) + && !user->isInaccessible() && session().serverConfig().revokePrivateInbox && (session().serverConfig().revokePrivateTimeLimit == 0x7FFFFFFF); } else if (const auto chat = asChat()) { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 21aad4cde0..c3f48eb1b6 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -235,6 +235,7 @@ public: [[nodiscard]] bool isGigagroup() const; [[nodiscard]] bool isRepliesChat() const; [[nodiscard]] bool isVerifyCodes() const; + [[nodiscard]] bool isFreezeAppealChat() const; [[nodiscard]] bool sharedMediaInfo() const; [[nodiscard]] bool savedSublistsInfo() const; [[nodiscard]] bool hasStoriesHidden() const; diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index 1b9a8ca063..c529be692e 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -69,6 +69,7 @@ struct StarGift { TimeId lastSaleDate = 0; bool upgradable = false; bool birthday = false; + bool soldOut = false; friend inline bool operator==( const StarGift &, diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 9c28fad86c..c1c83d0808 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "api/api_credits.h" +#include "api/api_global_privacy.h" #include "api/api_sensitive_content.h" #include "api/api_statistics.h" #include "storage/localstorage.h" @@ -673,6 +674,13 @@ bool UserData::hasCalls() const { && (callsStatus() != CallsStatus::Unknown); } +void UserData::setDisallowedGiftTypes(Api::DisallowedGiftTypes types) { + if (_disallowedGiftTypes != types) { + _disallowedGiftTypes = types; + session().changes().peerUpdated(this, UpdateFlag::GiftSettings); + } +} + namespace Data { void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { @@ -829,6 +837,31 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setBotVerifyDetails( ParseBotVerifyDetails(update.vbot_verification())); + if (const auto gifts = update.vdisallowed_gifts()) { + const auto &data = gifts->data(); + user->setDisallowedGiftTypes(Api::DisallowedGiftType() + | (data.is_disallow_unlimited_stargifts() + ? Api::DisallowedGiftType::Unlimited + : Api::DisallowedGiftType()) + | (data.is_disallow_limited_stargifts() + ? Api::DisallowedGiftType::Limited + : Api::DisallowedGiftType()) + | (data.is_disallow_unique_stargifts() + ? Api::DisallowedGiftType::Unique + : Api::DisallowedGiftType()) + | (data.is_disallow_premium_gifts() + ? Api::DisallowedGiftType::Premium + : Api::DisallowedGiftType()) + | (update.is_display_gifts_button() + ? Api::DisallowedGiftType::SendHide + : Api::DisallowedGiftType())); + } else { + user->setDisallowedGiftTypes(Api::DisallowedGiftTypes() + | (update.is_display_gifts_button() + ? Api::DisallowedGiftType::SendHide + : Api::DisallowedGiftType())); + } + user->owner().stories().apply(user, update.vstories()); user->fullUpdated(); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 515a333135..5e57eaef90 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -15,12 +15,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_lastseen_status.h" #include "data/data_user_names.h" #include "dialogs/dialogs_key.h" +#include "base/flags.h" namespace Data { struct BotCommand; struct BusinessDetails; } // namespace Data +namespace Api { +enum class DisallowedGiftType : uchar; +using DisallowedGiftTypes = base::flags; +} // namespace Api + struct StarRefProgram { StarsAmount revenuePerUser; TimeId endDate = 0; @@ -262,6 +268,11 @@ public: std::unique_ptr botInfo; + [[nodiscard]] Api::DisallowedGiftTypes disallowedGiftTypes() const { + return _disallowedGiftTypes; + } + void setDisallowedGiftTypes(Api::DisallowedGiftTypes types); + private: auto unavailableReasons() const -> const std::vector & override; @@ -293,6 +304,8 @@ private: static constexpr auto kInaccessibleAccessHashOld = 0xFFFFFFFFFFFFFFFFULL; + Api::DisallowedGiftTypes _disallowedGiftTypes; + }; namespace Data { diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index d9203debdc..75db74f85d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -24,6 +24,10 @@ DialogRow { unreadMarkDiameter: pixels; tagTop: pixels; } +DialogRightButton { + button: RoundButton; + margin: margins; +} ThreeStateIcon { icon: icon; @@ -115,11 +119,16 @@ dialogRowFilterTagSkip: 4px; dialogRowFilterTagStyle: TextStyle(defaultTextStyle) { font: font(10px); } -dialogRowOpenBotTextStyle: semiboldTextStyle; -dialogRowOpenBotHeight: 20px; -dialogRowOpenBotRight: 10px; -dialogRowOpenBotTop: 32px; -dialogRowOpenBotRecentTop: 28px; +dialogRowOpenBot: DialogRightButton { + button: RoundButton(defaultActiveButton) { + height: 20px; + textTop: 1px; + } + margin: margins(0px, 32px, 10px, 0px); +} +dialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) { + margin: margins(0px, 32px, 28px, 0px); +} forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }}; forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }}; @@ -789,3 +798,18 @@ dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px); dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) { minWidth: 128px; } + +dialogsQuickActionSize: 20px; +dialogsQuickActionRippleSize: 80px; + +dialogsSponsoredButton: DialogRightButton(dialogRowOpenBot) { + button: RoundButton(defaultLightButton) { + textFg: windowActiveTextFg; + textFgOver: windowActiveTextFg; + textBg: lightButtonBgOver; + textBgOver: lightButtonBgOver; + height: 20px; + textTop: 1px; + } + margin: margins(0px, 9px, 10px, 0px); +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_common.h b/Telegram/SourceFiles/dialogs/dialogs_common.h index 4d6523f975..c1e1cd755d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_common.h +++ b/Telegram/SourceFiles/dialogs/dialogs_common.h @@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +namespace style { +struct DialogRightButton; +} // namespace style + namespace Ui { class RippleAnimation; } // namespace Ui @@ -114,11 +118,16 @@ struct RowsByLetter { }; struct RightButton final { + const style::DialogRightButton *st = nullptr; QImage bg; QImage selectedBg; QImage activeBg; Ui::Text::String text; std::unique_ptr ripple; + + explicit operator bool() const { + return st != nullptr; + } }; } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index e636b0a9d8..8b79a9452b 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_search_tags.h" +#include "dialogs/dialogs_quick_action.h" #include "history/view/history_view_context_menu.h" #include "history/history.h" #include "history/history_item.h" @@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/rect.h" #include "ui/ui_utility.h" +#include "data/components/sponsored_messages.h" #include "data/data_drafts.h" #include "data/data_folder.h" #include "data/data_forum.h" @@ -54,12 +56,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "base/options.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" #include "mainwindow.h" #include "mainwidget.h" #include "storage/storage_account.h" #include "apiwrap.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "menu/menu_sponsored.h" #include "window/notifications_manager.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -242,10 +246,17 @@ struct InnerWidget::HashtagResult { BasicRow row; }; +struct InnerWidget::SponsoredSearchResult { + Api::SponsoredSearchResult data; + RightButton button; +}; + struct InnerWidget::PeerSearchResult { explicit PeerSearchResult(not_null peer) : peer(peer) { } + not_null peer; + std::unique_ptr sponsored; mutable Ui::Text::String name; mutable Ui::PeerBadge badge; BasicRow row; @@ -286,6 +297,12 @@ InnerWidget::InnerWidget( _topicJumpCache = nullptr; _chatsFilterTags.clear(); _rightButtons.clear(); + _pressedRightButtonData = nullptr; + for (const auto &result : _peerSearchResults) { + if (const auto sponsored = result->sponsored.get()) { + sponsored->button = {}; + } + } }, lifetime()); session().downloaderTaskFinished( @@ -833,13 +850,36 @@ void InnerWidget::paintEvent(QPaintEvent *e) { bool mayBeActive) { const auto &key = row->key(); const auto active = mayBeActive && isRowActive(row, activeEntry); - const auto forum = key.history() && key.history()->isForum(); + const auto history = key.history(); + const auto forum = history && history->isForum(); if (forum && !_topicJumpCache) { _topicJumpCache = std::make_unique(); } const auto expanding = forum - && (key.history()->peer->id == childListShown.peerId); + && (history->peer->id == childListShown.peerId); context.rightButton = maybeCacheRightButton(row); + if (history) { + if (_activeQuickAction + && (_activeQuickAction->data.msgBareId + == history->peer->id.value)) { + context.quickActionContext = _activeQuickAction.get(); + } else if (!_inactiveQuickActions.empty()) { + auto it = _inactiveQuickActions.begin(); + while (it != _inactiveQuickActions.end()) { + const auto raw = it->get(); + if (raw->finishedAt + && (ms - raw->finishedAt + > st::defaultRippleAnimation.hideDuration)) { + _inactiveQuickActions.erase(it); + } else { + if (raw->data.msgBareId == history->peer->id.value) { + context.quickActionContext = raw; + } + ++it; + } + } + } + } context.st = (forum ? &st::forumDialogRow : _st.get()); @@ -864,7 +904,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } if (active && (filter.flags() & Data::ChatFilter::Flag::NoRead) - && !filter.contains(key.history(), true)) { + && !filter.contains(history, true)) { // Hack for History::fakeUnreadWhileOpened(). continue; } @@ -919,6 +959,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { && _selectedTopicJump && (!_pressed || _pressedTopicJump); Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), context); + if (context.quickActionContext) { + context.quickActionContext = nullptr; + } }; if (_state == WidgetState::Default) { const auto collapsedSkip = collapsedRowsOffset(); @@ -1108,15 +1151,36 @@ void InnerWidget::paintEvent(QPaintEvent *e) { const auto activePeer = activeEntry.key.peer(); for (; from < to; ++from) { const auto &result = _peerSearchResults[from]; + if (result->sponsored + && r.y() <= (skip + from * st::dialogsRowHeight) + && r.y() + r.height() >= (skip + (from + 1) * st::dialogsRowHeight)) { + session().sponsoredMessages().view( + result->sponsored->data.randomId); + } const auto peer = result->peer; const auto active = !activeEntry.fullId && activePeer && ((peer == activePeer) || (peer->migrateTo() == activePeer)); - const auto selected = (from == (isPressed() + const auto selected = (from == ((_peerSearchMenu >= 0) + ? _peerSearchMenu + : isPressed() ? _peerSearchPressed : _peerSearchSelected)); + if (result->sponsored + && result->sponsored->button.text.isEmpty()) { + fillRightButton( + result->sponsored->button, + tr::lng_search_sponsored_button( + tr::now, + Ui::Text::WithEntities), + st::dialogsSponsoredButton); + } + paintPeerSearchResult(p, result.get(), { + .rightButton = (result->sponsored + ? &result->sponsored->button + : nullptr), .st = &st::defaultDialogRow, .currentBg = currentBg(), .now = ms, @@ -1274,36 +1338,47 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } } +void InnerWidget::fillRightButton( + RightButton &button, + const TextWithEntities &text, + const style::DialogRightButton &st) { + button.st = &st; + button.text.setMarkedText(st.button.style, text); + const auto size = QSize( + button.text.maxWidth() + button.text.minHeight(), + st.button.height); + const auto generateBg = [&](const style::color &c) { + auto bg = QImage( + style::DevicePixelRatio() * size, + QImage::Format_ARGB32_Premultiplied); + bg.setDevicePixelRatio(style::DevicePixelRatio()); + bg.fill(Qt::transparent); + { + auto p = QPainter(&bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(c); + const auto r = size.height() / 2; + p.drawRoundedRect(Rect(size), r, r); + } + return bg; + }; + button.bg = generateBg(st.button.textBg); + button.selectedBg = generateBg(st.button.textBgOver); + button.activeBg = generateBg(st.button.textFg); +} + [[nodiscard]] RightButton *InnerWidget::maybeCacheRightButton(Row *row) { if (const auto user = MaybeBotWithApp(row)) { const auto it = _rightButtons.find(user->id); if (it == _rightButtons.end()) { auto rightButton = RightButton(); - const auto text = tr::lng_profile_open_app_short(tr::now); - rightButton.text.setText(st::dialogRowOpenBotTextStyle, text); - const auto size = QSize( - rightButton.text.maxWidth() - + rightButton.text.minHeight(), - st::dialogRowOpenBotHeight); - const auto generateBg = [&](const style::color &c) { - auto bg = QImage( - style::DevicePixelRatio() * size, - QImage::Format_ARGB32_Premultiplied); - bg.setDevicePixelRatio(style::DevicePixelRatio()); - bg.fill(Qt::transparent); - { - auto p = QPainter(&bg); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(c); - const auto r = size.height() / 2; - p.drawRoundedRect(Rect(size), r, r); - } - return bg; - }; - rightButton.bg = generateBg(st::activeButtonBg); - rightButton.selectedBg = generateBg(st::activeButtonBgOver); - rightButton.activeBg = generateBg(st::activeButtonFg); + fillRightButton( + rightButton, + tr::lng_profile_open_app_short( + tr::now, + Ui::Text::WithEntities), + st::dialogRowOpenBot); return &(_rightButtons.emplace( user->id, std::move(rightButton)).first->second); @@ -1426,7 +1501,11 @@ void InnerWidget::paintPeerSearchResult( context.st->photoSize); auto nameleft = context.st->nameLeft; - auto namewidth = context.width - nameleft - context.st->padding.right(); + auto available = context.width - nameleft - context.st->padding.right(); + auto namewidth = available; + if (const auto used = Ui::PaintRightButton(p, context)) { + namewidth -= used - st::dialogsUnreadPadding; + } QRect rectForName(nameleft, context.st->nameTop, namewidth, st::semiboldFont->height); if (result->name.isEmpty()) { @@ -1586,7 +1665,7 @@ void InnerWidget::clearIrrelevantState() { _filteredSelected = -1; setFilteredPressed(-1, false, false); _peerSearchSelected = -1; - setPeerSearchPressed(-1); + setPeerSearchPressed(-1, false); _previewSelected = -1; setPreviewPressed(-1); _searchedSelected = -1; @@ -1605,20 +1684,28 @@ bool InnerWidget::lookupIsInBotAppButton( if (const auto user = MaybeBotWithApp(row)) { const auto it = _rightButtons.find(user->id); if (it != _rightButtons.end()) { - const auto s = it->second.bg.size() / style::DevicePixelRatio(); - const auto r = QRect( - width() - s.width() - st::dialogRowOpenBotRight, - st::dialogRowOpenBotTop, - s.width(), - s.height()); - if (r.contains(localPosition)) { - return true; - } + return lookupIsInRightButton(it->second, localPosition); } } return false; } +bool InnerWidget::lookupIsInRightButton( + const RightButton &button, + QPoint localPosition) { + if (!button.st) { + return false; + } + + const auto s = button.bg.size() / style::DevicePixelRatio(); + const auto r = QRect( + width() - s.width() - button.st->margin.right(), + button.st->margin.top(), + s.width(), + s.height()); + return r.contains(localPosition); +} + void InnerWidget::selectByMouse(QPoint globalPosition) { const auto local = mapFromGlobal(globalPosition); if (updateReorderPinned(local)) { @@ -1662,16 +1749,16 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { const auto mappedY = selected ? mouseY - offset - selected->top() : 0; const auto selectedTopicJump = selected && selected->lookupIsInTopicJump(local.x(), mappedY); - const auto selectedBotApp = selected + const auto selectedRightButton = selected && lookupIsInBotAppButton(selected, QPoint(local.x(), mappedY)); if (_collapsedSelected != collapsedSelected || _selected != selected || _selectedTopicJump != selectedTopicJump - || _selectedBotApp != selectedBotApp) { + || _selectedRightButton != selectedRightButton) { updateSelectedRow(); _selected = selected; _selectedTopicJump = selectedTopicJump; - _selectedBotApp = selectedBotApp; + _selectedRightButton = selectedRightButton; _collapsedSelected = collapsedSelected; updateSelectedRow(); setCursor((_selected || _collapsedSelected >= 0) @@ -1711,29 +1798,39 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { && _filterResults[filteredSelected].row->lookupIsInTopicJump( local.x(), mappedY); - const auto selectedBotApp = (filteredSelected >= 0) + const auto selectedRightButton = (filteredSelected >= 0) && lookupIsInBotAppButton( _filterResults[filteredSelected].row, QPoint(local.x(), mappedY)); if (_filteredSelected != filteredSelected || _selectedTopicJump != selectedTopicJump - || _selectedBotApp != selectedBotApp) { + || _selectedRightButton != selectedRightButton) { updateSelectedRow(); _filteredSelected = filteredSelected; _selectedTopicJump = selectedTopicJump; - _selectedBotApp = selectedBotApp; + _selectedRightButton = selectedRightButton; updateSelectedRow(); } } if (!_peerSearchResults.empty()) { - auto skip = peerSearchOffset(); + const auto skip = peerSearchOffset(); auto peerSearchSelected = (mouseY >= skip) ? ((mouseY - skip) / st::dialogsRowHeight) : -1; if (peerSearchSelected < 0 || peerSearchSelected >= _peerSearchResults.size()) { peerSearchSelected = -1; } - if (_peerSearchSelected != peerSearchSelected) { + const auto mappedY = (peerSearchSelected >= 0) + ? mouseY - skip - (peerSearchSelected * st::dialogsRowHeight) + : 0; + const auto selectedRightButton = (peerSearchSelected >= 0) + && _peerSearchResults[peerSearchSelected]->sponsored + && lookupIsInRightButton( + _peerSearchResults[peerSearchSelected]->sponsored->button, + QPoint(local.x(), mappedY)); + if (_peerSearchSelected != peerSearchSelected + || _selectedRightButton != selectedRightButton) { updateSelectedRow(); _peerSearchSelected = peerSearchSelected; + _selectedRightButton = selectedRightButton; updateSelectedRow(); } } @@ -1820,12 +1917,15 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { selectByMouse(e->globalPos()); _pressButton = e->button(); - setPressed(_selected, _selectedTopicJump, _selectedBotApp); + setPressed(_selected, _selectedTopicJump, _selectedRightButton); setCollapsedPressed(_collapsedSelected); setHashtagPressed(_hashtagSelected); _hashtagDeletePressed = _hashtagDeleteSelected; - setFilteredPressed(_filteredSelected, _selectedTopicJump, _selectedBotApp); - setPeerSearchPressed(_peerSearchSelected); + setFilteredPressed( + _filteredSelected, + _selectedTopicJump, + _selectedRightButton); + setPeerSearchPressed(_peerSearchSelected, _selectedRightButton); setPreviewPressed(_previewSelected); setSearchedPressed(_searchedSelected); _pressedMorePosts = _selectedMorePosts; @@ -1854,7 +1954,9 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { }; const auto origin = e->pos() - QPoint(0, dialogsOffset() + _pressed->top()); - if (addBotAppRipple(origin, updateCallback)) { + if ((_pressButton == Qt::MiddleButton) + && addQuickActionRipple(row, updateCallback)) { + } else if (addRightButtonRipple(origin, updateCallback)) { } else if (_pressedTopicJump) { row->addTopicJumpRipple( origin, @@ -1881,7 +1983,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { const auto origin = e->pos() - QPoint(0, filteredOffset() + result.top); const auto updateCallback = [=] { repaintDialogRow(filterId, row); }; - if (addBotAppRipple(origin, updateCallback)) { + if (addRightButtonRipple(origin, updateCallback)) { } else if (_pressedTopicJump) { row->addTopicJumpRipple( origin, @@ -1896,11 +1998,19 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { } } else if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) { auto &result = _peerSearchResults[_peerSearchPressed]; - auto row = &result->row; - row->addRipple( - e->pos() - QPoint(0, peerSearchOffset() + _peerSearchPressed * st::dialogsRowHeight), - QSize(width(), st::dialogsRowHeight), - [this, peer = result->peer] { updateSearchResult(peer); }); + const auto row = &result->row; + const auto origin = e->pos() + - QPoint(0, peerSearchOffset() + _peerSearchPressed * st::dialogsRowHeight); + const auto updateCallback = [this, peer = result->peer] { + updateSearchResult(peer); + }; + if (addRightButtonRipple(origin, updateCallback)) { + } else { + row->addRipple( + origin, + QSize(width(), st::dialogsRowHeight), + updateCallback); + } } else if (base::in_range(_searchedPressed, 0, _searchResults.size())) { auto &row = _searchResults[_searchedPressed]; row->addRipple( @@ -1916,22 +2026,96 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { } } -bool InnerWidget::addBotAppRipple(QPoint origin, Fn updateCallback) { - if (!(_pressedBotApp && _pressedBotAppData)) { +bool InnerWidget::addRightButtonRipple(QPoint origin, Fn updateCallback) { + if (!(_pressedRightButton && _pressedRightButtonData)) { return false; } - const auto size = _pressedBotAppData->bg.size() + const auto size = _pressedRightButtonData->bg.size() / style::DevicePixelRatio(); - if (!_pressedBotAppData->ripple) { - _pressedBotAppData->ripple = std::make_unique( - st::defaultRippleAnimation, + if (!_pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple = std::make_unique( + _pressedRightButtonData->st->button.ripple, Ui::RippleAnimation::RoundRectMask(size, size.height() / 2), - updateCallback); + std::move(updateCallback)); } const auto shift = QPoint( - width() - size.width() - st::dialogRowOpenBotRight, - st::dialogRowOpenBotTop); - _pressedBotAppData->ripple->add(origin - shift); + width() - size.width() - _pressedRightButtonData->st->margin.right(), + _pressedRightButtonData->st->margin.top()); + _pressedRightButtonData->ripple->add(origin - shift); + return true; +} + +bool InnerWidget::addQuickActionRipple( + not_null row, + Fn updateCallback) { + if (_activeQuickAction) { + return false; + } + const auto action = Core::App().settings().quickDialogAction(); + if (action == Dialogs::Ui::QuickDialogAction::Disabled) { + return false; + } + const auto history = row->history(); + if (!history) { + return false; + } + const auto type = ResolveQuickDialogLabel(history, action, _filterId); + if (type == Dialogs::Ui::QuickDialogActionLabel::Disabled) { + return false; + } + const auto key = history->peer->id.value; + const auto context = ensureQuickAction(key); + if (context->data) { + return false; + } + + auto name = ResolveQuickDialogLottieIconName(type); + context->icon = Lottie::MakeIcon({ + .name = std::move(name), + .sizeOverride = Size(st::dialogsQuickActionSize), + }); + context->action = action; + context->icon->jumpTo(context->icon->framesCount() - 1, [=] { + const auto size = QSize( + st::dialogsQuickActionRippleSize, + row->height()); + const auto isRemovingFromList + = (action == Dialogs::Ui::QuickDialogAction::Archive); + if (!context->ripple) { + context->ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RectMask(size), + isRemovingFromList + ? Fn([=] { update(); }) + : updateCallback); + } + if (!context->rippleFg) { + context->rippleFg = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::MaskByDrawer( + size, + true, + [&](QPainter &p) { + p.setCompositionMode( + QPainter::CompositionMode_Source); + p.fillRect(Rect(size), Qt::transparent); + DrawQuickAction( + p, + Rect(size), + context->icon.get(), + ResolveQuickDialogLabel( + row->history(), + action, + _filterId)); + }), + isRemovingFromList + ? Fn([=] { update(); }) + : std::move(updateCallback)); + } + context->ripple->add(QPoint(size.width() / 2, size.height() / 2)); + context->rippleFg->add(QPoint(size.width() / 2, size.height() / 2)); + }); + return true; } @@ -1950,12 +2134,17 @@ void InnerWidget::checkReorderPinnedStart(QPoint localPosition) { if (!_pressed || _dragging || (_state != WidgetState::Default) - || _pressedBotApp) { + || _pressedRightButtonData) { return; } else if (qAbs(localPosition.y() - _dragStart.y()) < style::ConvertScale(kStartReorderThreshold)) { return; } + if ((_pressButton == Qt::MiddleButton) + && (Core::App().settings().quickDialogAction() + != Dialogs::Ui::QuickDialogAction::Disabled)) { + return; + } _dragging = _pressed; startReorderPinned(localPosition); } @@ -2215,7 +2404,7 @@ void InnerWidget::mousePressReleased( setCollapsedPressed(-1); const auto pressedTopicRootId = _pressedTopicJumpRootId; const auto pressedTopicJump = _pressedTopicJump; - const auto pressedBotApp = _pressedBotApp; + const auto pressedRightButton = _pressedRightButton; auto pressed = _pressed; clearPressed(); auto hashtagPressed = _hashtagPressed; @@ -2225,7 +2414,7 @@ void InnerWidget::mousePressReleased( auto filteredPressed = _filteredPressed; setFilteredPressed(-1, false, false); auto peerSearchPressed = _peerSearchPressed; - setPeerSearchPressed(-1); + setPeerSearchPressed(-1, false); auto previewPressed = _previewPressed; setPreviewPressed(-1); auto searchedPressed = _searchedPressed; @@ -2237,8 +2426,28 @@ void InnerWidget::mousePressReleased( if (wasDragging) { selectByMouse(globalPosition); } - if (_pressedBotAppData && _pressedBotAppData->ripple) { - _pressedBotAppData->ripple->lastStop(); + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); + } + if (_activeQuickAction && pressed && !_activeQuickAction->data) { + if (const auto history = pressed->history()) { + const auto raw = _activeQuickAction.get(); + if (raw->ripple) { + raw->ripple->lastStop(); + } + if (raw->rippleFg) { + raw->rippleFg->lastStop(); + } + + if (pressed == _selected) { + PerformQuickDialogAction( + _controller, + history->peer, + raw->action, + _filterId); + } + deactivateQuickAction(); + } } updateSelectedRow(); if (!wasDragging && button == Qt::LeftButton) { @@ -2246,13 +2455,14 @@ void InnerWidget::mousePressReleased( || (pressed && pressed == _selected && pressedTopicJump == _selectedTopicJump - && pressedBotApp == _selectedBotApp) + && pressedRightButton == _selectedRightButton) || (hashtagPressed >= 0 && hashtagPressed == _hashtagSelected && hashtagDeletePressed == _hashtagDeleteSelected) || (filteredPressed >= 0 && filteredPressed == _filteredSelected) || (peerSearchPressed >= 0 - && peerSearchPressed == _peerSearchSelected) + && peerSearchPressed == _peerSearchSelected + && pressedRightButton == _selectedRightButton) || (previewPressed >= 0 && previewPressed == _previewSelected) || (searchedPressed >= 0 @@ -2261,13 +2471,15 @@ void InnerWidget::mousePressReleased( && pressedMorePosts == _selectedMorePosts) || (pressedChatTypeFilter && pressedChatTypeFilter == _selectedChatTypeFilter)) { - if (pressedBotApp && (pressed || filteredPressed >= 0)) { + if (pressedRightButton && (pressed || filteredPressed >= 0)) { const auto &row = pressed ? pressed : _filterResults[filteredPressed].row.get(); if (const auto user = MaybeBotWithApp(row)) { _openBotMainAppRequests.fire(peerToUser(user->id)); } + } else if (pressedRightButton && peerSearchPressed >= 0) { + showSponsoredMenu(peerSearchPressed, globalPosition); } else { chooseRow(modifiers, pressedTopicRootId); } @@ -2294,25 +2506,25 @@ void InnerWidget::setCollapsedPressed(int pressed) { void InnerWidget::setPressed( Row *pressed, bool pressedTopicJump, - bool pressedBotApp) { + bool pressedRightButton) { if ((_pressed != pressed) || (pressed && _pressedTopicJump != pressedTopicJump) - || (pressed && _pressedBotApp != pressedBotApp)) { + || (pressed && _pressedRightButton != pressedRightButton)) { if (_pressed) { _pressed->stopLastRipple(); } - if (_pressedBotAppData && _pressedBotAppData->ripple) { - _pressedBotAppData->ripple->lastStop(); + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); } _pressed = pressed; - if (pressed || !pressedTopicJump || !pressedBotApp) { + if (pressed || !pressedTopicJump || !pressedRightButton) { _pressedTopicJump = pressedTopicJump; - _pressedBotApp = pressedBotApp; - if (pressedBotApp) { + _pressedRightButton = pressedRightButton; + if (pressedRightButton) { if (const auto user = MaybeBotWithApp(pressed)) { const auto it = _rightButtons.find(user->id); if (it != _rightButtons.end()) { - _pressedBotAppData = &(it->second); + _pressedRightButtonData = &(it->second); } } } @@ -2339,26 +2551,26 @@ void InnerWidget::setHashtagPressed(int pressed) { void InnerWidget::setFilteredPressed( int pressed, bool pressedTopicJump, - bool pressedBotApp) { + bool pressedRightButton) { if (_filteredPressed != pressed || (pressed >= 0 && _pressedTopicJump != pressedTopicJump) - || (pressed >= 0 && _pressedBotApp != pressedBotApp)) { + || (pressed >= 0 && _pressedRightButton != pressedRightButton)) { if (base::in_range(_filteredPressed, 0, _filterResults.size())) { _filterResults[_filteredPressed].row->stopLastRipple(); } - if (_pressedBotAppData && _pressedBotAppData->ripple) { - _pressedBotAppData->ripple->lastStop(); + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); } _filteredPressed = pressed; - if (pressed >= 0 || !pressedTopicJump || !pressedBotApp) { + if (pressed >= 0 || !pressedTopicJump || !pressedRightButton) { _pressedTopicJump = pressedTopicJump; - _pressedBotApp = pressedBotApp; - if (pressed >= 0 && pressedBotApp) { + _pressedRightButton = pressedRightButton; + if (pressed >= 0 && pressedRightButton) { const auto &row = _filterResults[pressed].row; if (const auto history = row->history()) { const auto it = _rightButtons.find(history->peer->id); if (it != _rightButtons.end()) { - _pressedBotAppData = &(it->second); + _pressedRightButtonData = &(it->second); } } } @@ -2371,11 +2583,26 @@ void InnerWidget::setFilteredPressed( } } -void InnerWidget::setPeerSearchPressed(int pressed) { - if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) { - _peerSearchResults[_peerSearchPressed]->row.stopLastRipple(); +void InnerWidget::setPeerSearchPressed(int pressed, bool pressedRightButton) { + if (_peerSearchPressed != pressed + || (pressed >= 0 && _pressedRightButton != pressedRightButton)) { + if (base::in_range(_peerSearchPressed, 0, _peerSearchResults.size())) { + _peerSearchResults[_peerSearchPressed]->row.stopLastRipple(); + } + if (_pressedRightButtonData && _pressedRightButtonData->ripple) { + _pressedRightButtonData->ripple->lastStop(); + } + _peerSearchPressed = pressed; + if (pressed >= 0 || !pressedRightButton) { + _pressedRightButton = pressedRightButton; + if (pressed >= 0 && pressedRightButton) { + const auto &entry = _peerSearchResults[pressed]; + if (entry->sponsored) { + _pressedRightButtonData = &entry->sponsored->button; + } + } + } } - _peerSearchPressed = pressed; } void InnerWidget::setPreviewPressed(int pressed) { @@ -2438,7 +2665,7 @@ void InnerWidget::dialogRowReplaced( _selected = newRow; } if (_pressed == oldRow) { - setPressed(newRow, _pressedTopicJump, _pressedBotApp); + setPressed(newRow, _pressedTopicJump, _pressedRightButton); } if (_dragging == oldRow) { if (newRow) { @@ -2836,6 +3063,11 @@ bool InnerWidget::showChatPreview() { void InnerWidget::chatPreviewShown(bool shown, RowDescriptor row) { _chatPreviewScheduled = false; if (shown) { + const auto chosen = computeChosenRow(); + if (!chosen.sponsoredRandomId.isEmpty() && row.key == chosen.key) { + auto &messages = session().sponsoredMessages(); + messages.clicked(chosen.sponsoredRandomId, false, false); + } _chatPreviewRow = row; if (base::take(_chatPreviewTouchGlobal)) { _touchCancelRequests.fire({}); @@ -2950,6 +3182,62 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) { } } +void InnerWidget::showSponsoredMenu(int peerSearchIndex, QPoint globalPos) { + _menu = nullptr; + + const auto count = int(_peerSearchResults.size()); + const auto entry = (peerSearchIndex >= 0 && peerSearchIndex < count) + ? _peerSearchResults[peerSearchIndex].get() + : nullptr; + if (!entry || !entry->sponsored) { + return; + } + + _peerSearchMenu = peerSearchIndex; + _menu = base::make_unique_q( + this, + st::popupMenuExpandedSeparator); + const auto peer = entry->peer; + const auto remove = crl::guard(this, [=] { + _sponsoredRemoved.emplace(peer); + _peerSearchResults.erase( + ranges::remove( + _peerSearchResults, + peer, + &PeerSearchResult::peer), + end(_peerSearchResults)); + refresh(); + }); + Menu::FillSponsored( + this, + Ui::Menu::CreateAddActionCallback(_menu), + _controller->uiShow(), + Menu::SponsoredPhrases::Search, + session().sponsoredMessages().lookupDetails(entry->sponsored->data), + session().sponsoredMessages().createReportCallback( + entry->sponsored->data.randomId, + remove), + false, + false); + QObject::connect(_menu.get(), &QObject::destroyed, [=] { + if (_peerSearchMenu >= 0 + && _peerSearchMenu < _peerSearchResults.size()) { + const auto index = std::exchange(_peerSearchMenu, -1); + updateSearchResult(_peerSearchResults[index]->peer); + } + const auto globalPosition = QCursor::pos(); + if (rect().contains(mapFromGlobal(globalPosition))) { + setMouseTracking(true); + selectByMouse(globalPosition); + } + }); + if (_menu->empty()) { + _menu = nullptr; + } else { + _menu->popup(globalPos); + } +} + void InnerWidget::parentGeometryChanged() { const auto globalPosition = QCursor::pos(); if (rect().contains(mapFromGlobal(globalPosition))) { @@ -2984,7 +3272,7 @@ bool InnerWidget::processTouchEvent(not_null e) { } if (_chatPreviewTouchGlobal) { const auto delta = (*_chatPreviewTouchGlobal - *point); - if (delta.manhattanLength() > _st->photoSize) { + if (delta.manhattanLength() >= QApplication::startDragDistance()) { cancelChatPreview(); } } @@ -2993,7 +3281,7 @@ bool InnerWidget::processTouchEvent(not_null e) { return _dragging != nullptr; } else if (_touchDragStartGlobal) { const auto delta = (*_touchDragStartGlobal - *point); - if (delta.manhattanLength() > QApplication::startDragDistance()) { + if (delta.manhattanLength() >= QApplication::startDragDistance()) { if (_touchDragPinnedTimer.isActive()) { _touchDragPinnedTimer.cancel(); _touchDragStartGlobal = {}; @@ -3231,14 +3519,23 @@ InnerWidget::~InnerWidget() { clearSearchResults(); } -void InnerWidget::clearSearchResults(bool clearPeerSearchResults) { - if (clearPeerSearchResults) { - _peerSearchResults.clear(); +void InnerWidget::clearSearchResults(bool alsoPeerSearchResults) { + if (alsoPeerSearchResults) { + clearPeerSearchResults(); } _searchResults.clear(); _searchedCount = _searchedMigratedCount = 0; } +void InnerWidget::clearPeerSearchResults() { + _peerSearchResults.clear(); + if (_pressedRightButtonSponsored) { + _pressedRightButtonData = nullptr; + _pressedRightButtonSponsored = false; + _pressedRightButton = false; + } +} + void InnerWidget::clearPreviewResults() { _previewResults.clear(); _previewCount = 0; @@ -3554,40 +3851,45 @@ void InnerWidget::searchReceived( refresh(); } -void InnerWidget::peerSearchReceived( - const QString &query, - const QVector &my, - const QVector &result) { +void InnerWidget::peerSearchReceived(Api::PeerSearchResult result) { if (_state != WidgetState::Filtered) { return; } - _peerSearchQuery = query.toLower().trimmed(); - _peerSearchResults.clear(); - _peerSearchResults.reserve(result.size()); - for (const auto &mtpPeer : my) { - if (const auto peer = session().data().peerLoaded(peerFromMTP(mtpPeer))) { - appendToFiltered(peer->owner().history(peer)); - } else { - LOG(("API Error: " - "user %1 was not loaded in InnerWidget::peopleReceived()" - ).arg(peerFromMTP(mtpPeer).value)); - } + _peerSearchQuery = result.query.toLower().trimmed(); + clearPeerSearchResults(); + _peerSearchResults.reserve(result.peers.size() + + result.sponsored.size()); + for (const auto &peer : result.my) { + appendToFiltered(peer->owner().history(peer)); } - for (const auto &mtpPeer : result) { - if (const auto peer = session().data().peerLoaded(peerFromMTP(mtpPeer))) { - if (const auto history = peer->owner().historyLoaded(peer)) { - if (history->inChatList()) { - continue; // skip existing chats - } - } - _peerSearchResults.push_back(std::make_unique( - peer)); - } else { - LOG(("API Error: " - "user %1 was not loaded in InnerWidget::peopleReceived()" - ).arg(peerFromMTP(mtpPeer).value)); + const auto inlist = [&](not_null peer) { + if (const auto history = peer->owner().historyLoaded(peer)) { + // Skip existing chats. + return history->inChatList(); } + return false; + }; + auto added = base::flat_set>(); + for (const auto &sponsored : result.sponsored) { + const auto peer = sponsored.peer; + if (inlist(peer) || _sponsoredRemoved.contains(peer)) { + continue; + } + _peerSearchResults.push_back( + std::make_unique(peer)); + _peerSearchResults.back()->sponsored + = std::make_unique(SponsoredSearchResult{ + .data = sponsored, + }); + added.emplace(peer); + } + for (const auto &peer : result.peers) { + if (added.contains(peer) || inlist(peer)) { + continue; + } + _peerSearchResults.push_back( + std::make_unique(peer)); } refresh(); } @@ -3921,7 +4223,7 @@ void InnerWidget::clearFilter() { _hashtagResults.clear(); _filterResults.clear(); _filterResultsGlobal.clear(); - _peerSearchResults.clear(); + clearPeerSearchResults(); _searchResults.clear(); _previewResults.clear(); _trackedHistories.clear(); @@ -4377,10 +4679,13 @@ ChosenRow InnerWidget::computeChosenRow() const { .filteredRow = true, }; } else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) { - const auto peer = _peerSearchResults[_peerSearchSelected]->peer; + const auto row = _peerSearchResults[_peerSearchSelected].get(); return { - .key = session().data().history(peer), - .message = Data::UnreadMessagePosition + .key = session().data().history(row->peer), + .message = Data::UnreadMessagePosition, + .sponsoredRandomId = (row->sponsored + ? row->sponsored->data.randomId + : QByteArray()), }; } else if (base::in_range(_previewSelected, 0, _previewResults.size())) { const auto result = _previewResults[_previewSelected].get(); @@ -5004,4 +5309,100 @@ rpl::producer InnerWidget::openBotMainAppRequests() const { return _openBotMainAppRequests.events(); } +void InnerWidget::setSwipeContextData( + int64 key, + std::optional data) { + if (!key) { + return; + } + if (!data) { + _activeQuickAction = nullptr; + return; + } + const auto context = ensureQuickAction(key); + + context->data = base::take(*data); + if (context->data.msgBareId) { + constexpr auto kStartAnimateThreshold = 0.32; + constexpr auto kResetAnimateThreshold = 0.24; + if (context->data.ratio > kStartAnimateThreshold) { + if (context->icon + && !context->icon->frameIndex() + && !context->icon->animating()) { + context->icon->animate( + [=] { update(); }, + 0, + context->icon->framesCount()); + } + } else if (context->data.ratio < kResetAnimateThreshold) { + if (context->icon + && context->icon->frameIndex()) { + context->icon->jumpTo(0, [=] { update(); }); + } + } + update(); + } +} + +not_null InnerWidget::ensureQuickAction(int64 key) { + Expects(key != 0); + + if (_activeQuickAction) { + if (_activeQuickAction->data.msgBareId == key) { + return _activeQuickAction.get(); + } else { + deactivateQuickAction(); + } + } + _activeQuickAction = std::make_unique(); + _activeQuickAction->data.msgBareId = key; + return _activeQuickAction.get(); +} + +int64 InnerWidget::calcSwipeKey(int top) { + top -= dialogsOffset(); + for (auto it = _shownList->begin(); it != _shownList->end(); ++it) { + const auto row = it->get(); + const auto from = row->top(); + const auto to = from + row->height(); + if (top >= from && top < to) { + if (const auto peer = row->key().peer()) { + return peer->id.value; + } + return 0; + } + } + return 0; +} + +void InnerWidget::prepareQuickAction( + int64 key, + Dialogs::Ui::QuickDialogAction action) { + Expects(key != 0); + + const auto context = ensureQuickAction(key); + auto name = ResolveQuickDialogLottieIconName( + ResolveQuickDialogLabel( + session().data().history(PeerId(key)), + action, + _filterId)); + context->icon = Lottie::MakeIcon({ + .name = std::move(name), + .sizeOverride = Size(st::dialogsQuickActionSize), + }); + context->action = action; +} + +void InnerWidget::clearQuickActions() { + _inactiveQuickActions.clear(); +} + +void InnerWidget::deactivateQuickAction() { + if (_activeQuickAction) { + _activeQuickAction->finishedAt = crl::now(); + _inactiveQuickActions.push_back( + QuickActionPtr{ _activeQuickAction.release() }); + } +} + } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 2500f43d1f..9842faf67a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "base/timer.h" #include "dialogs/dialogs_key.h" +#include "dialogs/ui/dialogs_quick_action_context.h" #include "data/data_messages.h" #include "ui/dragging_scroll_manager.h" #include "ui/effects/animations.h" @@ -19,12 +20,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace style { struct DialogRow; +struct DialogRightButton; } // namespace style +namespace Api { +struct PeerSearchResult; +} // namespace Api + namespace MTP { class Error; } // namespace MTP +namespace Lottie { +class Icon; +} // namespace Lottie + namespace Main { class Session; } // namespace Main @@ -34,6 +44,9 @@ class IconButton; class PopupMenu; class FlatLabel; struct ScrollToRequest; +namespace Controls { +enum class QuickDialogAction; +} // namespace Controls } // namespace Ui namespace Window { @@ -70,6 +83,7 @@ enum class ChatTypeFilter : uchar; struct ChosenRow { Key key; Data::MessagePosition message; + QByteArray sponsoredRandomId; bool userpicClick : 1 = false; bool filteredRow : 1 = false; bool newWindow : 1 = false; @@ -118,10 +132,7 @@ public: HistoryItem *inject, SearchRequestType type, int fullCount); - void peerSearchReceived( - const QString &query, - const QVector &my, - const QVector &result); + void peerSearchReceived(Api::PeerSearchResult result); [[nodiscard]] FilterId filterId() const; @@ -208,6 +219,13 @@ public: [[nodiscard]] rpl::producer openBotMainAppRequests() const; + void setSwipeContextData( + int64 key, + std::optional data); + [[nodiscard]] int64 calcSwipeKey(int top); + void prepareQuickAction(int64 key, Dialogs::Ui::QuickDialogAction); + void clearQuickActions(); + protected: void visibleTopBottomUpdated( int visibleTop, @@ -225,6 +243,7 @@ protected: private: struct CollapsedRow; struct HashtagResult; + struct SponsoredSearchResult; struct PeerSearchResult; struct TagCache; @@ -282,6 +301,7 @@ private: void repaintDialogRow(RowDescriptor row); void refreshDialogRow(RowDescriptor row); bool updateEntryHeight(not_null entry); + void showSponsoredMenu(int peerSearchIndex, QPoint globalPos); void clearMouseSelection(bool clearSelection = false); void mousePressReleased( @@ -295,14 +315,17 @@ private: void scrollToItem(int top, int height); void scrollToDefaultSelected(); void setCollapsedPressed(int pressed); - void setPressed(Row *pressed, bool pressedTopicJump, bool pressedBotApp); + void setPressed( + Row *pressed, + bool pressedTopicJump, + bool pressedRightButton); void clearPressed(); void setHashtagPressed(int pressed); void setFilteredPressed( int pressed, bool pressedTopicJump, - bool pressedBotApp); - void setPeerSearchPressed(int pressed); + bool pressedRightButton); + void setPeerSearchPressed(int pressed, bool pressedRightButton); void setPreviewPressed(int pressed); void setSearchedPressed(int pressed); bool isPressed() const { @@ -338,6 +361,9 @@ private: void repaintDialogRowCornerStatus(not_null history); bool addBotAppRipple(QPoint origin, Fn updateCallback); + bool addQuickActionRipple(not_null row, Fn updateCallback); + + bool addRightButtonRipple(QPoint origin, Fn updateCallback); void setupShortcuts(); RowDescriptor computeJump( @@ -441,7 +467,8 @@ private: Ui::VideoUserpic *validateVideoUserpic(not_null history); Row *shownRowByKey(Key key); - void clearSearchResults(bool clearPeerSearchResults = true); + void clearSearchResults(bool alsoPeerSearchResults = true); + void clearPeerSearchResults(); void clearPreviewResults(); void updateSelectedRow(Key key = Key()); void trackResultsHistory(not_null history); @@ -468,10 +495,21 @@ private: void saveChatsFilterScrollState(FilterId filterId); void restoreChatsFilterScrollState(FilterId filterId); + [[nodiscard]] not_null ensureQuickAction( + int64 key); + void deactivateQuickAction(); + [[nodiscard]] bool lookupIsInBotAppButton( Row *row, QPoint localPosition); + [[nodiscard]] bool lookupIsInRightButton( + const RightButton &button, + QPoint localPosition); [[nodiscard]] RightButton *maybeCacheRightButton(Row *row); + void fillRightButton( + RightButton &button, + const TextWithEntities &text, + const style::DialogRightButton &st); [[nodiscard]] QImage *cacheChatsFilterTag( const Data::ChatFilter &filter, @@ -507,9 +545,10 @@ private: bool _selectedTopicJump = false; bool _pressedTopicJump = false; - RightButton *_pressedBotAppData = nullptr; - bool _selectedBotApp = false; - bool _pressedBotApp = false; + RightButton *_pressedRightButtonData = nullptr; + bool _pressedRightButtonSponsored = false; + bool _selectedRightButton = false; + bool _pressedRightButton = false; Row *_dragging = nullptr; int _draggingIndex = -1; @@ -544,9 +583,11 @@ private: rpl::lifetime _trackedLifetime; QString _peerSearchQuery; + base::flat_set> _sponsoredRemoved; std::vector> _peerSearchResults; int _peerSearchSelected = -1; int _peerSearchPressed = -1; + int _peerSearchMenu = -1; std::vector> _previewResults; int _previewCount = 0; @@ -611,6 +652,10 @@ private: rpl::event_stream<> _refreshHashtagsRequests; rpl::event_stream _openBotMainAppRequests; + using QuickActionPtr = std::unique_ptr; + QuickActionPtr _activeQuickAction; + std::vector _inactiveQuickActions; + RowDescriptor _chatPreviewRow; bool _chatPreviewScheduled = false; std::optional _chatPreviewTouchGlobal; diff --git a/Telegram/SourceFiles/dialogs/dialogs_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_list.cpp index ce6a724625..e3c8044413 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_list.cpp @@ -198,7 +198,7 @@ Row *List::rowAtY(int y) const { List::iterator List::findByY(int y) const { return ranges::lower_bound(_rows, y, ranges::less(), [](const Row *row) { - return row->top() + row->height(); + return row->top() + row->height() - 1; }); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_quick_action.cpp b/Telegram/SourceFiles/dialogs/dialogs_quick_action.cpp new file mode 100644 index 0000000000..340a9f3b6e --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_quick_action.cpp @@ -0,0 +1,245 @@ +/* +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 "dialogs/dialogs_quick_action.h" + +#include "dialogs/ui/dialogs_quick_action_context.h" +#include "apiwrap.h" +#include "data/data_histories.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "dialogs/dialogs_entry.h" +#include "history/history.h" +#include "lang/lang_instance.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "menu/menu_mute.h" +#include "window/window_peer_menu.h" +#include "window/window_session_controller.h" +#include "styles/style_dialogs.h" + +namespace Dialogs { +namespace { + +const style::font &SwipeActionFont( + Dialogs::Ui::QuickDialogActionLabel action, + int availableWidth) { + struct Entry final { + Dialogs::Ui::QuickDialogActionLabel action; + QString langId; + style::font font; + }; + static auto Fonts = std::vector(); + for (auto &entry : Fonts) { + if (entry.action == action) { + if (entry.langId == Lang::GetInstance().id()) { + return entry.font; + } + } + } + constexpr auto kNormalFontSize = 13; + constexpr auto kMinFontSize = 5; + for (auto i = kNormalFontSize; i >= kMinFontSize; --i) { + auto font = style::font( + style::ConvertScale(i, style::Scale()), + st::semiboldFont->flags(), + st::semiboldFont->family()); + if (font->width(ResolveQuickDialogLabel(action)) <= availableWidth + || i == kMinFontSize) { + Fonts.emplace_back(Entry{ + .action = action, + .langId = Lang::GetInstance().id(), + .font = std::move(font), + }); + return Fonts.back().font; + } + } + Unexpected("SwipeActionFont: can't find font."); +} + +} // namespace + +void PerformQuickDialogAction( + not_null controller, + not_null peer, + Ui::QuickDialogAction action, + FilterId filterId) { + const auto history = peer->owner().history(peer); + if (action == Dialogs::Ui::QuickDialogAction::Mute) { + const auto isMuted = rpl::variable( + MuteMenu::ThreadDescriptor(history).isMutedValue()).current(); + MuteMenu::ThreadDescriptor(history).updateMutePeriod(isMuted + ? 0 + : std::numeric_limits::max()); + } else if (action == Dialogs::Ui::QuickDialogAction::Pin) { + const auto entry = (Dialogs::Entry*)(history); + Window::TogglePinnedThread(controller, entry, filterId); + } else if (action == Dialogs::Ui::QuickDialogAction::Read) { + if (Window::IsUnreadThread(history)) { + Window::MarkAsReadThread(history); + } else if (history) { + peer->owner().histories().changeDialogUnreadMark(history, true); + } + } else if (action == Dialogs::Ui::QuickDialogAction::Archive) { + history->session().api().toggleHistoryArchived( + history, + !Window::IsArchived(history), + [] {}); + } else if (action == Dialogs::Ui::QuickDialogAction::Delete) { + Window::DeleteAndLeaveHandler(controller, peer)(); + } +} + +QString ResolveQuickDialogLottieIconName(Ui::QuickDialogActionLabel action) { + switch (action) { + case Ui::QuickDialogActionLabel::Mute: + return u"swipe_mute"_q; + case Ui::QuickDialogActionLabel::Unmute: + return u"swipe_unmute"_q; + case Ui::QuickDialogActionLabel::Pin: + return u"swipe_pin"_q; + case Ui::QuickDialogActionLabel::Unpin: + return u"swipe_unpin"_q; + case Ui::QuickDialogActionLabel::Read: + return u"swipe_read"_q; + case Ui::QuickDialogActionLabel::Unread: + return u"swipe_unread"_q; + case Ui::QuickDialogActionLabel::Archive: + return u"swipe_archive"_q; + case Ui::QuickDialogActionLabel::Unarchive: + return u"swipe_unarchive"_q; + case Ui::QuickDialogActionLabel::Delete: + return u"swipe_delete"_q; + default: + return u"swipe_disabled"_q; + } +} + +Ui::QuickDialogActionLabel ResolveQuickDialogLabel( + not_null history, + Ui::QuickDialogAction action, + FilterId filterId) { + if (action == Dialogs::Ui::QuickDialogAction::Mute) { + if (history->peer->isSelf()) { + return Ui::QuickDialogActionLabel::Disabled; + } + const auto isMuted = rpl::variable( + MuteMenu::ThreadDescriptor(history).isMutedValue()).current(); + return isMuted + ? Ui::QuickDialogActionLabel::Unmute + : Ui::QuickDialogActionLabel::Mute; + } else if (action == Dialogs::Ui::QuickDialogAction::Pin) { + const auto entry = (Dialogs::Entry*)(history); + return entry->isPinnedDialog(filterId) + ? Ui::QuickDialogActionLabel::Unpin + : Ui::QuickDialogActionLabel::Pin; + } else if (action == Dialogs::Ui::QuickDialogAction::Read) { + const auto unread = Window::IsUnreadThread(history); + if (history->isForum() && !unread) { + return Ui::QuickDialogActionLabel::Disabled; + } + return unread + ? Ui::QuickDialogActionLabel::Read + : Ui::QuickDialogActionLabel::Unread; + } else if (action == Dialogs::Ui::QuickDialogAction::Archive) { + if (!Window::CanArchive(history, history->peer)) { + return Ui::QuickDialogActionLabel::Disabled; + } + return Window::IsArchived(history) + ? Ui::QuickDialogActionLabel::Unarchive + : Ui::QuickDialogActionLabel::Archive; + } else if (action == Dialogs::Ui::QuickDialogAction::Delete) { + return Ui::QuickDialogActionLabel::Delete; + } + return Ui::QuickDialogActionLabel::Disabled; +} + +QString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel action) { + switch (action) { + case Ui::QuickDialogActionLabel::Mute: + return tr::lng_settings_quick_dialog_action_mute(tr::now); + case Ui::QuickDialogActionLabel::Unmute: + return tr::lng_settings_quick_dialog_action_unmute(tr::now); + case Ui::QuickDialogActionLabel::Pin: + return tr::lng_settings_quick_dialog_action_pin(tr::now); + case Ui::QuickDialogActionLabel::Unpin: + return tr::lng_settings_quick_dialog_action_unpin(tr::now); + case Ui::QuickDialogActionLabel::Read: + return tr::lng_settings_quick_dialog_action_read(tr::now); + case Ui::QuickDialogActionLabel::Unread: + return tr::lng_settings_quick_dialog_action_unread(tr::now); + case Ui::QuickDialogActionLabel::Archive: + return tr::lng_settings_quick_dialog_action_archive(tr::now); + case Ui::QuickDialogActionLabel::Unarchive: + return tr::lng_settings_quick_dialog_action_unarchive(tr::now); + case Ui::QuickDialogActionLabel::Delete: + return tr::lng_settings_quick_dialog_action_delete(tr::now); + default: + return tr::lng_settings_quick_dialog_action_disabled(tr::now); + }; +} + +const style::color &ResolveQuickActionBg( + Ui::QuickDialogActionLabel action) { + switch (action) { + case Ui::QuickDialogActionLabel::Delete: + return st::attentionButtonFg; + case Ui::QuickDialogActionLabel::Disabled: + return st::windowSubTextFgOver; + case Ui::QuickDialogActionLabel::Mute: + case Ui::QuickDialogActionLabel::Unmute: + case Ui::QuickDialogActionLabel::Pin: + case Ui::QuickDialogActionLabel::Unpin: + case Ui::QuickDialogActionLabel::Read: + case Ui::QuickDialogActionLabel::Unread: + case Ui::QuickDialogActionLabel::Archive: + case Ui::QuickDialogActionLabel::Unarchive: + default: + return st::windowBgActive; + }; +} + +const style::color &ResolveQuickActionBgActive( + Ui::QuickDialogActionLabel action) { + return st::windowSubTextFgOver; +} + +void DrawQuickAction( + QPainter &p, + const QRect &rect, + not_null icon, + Ui::QuickDialogActionLabel label, + float64 iconRatio, + bool twoLines) { + const auto iconSize = st::dialogsQuickActionSize * iconRatio; + const auto innerHeight = iconSize * 2; + const auto top = (rect.height() - innerHeight) / 2; + icon->paint(p, rect.x() + (rect.width() - iconSize) / 2, top); + p.setPen(st::premiumButtonFg); + p.setBrush(Qt::NoBrush); + const auto availableWidth = rect.width(); + p.setFont(SwipeActionFont(label, availableWidth)); + if (twoLines) { + auto text = ResolveQuickDialogLabel(label); + const auto index = text.indexOf(' '); + if (index != -1) { + text = text.replace(index, 1, '\n'); + } + p.drawText( + QRect(rect.x(), top, availableWidth, innerHeight), + std::move(text), + style::al_bottom); + } else { + p.drawText( + QRect(rect.x(), top, availableWidth, innerHeight), + ResolveQuickDialogLabel(label), + style::al_bottom); + } +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_quick_action.h b/Telegram/SourceFiles/dialogs/dialogs_quick_action.h new file mode 100644 index 0000000000..4ba26f291d --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_quick_action.h @@ -0,0 +1,57 @@ +/* +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 + +class History; +class PeerData; + +namespace Dialogs::Ui { +enum class QuickDialogAction; +enum class QuickDialogActionLabel; +} // namespace Dialogs::Ui + +namespace Lottie { +class Icon; +} // namespace Lottie + +namespace Window { +class SessionController; +} // namespace Window + +namespace Dialogs { + +void PerformQuickDialogAction( + not_null controller, + not_null peer, + Ui::QuickDialogAction action, + FilterId filterId); + +[[nodiscard]] QString ResolveQuickDialogLottieIconName( + Ui::QuickDialogActionLabel action); + +[[nodiscard]] Ui::QuickDialogActionLabel ResolveQuickDialogLabel( + not_null history, + Ui::QuickDialogAction action, + FilterId filterId); + +[[nodiscard]] QString ResolveQuickDialogLabel(Ui::QuickDialogActionLabel); + +[[nodiscard]] const style::color &ResolveQuickActionBg( + Ui::QuickDialogActionLabel); +[[nodiscard]] const style::color &ResolveQuickActionBgActive( + Ui::QuickDialogActionLabel); + +void DrawQuickAction( + QPainter &p, + const QRect &rect, + not_null icon, + Ui::QuickDialogActionLabel label, + float64 iconRatio = 1., + bool twoLines = false); + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 9dfd18067a..9bed1f0969 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_widget.h" +#include "base/call_delayed.h" #include "base/qt/qt_key_modifiers.h" #include "base/options.h" #include "dialogs/ui/chat_search_in.h" @@ -15,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_suggestions.h" #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_search_from_controllers.h" +#include "dialogs/dialogs_quick_action.h" #include "dialogs/dialogs_key.h" #include "history/history.h" #include "history/history_item.h" @@ -35,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/more_chats_bar.h" #include "ui/controls/download_bar.h" #include "ui/controls/jump_down_button.h" +#include "ui/controls/swipe_handler.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/ui_utility.h" @@ -46,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session_settings.h" #include "api/api_chat_filters.h" #include "apiwrap.h" +#include "chat_helpers/message_field.h" #include "core/application.h" #include "core/ui_integration.h" #include "core/update_checker.h" @@ -59,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "storage/storage_domain.h" #include "data/components/recent_peers.h" +#include "data/components/sponsored_messages.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -366,6 +371,7 @@ Widget::Widget( _storiesContents.events() | rpl::flatten_latest()) : nullptr) , _searchTimer([=] { search(); }) +, _peerSearch(&controller->session(), Api::PeerSearch::Type::WithSponsored) , _singleMessageSearch(&controller->session()) { const auto makeChildListShown = [](PeerId peerId, float64 shown) { return InnerWidget::ChildListShown{ peerId, shown }; @@ -381,6 +387,10 @@ Widget::Widget( _childListPeerId.value(), _childListShown.value(), makeChildListShown))); + _scroll->heightValue() | rpl::start_with_next([=](int height) { + _inner->setMinimumHeight(height); + _inner->refresh(); + }, _inner->lifetime()); _scrollToTop->raise(); _lockUnlock->toggle(false, anim::type::instant); @@ -671,18 +681,169 @@ Widget::Widget( setupMoreChatsBar(); setupDownloadBar(); } + setupSwipeBack(); if (session().settings().dialogsFiltersEnabled() && (Core::App().settings().chatFiltersHorizontal() || !controller->enoughSpaceForFilters())) { toggleFiltersMenu(true); } + + setupFrozenAccountBar(); +} + +void Widget::setupSwipeBack() { + const auto isMainList = [=] { + const auto current = controller()->activeChatsFilterCurrent(); + const auto &chatsFilters = session().data().chatsFilters(); + if (chatsFilters.has()) { + return chatsFilters.defaultId() == current; + } + return !current; + }; + + auto update = [=](Ui::Controls::SwipeContextData data) { + if (data.translation != 0) { + if (data.translation < 0 + && _inner + && (Core::App().settings().quickDialogAction() + != Ui::QuickDialogAction::Disabled)) { + _inner->setSwipeContextData(data.msgBareId, std::move(data)); + } else { + if (!_swipeBackData.callback) { + _swipeBackData = Ui::Controls::SetupSwipeBack( + this, + []() -> std::pair { + return { + st::historyForwardChooseBg->c, + st::historyForwardChooseFg->c, + }; + }, + _swipeBackMirrored, + _swipeBackIconMirrored); + } + _swipeBackData.callback(data); + } + return; + } else { + if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } + if (_inner) { + _inner->setSwipeContextData(data.msgBareId, std::nullopt); + _inner->update(); + } + } + }; + + auto init = [=](int top, Qt::LayoutDirection direction) { + _swipeBackIconMirrored = false; + _swipeBackMirrored = false; + if (_childListShown.current()) { + return Ui::Controls::SwipeHandlerFinishData(); + } + const auto isRightToLeft = direction == Qt::RightToLeft; + const auto action = Core::App().settings().quickDialogAction(); + const auto isDisabled = action == Ui::QuickDialogAction::Disabled; + if (_inner) { + _inner->clearQuickActions(); + if (!isRightToLeft) { + if (const auto key = _inner->calcSwipeKey(top); + key && !isDisabled) { + _inner->prepareQuickAction(key, action); + return Ui::Controls::SwipeHandlerFinishData{ + .callback = [=, session = &session()] { + auto callback = [=, peerId = PeerId(key)] { + PerformQuickDialogAction( + controller(), + session->data().peer(peerId), + action, + _inner->filterId()); + }; + base::call_delayed( + st::slideWrapDuration, + session, + std::move(callback)); + }, + .msgBareId = key, + .speedRatio = 1., + .reachRatioDuration = crl::time( + st::slideWrapDuration), + .provideReachOutRatio = true, + }; + } + } + } + if (controller()->openedFolder().current()) { + if (!isRightToLeft) { + return Ui::Controls::SwipeHandlerFinishData(); + } + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + _swipeBackData = {}; + if (controller()->openedFolder().current()) { + if (!controller()->windowId().folder()) { + controller()->closeFolder(); + } + } + }); + } + if (controller()->shownForum().current()) { + if (!isRightToLeft) { + return Ui::Controls::SwipeHandlerFinishData(); + } + const auto id = controller()->windowId(); + const auto initial = id.forum(); + if (initial) { + return Ui::Controls::SwipeHandlerFinishData(); + } + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + _swipeBackData = {}; + if (const auto forum = controller()->shownForum().current()) { + controller()->closeForum(); + } + }); + } + if (isRightToLeft && isMainList()) { + _swipeBackIconMirrored = true; + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + _swipeBackIconMirrored = false; + _swipeBackData = {}; + if (isMainList()) { + showMainMenu(); + } + }); + } + if (session().data().chatsFilters().has() && isDisabled) { + _swipeBackMirrored = !isRightToLeft; + using namespace Window; + const auto next = !isRightToLeft; + if (CheckAndJumpToNearChatsFilter(controller(), next, false)) { + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + _swipeBackData = {}; + CheckAndJumpToNearChatsFilter(controller(), next, true); + }); + } + } + + return Ui::Controls::SwipeHandlerFinishData(); + }; + + Ui::Controls::SetupSwipeHandler({ + .widget = _inner, + .scroll = _scroll.data(), + .update = std::move(update), + .init = std::move(init), + }); + } void Widget::chosenRow(const ChosenRow &row) { storiesToggleExplicitExpand(false); - if (!_searchState.query.isEmpty()) { + if (!row.sponsoredRandomId.isEmpty()) { + auto &messages = session().sponsoredMessages(); + messages.clicked(row.sponsoredRandomId, false, false); + } else if (!_searchState.query.isEmpty()) { if (const auto history = row.key.history()) { session().recentPeers().bump(history->peer); } @@ -692,6 +853,7 @@ void Widget::chosenRow(const ChosenRow &row) { const auto topicJump = history ? history->peer->forumTopicFor(row.message.fullId.msg) : nullptr; + if (topicJump) { if (controller()->shownForum().current() == topicJump->forum()) { controller()->closeForum(); @@ -843,6 +1005,29 @@ void Widget::setupTouchChatPreview() { }, _inner->lifetime()); } +void Widget::setupFrozenAccountBar() { + session().frozenValue( + ) | rpl::start_with_next([=] { + updateFrozenAccountBar(); + updateControlsGeometry(); + }, lifetime()); +} + +void Widget::updateFrozenAccountBar() { + if (_layout == Layout::Child + || _openedForum + || _openedFolder + || !session().frozen()) { + _frozenAccountBar = nullptr; + } else if (!_frozenAccountBar) { + _frozenAccountBar = FrozenWriteRestriction( + this, + controller()->uiShow(), + FrozenWriteRestrictionType::DialogsList); + _frozenAccountBar->show(); + } +} + void Widget::setupMoreChatsBar() { if (_layout == Layout::Child) { return; @@ -1220,6 +1405,14 @@ void Widget::setupShortcuts() { } return true; }); + request->check(Command::ShowChatPreview, 1) + && request->handle([=] { + if (_inner) { + Window::ActivateWindow(controller()); + return _inner->showChatPreview(); + } + return true; + }); } }, lifetime()); } @@ -1258,6 +1451,9 @@ void Widget::updateControlsVisibility(bool fast) { if (_moreChatsBar) { _moreChatsBar->show(); } + if (_frozenAccountBar) { + _frozenAccountBar->show(); + } if (_chatFilters) { _chatFilters->show(); } @@ -1541,7 +1737,7 @@ void Widget::changeOpenedSubsection( change(); refreshTopBars(); updateControlsVisibility(true); - _peerSearchRequest = 0; + _peerSearch.clear(); _api.request(base::take(_topicSearchRequest)).cancel(); if (animated == anim::type::normal) { if (_connecting) { @@ -1576,6 +1772,7 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { if (_stories) { storiesExplicitCollapse(); } + updateFrozenAccountBar(); }, (folder != nullptr), animated); } @@ -1632,6 +1829,7 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { _api.request(base::take(_topicSearchRequest)).cancel(); _inner->changeOpenedForum(forum); storiesToggleExplicitExpand(false); + updateFrozenAccountBar(); updateStoriesVisibility(); }, (forum != nullptr), animated); } @@ -1963,6 +2161,9 @@ void Widget::startWidthAnimation() { } _widthAnimationCache = grabNonNarrowScrollFrame(); _scroll->hide(); + if (_frozenAccountBar) { + _frozenAccountBar->hide(); + } if (_chatFilters) { _chatFilters->hide(); } @@ -1973,6 +2174,9 @@ void Widget::stopWidthAnimation() { _widthAnimationCache = QPixmap(); if (!_showAnimation) { _scroll->setVisible(!_suggestions); + if (_frozenAccountBar) { + _frozenAccountBar->setVisible(!_suggestions); + } if (_chatFilters) { _chatFilters->setVisible(!_suggestions); } @@ -2077,6 +2281,9 @@ void Widget::startSlideAnimation( if (_moreChatsBar) { _moreChatsBar->hide(); } + if (_frozenAccountBar) { + _frozenAccountBar->hide(); + } if (_chatFilters) { _chatFilters->hide(); } @@ -2241,10 +2448,9 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { { .posts = true, .start = true }, &_postsProcess); } - _api.request(base::take(_peerSearchRequest)).cancel(); - _peerSearchQuery = QString(); - peerSearchApplyEmpty(0); + _peerSearch.clear(); _api.request(base::take(_topicSearchRequest)).cancel(); + peerSearchReceived({}); return true; } else if (inCache) { const auto success = _singleMessageSearch.lookup(query, [=] { @@ -2349,35 +2555,18 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { } else { _inner->searchRequested(false); } - const auto peerQuery = Api::ConvertPeerSearchQuery(query); - if (searchForPeersRequired(peerQuery)) { - if (inCache) { - auto i = _peerSearchCache.find(peerQuery); - if (i != _peerSearchCache.end()) { - _peerSearchQuery = peerQuery; - _peerSearchRequest = 0; - peerSearchReceived(i->second, 0); - } - } else if (_peerSearchQuery != peerQuery) { - _peerSearchQuery = peerQuery; - _peerSearchFull = false; - _peerSearchRequest = _api.request(MTPcontacts_Search( - MTP_string(_peerSearchQuery), - MTP_int(SearchPeopleLimit) - )).done([=]( - const MTPcontacts_Found &result, - mtpRequestId requestId) { - peerSearchReceived(result, requestId); - }).fail([=](const MTP::Error &error, mtpRequestId requestId) { - peerSearchFailed(error, requestId); - }).send(); - _peerSearchQueries.emplace(_peerSearchRequest, _peerSearchQuery); - } + if (peerSearchRequired()) { + const auto requestType = inCache + ? Api::PeerSearch::RequestType::CacheOnly + : Api::PeerSearch::RequestType::CacheOrRemote; + _peerSearch.request(query, [=](Api::PeerSearchResult result) { + peerSearchReceived(result); + }, requestType); } else { - _api.request(base::take(_peerSearchRequest)).cancel(); - _peerSearchQuery = peerQuery; - peerSearchApplyEmpty(0); + _peerSearch.clear(); + peerSearchReceived({}); } + const auto peerQuery = Api::ConvertPeerSearchQuery(query); if (searchForTopicsRequired(peerQuery)) { if (inCache) { if (_topicSearchQuery != peerQuery) { @@ -2396,11 +2585,8 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { return result; } -bool Widget::searchForPeersRequired(const QString &query) const { - return _searchState.filterChatsList() - && !_openedForum - && !query.isEmpty() - && (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None); +bool Widget::peerSearchRequired() const { + return _searchState.filterChatsList() && !_openedForum; } bool Widget::searchForTopicsRequired(const QString &query) const { @@ -2800,31 +2986,10 @@ void Widget::searchReceived( update(); } -void Widget::peerSearchReceived( - const MTPcontacts_Found &result, - mtpRequestId requestId) { - const auto state = _inner->state(); - auto q = _peerSearchQuery; - if (state == WidgetState::Filtered) { - auto i = _peerSearchQueries.find(requestId); - if (i != _peerSearchQueries.end()) { - _peerSearchCache[i->second] = result; - _peerSearchQueries.erase(i); - } - } - if (_peerSearchRequest == requestId) { - switch (result.type()) { - case mtpc_contacts_found: { - auto &d = result.c_contacts_found(); - session().data().processUsers(d.vusers()); - session().data().processChats(d.vchats()); - _inner->peerSearchReceived(q, d.vmy_results().v, d.vresults().v); - } break; - } - - _peerSearchRequest = 0; - listScrollUpdated(); - } +void Widget::peerSearchReceived(Api::PeerSearchResult result) { + _inner->peerSearchReceived(std::move(result)); + listScrollUpdated(); + update(); } void Widget::searchApplyEmpty( @@ -2840,17 +3005,6 @@ void Widget::searchApplyEmpty( process); } -void Widget::peerSearchApplyEmpty(mtpRequestId id) { - _peerSearchFull = true; - peerSearchReceived( - MTP_contacts_found( - MTP_vector(0), - MTP_vector(0), - MTP_vector(0), - MTP_vector(0)), - id); -} - void Widget::searchFailed( SearchRequestType type, const MTP::Error &error, @@ -2863,13 +3017,6 @@ void Widget::searchFailed( } } -void Widget::peerSearchFailed(const MTP::Error &error, mtpRequestId id) { - if (_peerSearchRequest == id) { - _peerSearchRequest = 0; - _peerSearchFull = true; - } -} - void Widget::dragEnterEvent(QDragEnterEvent *e) { using namespace Storage; @@ -3294,12 +3441,7 @@ bool Widget::applySearchState(SearchState state) { clearSearchCache(searchCleared); } if (state.query.isEmpty()) { - _peerSearchCache.clear(); - const auto queries = base::take(_peerSearchQueries); - for (const auto &[requestId, query] : queries) { - _api.request(requestId).cancel(); - } - _peerSearchQuery = QString(); + _peerSearch.clear(); } if (_searchState.query != currentSearchQuery()) { @@ -3357,8 +3499,8 @@ void Widget::clearSearchCache(bool clearPosts) { _topicSearchQuery = QString(); _topicSearchOffsetDate = 0; _topicSearchOffsetId = _topicSearchOffsetTopicId = 0; - _api.request(base::take(_peerSearchRequest)).cancel(); _api.request(base::take(_topicSearchRequest)).cancel(); + _peerSearch.clear(); cancelSearchRequest(); } @@ -3660,9 +3802,17 @@ void Widget::updateControlsGeometry() { if (_chatFilters) { _chatFilters->resizeToWidth(barw); } + if (_frozenAccountBar) { + _frozenAccountBar->resize(barw, _frozenAccountBar->height()); + } _updateScrollGeometryCached = [=] { - const auto moreChatsBarTop = expandedStoriesTop + const auto frozenBarTop = expandedStoriesTop + ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded); + if (_frozenAccountBar) { + _frozenAccountBar->move(0, frozenBarTop); + } + const auto moreChatsBarTop = frozenBarTop + + (_frozenAccountBar ? _frozenAccountBar->height() : 0); if (_moreChatsBar) { _moreChatsBar->move(0, moreChatsBarTop); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 46da4b6954..7d231d3c77 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_peer_search.h" #include "base/timer.h" #include "dialogs/dialogs_key.h" #include "window/section_widget.h" +#include "ui/controls/swipe_handler_data.h" #include "ui/effects/animations.h" #include "ui/userpic_view.h" #include "mtproto/sender.h" @@ -187,9 +189,7 @@ private: const MTPmessages_Messages &result, not_null process, bool cacheResults = false); - void peerSearchReceived( - const MTPcontacts_Found &result, - mtpRequestId requestId); + void peerSearchReceived(Api::PeerSearchResult result); void escape(); void submit(); void cancelSearchRequest(); @@ -200,17 +200,19 @@ private: void setupSupportMode(); void setupTouchChatPreview(); + void setupFrozenAccountBar(); void setupConnectingWidget(); void setupMainMenuToggle(); void setupMoreChatsBar(); void setupDownloadBar(); void setupShortcuts(); void setupStories(); + void setupSwipeBack(); void storiesExplicitCollapse(); void collectStoriesUserpicsViews(Data::StorySourcesList list); void storiesToggleExplicitExpand(bool expand); void trackScroll(not_null widget); - [[nodiscard]] bool searchForPeersRequired(const QString &query) const; + [[nodiscard]] bool peerSearchRequired() const; [[nodiscard]] bool searchForTopicsRequired(const QString &query) const; // Child list may be unable to set specific search state. @@ -221,6 +223,7 @@ private: void showMainMenu(); void clearSearchCache(bool clearPosts); void setSearchQuery(const QString &query, int cursorPosition = -1); + void updateFrozenAccountBar(); void updateControlsVisibility(bool fast = false); void updateLockUnlockVisibility( anim::type animated = anim::type::instant); @@ -263,11 +266,9 @@ private: SearchRequestType type, const MTP::Error &error, not_null process); - void peerSearchFailed(const MTP::Error &error, mtpRequestId requestId); void searchApplyEmpty( SearchRequestType type, not_null process); - void peerSearchApplyEmpty(mtpRequestId id); void updateForceDisplayWide(); void scrollToDefault(bool verytop = false); @@ -298,6 +299,9 @@ private: const Layout _layout = Layout::Main; int _narrowWidth = 0; + + std::unique_ptr _frozenAccountBar; + object_ptr _searchControls; object_ptr _subsectionTopBar = { nullptr }; struct { @@ -366,10 +370,6 @@ private: base::Timer _searchTimer; - QString _peerSearchQuery; - bool _peerSearchFull = false; - mtpRequestId _peerSearchRequest = 0; - QString _topicSearchQuery; TimeId _topicSearchOffsetDate = 0; MsgId _topicSearchOffsetId = 0; @@ -383,14 +383,17 @@ private: ChatSearchTab _searchQueryTab = {}; ChatTypeFilter _searchQueryFilter = {}; + Ui::Controls::SwipeBackResult _swipeBackData; + bool _swipeBackMirrored = false; + bool _swipeBackIconMirrored = false; + SearchProcessState _searchProcess; SearchProcessState _migratedProcess; SearchProcessState _postsProcess; int _historiesRequest = 0; // Not real mtpRequestId. + Api::PeerSearch _peerSearch; Api::SingleMessageSearch _singleMessageSearch; - base::flat_map _peerSearchCache; - base::flat_map _peerSearchQueries; QPixmap _widthAnimationCache; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 1d67b22a27..b27f692392 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "dialogs/dialogs_list.h" #include "dialogs/dialogs_three_state_icon.h" +#include "dialogs/dialogs_quick_action.h" #include "dialogs/ui/dialogs_video_userpic.h" #include "history/history.h" #include "history/history_item.h" @@ -28,12 +29,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_unread_things.h" #include "history/view/history_view_item_preview.h" #include "history/view/history_view_send_action.h" +#include "lang/lang_instance.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" #include "main/main_session.h" #include "storage/localstorage.h" #include "support/support_helper.h" #include "ui/empty_userpic.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/power_saving.h" #include "ui/text/format_values.h" #include "ui/text/text_options.h" @@ -89,16 +93,18 @@ void PaintRowTopRight( text); } -int PaintRightButton(QPainter &p, const PaintContext &context) { +int PaintRightButtonImpl(QPainter &p, const PaintContext &context) { if (context.width < st::columnMinimalWidthLeft) { return 0; } if (const auto rightButton = context.rightButton) { + Assert(rightButton->st != nullptr); + const auto size = rightButton->bg.size() / style::DevicePixelRatio(); const auto left = context.width - size.width() - - st::dialogRowOpenBotRight; - const auto top = st::dialogRowOpenBotTop; + - rightButton->st->margin.right(); + const auto top = rightButton->st->margin.top(); p.drawImage( left, top, @@ -113,22 +119,22 @@ int PaintRightButton(QPainter &p, const PaintContext &context) { left, top, size.width() - size.height() / 2, - context.active + (context.active ? &st::universalRippleAnimation.color->c - : &st::activeButtonBgRipple->c); + : &rightButton->st->button.ripple.color->c)); if (rightButton->ripple->empty()) { rightButton->ripple.reset(); } } p.setPen(context.active - ? st::activeButtonBg + ? rightButton->st->button.textBg : context.selected - ? st::activeButtonFgOver - : st::activeButtonFg); + ? rightButton->st->button.textFgOver + : rightButton->st->button.textFg); rightButton->text.draw(p, { .position = QPoint( left + size.height() / 2, - top + (st::dialogRowOpenBotHeight - rightButton->text.minHeight()) / 2), + top + rightButton->st->button.textTop), .outerWidth = size.width() - size.height() / 2, .availableWidth = size.width() - size.height() / 2, .elisionLines = 1, @@ -348,11 +354,29 @@ void PaintRow( draft = nullptr; } + const auto history = entry->asHistory(); + const auto thread = entry->asThread(); + const auto sublist = entry->asSublist(); + auto bg = context.active ? st::dialogsBgActive : context.selected ? st::dialogsBgOver : context.currentBg; + auto swipeTranslation = 0; + if (history + && context.quickActionContext + && !context.quickActionContext->ripple + && (history->peer->id.value + == context.quickActionContext->data.msgBareId)) { + if (context.quickActionContext->data.translation != 0) { + swipeTranslation = context.quickActionContext->data.translation + * -2; + } + } + if (swipeTranslation) { + p.translate(-swipeTranslation, 0); + } p.fillRect(geometry, bg); if (!(flags & Flag::TopicJumpRipple)) { auto ripple = context.active @@ -361,10 +385,6 @@ void PaintRow( row->paintRipple(p, 0, 0, context.width, &ripple->c); } - const auto history = entry->asHistory(); - const auto thread = entry->asThread(); - const auto sublist = entry->asSublist(); - if (flags & Flag::SavedMessages) { EmptyUserpic::PaintSavedMessages( p, @@ -833,6 +853,60 @@ void PaintRow( + (tag->width() / style::DevicePixelRatio()); } } + if (swipeTranslation) { + p.translate(swipeTranslation, 0); + const auto swipeActionRect = QRect( + rect::right(geometry) - swipeTranslation, + geometry.y(), + swipeTranslation, + geometry.height()); + p.setClipRegion(swipeActionRect); + const auto labelType = ResolveQuickDialogLabel( + history, + context.quickActionContext->action, + context.filter); + p.fillRect(swipeActionRect, ResolveQuickActionBg(labelType)); + if (context.quickActionContext->data.reachRatio) { + p.setPen(Qt::NoPen); + p.setBrush(ResolveQuickActionBgActive(labelType)); + const auto r = swipeTranslation + * context.quickActionContext->data.reachRatio; + const auto offset = st::dialogsQuickActionSize + + st::dialogsQuickActionSize / 2.; + p.drawEllipse(QPointF(geometry.width() - offset, offset), r, r); + } + const auto quickWidth = st::dialogsQuickActionSize * 3; + if (context.quickActionContext->icon) { + DrawQuickAction( + p, + QRect( + rect::right(geometry) - quickWidth, + geometry.y(), + quickWidth, + geometry.height()), + context.quickActionContext->icon.get(), + labelType); + } + p.setClipping(false); + } + if (const auto quick = context.quickActionContext; + quick && quick->ripple && quick->rippleFg) { + const auto labelType = ResolveQuickDialogLabel( + history, + context.quickActionContext->action, + context.filter); + const auto ripple = ResolveQuickActionBg(labelType); + const auto size = st::dialogsQuickActionRippleSize; + const auto x = geometry.width() - size; + quick->ripple->paint(p, x, 0, size, &ripple->c); + quick->rippleFg->paint(p, x, 0, size, &st::premiumButtonFg->c); + if (quick->ripple->empty()) { + quick->ripple.reset(); + } + if (quick->rippleFg->empty()) { + quick->rippleFg.reset(); + } + } } } // namespace @@ -1189,4 +1263,8 @@ void PaintCollapsedRow( } } +int PaintRightButton(QPainter &p, const PaintContext &context) { + return PaintRightButtonImpl(p, context); +} + } // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h index e014e657f5..50002c4847 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "dialogs/ui/dialogs_quick_action_context.h" #include "ui/cached_round_corners.h" namespace style { @@ -18,9 +19,6 @@ namespace st { extern const style::DialogRow &defaultDialogRow; } // namespace st -namespace Ui { -} // namespace Ui - namespace Data { class Forum; class Folder; @@ -57,6 +55,7 @@ struct TopicJumpCache { struct PaintContext { RightButton *rightButton = nullptr; std::vector *chatsFilterTags = nullptr; + QuickActionContext *quickActionContext = nullptr; not_null st; TopicJumpCache *topicJumpCache = nullptr; Data::Folder *folder = nullptr; @@ -111,4 +110,6 @@ void PaintCollapsedRow( int unread, const PaintContext &context); +int PaintRightButton(QPainter &p, const PaintContext &context); + } // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index eb9339b3e1..5ed84ef384 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -150,8 +150,7 @@ void MessageView::prepare( not_null item, Data::Forum *forum, Fn customEmojiRepaint, - ToPreviewOptions options, - Fn customLoadingFinishCallback) { + ToPreviewOptions options) { if (!forum) { _topics = nullptr; } else if (!_topics || _topics->forum() != forum) { @@ -213,11 +212,9 @@ void MessageView::prepare( if (!_loadingContext) { _loadingContext = std::make_unique(); item->history()->session().downloaderTaskFinished( - ) | rpl::start_with_next( - customLoadingFinishCallback - ? customLoadingFinishCallback - : Fn([=] { _textCachedFor = nullptr; }), - _loadingContext->lifetime); + ) | rpl::start_with_next([=] { + _textCachedFor = nullptr; + }, _loadingContext->lifetime); } _loadingContext->context = std::move(preview.loadingContext); } else { @@ -370,7 +367,18 @@ void MessageView::paint( if (image.hasSpoiler()) { const auto frame = DefaultImageSpoiler().frame( _spoiler->index(context.now, pausedSpoiler)); - FillSpoilerRect(p, mini, frame); + if (image.isEllipse()) { + const auto radius = st::dialogsMiniPreview / 2; + static auto mask = Images::CornersMask(radius); + FillSpoilerRect( + p, + mini, + Images::CornersMaskRef(mask), + frame, + _cornersCache); + } else { + FillSpoilerRect(p, mini, frame); + } } } rect.setLeft(rect.x() + w); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h index 56aa8c2a9d..4ee12aebd3 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h @@ -61,8 +61,7 @@ public: not_null item, Data::Forum *forum, Fn customEmojiRepaint, - ToPreviewOptions options, - Fn customLoadingFinishCallback = nullptr); + ToPreviewOptions options); void paint( Painter &p, @@ -95,6 +94,7 @@ private: mutable std::unique_ptr _spoiler; mutable std::unique_ptr _loadingContext; mutable const style::DialogsMiniIcon *_leftIcon = nullptr; + mutable QImage _cornersCache; mutable bool _hasPlainLinkAtBegin = false; }; diff --git a/Telegram/SourceFiles/history/history_view_swipe_data.h b/Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h similarity index 56% rename from Telegram/SourceFiles/history/history_view_swipe_data.h rename to Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h index c3dd9eb776..7c924c06ee 100644 --- a/Telegram/SourceFiles/history/history_view_swipe_data.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h @@ -7,14 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace HistoryView { +namespace Dialogs::Ui { -struct ChatPaintGestureHorizontalData { - float64 ratio = 0.; - float64 reachRatio = 0.; - int64 msgBareId = 0; - int translation = 0; - int cursorTop = 0; +using namespace ::Ui; + +enum class QuickDialogAction { + Mute, + Pin, + Read, + Archive, + Delete, + Disabled, }; -} // namespace HistoryView +} // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_quick_action_context.h b/Telegram/SourceFiles/dialogs/ui/dialogs_quick_action_context.h new file mode 100644 index 0000000000..a1baa28730 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_quick_action_context.h @@ -0,0 +1,47 @@ +/* +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 "dialogs/ui/dialogs_quick_action.h" +#include "ui/controls/swipe_handler_data.h" + +namespace Lottie { +class Icon; +} // namespace Lottie + +namespace Ui { +class RippleAnimation; +} // namespace Ui + +namespace Dialogs::Ui { + +using namespace ::Ui; + +enum class QuickDialogActionLabel { + Mute, + Unmute, + Pin, + Unpin, + Read, + Unread, + Archive, + Unarchive, + Delete, + Disabled, +}; + +struct QuickActionContext { + ::Ui::Controls::SwipeContextData data; + std::unique_ptr icon; + std::unique_ptr ripple; + std::unique_ptr rippleFg; + QuickDialogAction action; + crl::time finishedAt = 0; +}; + +} // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index d13f336f93..4ae07e5a92 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_common.h" #include "storage/storage_shared_media.h" #include "ui/boxes/confirm_box.h" +#include "ui/controls/swipe_handler.h" #include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" @@ -203,7 +204,7 @@ RecentRow::RecentRow(not_null peer) if (const auto user = peer->asUser()) { if (user->botInfo && user->botInfo->hasMainApp) { return std::make_unique( - st::dialogRowOpenBotTextStyle, + st::dialogRowOpenBotRecent.button.style, tr::lng_profile_open_app_short(tr::now)); } } @@ -274,20 +275,15 @@ QSize RecentRow::rightActionSize() const { if (_mainAppText && _badgeSize.isEmpty()) { return QSize( _mainAppText->maxWidth() + _mainAppText->minHeight(), - st::dialogRowOpenBotHeight); + st::dialogRowOpenBotRecent.button.height); } return _badgeSize; } QMargins RecentRow::rightActionMargins() const { if (_mainAppText && _badgeSize.isEmpty()) { - return QMargins( - 0, - st::dialogRowOpenBotRecentTop, - st::dialogRowOpenBotRight, - 0); - } - if (_badgeSize.isEmpty()) { + return st::dialogRowOpenBotRecent.margin; + } else if (_badgeSize.isEmpty()) { return {}; } const auto x = st::recentPeersItem.photoPosition.x(); @@ -320,8 +316,7 @@ void RecentRow::rightActionPaint( p.setPen(actionSelected ? st::activeButtonFgOver : st::activeButtonFg); - const auto top = 0 - + (st::dialogRowOpenBotHeight - _mainAppText->minHeight()) / 2; + const auto top = st::dialogRowOpenBotRecent.button.textTop; _mainAppText->draw(p, { .position = QPoint(x + size.height() / 2, y + top), .outerWidth = outerWidth, @@ -1552,6 +1547,61 @@ void Suggestions::setupApps() { }); } +Ui::Controls::SwipeHandlerArgs Suggestions::generateIncompleteSwipeArgs() { + _swipeLifetime.destroy(); + + auto update = [=](Ui::Controls::SwipeContextData data) { + if (data.translation != 0) { + if (!_swipeBackData.callback) { + _swipeBackData = Ui::Controls::SetupSwipeBack( + this, + [=]() -> std::pair { + return { + st::historyForwardChooseBg->c, + st::historyForwardChooseFg->c, + }; + }, + data.translation < 0); + } + _swipeBackData.callback(data); + return; + } else if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } + }; + auto init = [=](int, Qt::LayoutDirection direction) { + if (!_tabs) { + return Ui::Controls::SwipeHandlerFinishData(); + } + const auto activeSection = _tabs->activeSection(); + const auto isToLeft = direction == Qt::RightToLeft; + if ((isToLeft && activeSection > 0) + || (!isToLeft && activeSection < _tabKeys.size() - 1)) { + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + if (_tabs + && _tabs->activeSection() == activeSection) { + _swipeBackData = {}; + _tabs->setActiveSection(isToLeft + ? activeSection - 1 + : activeSection + 1); + } + }); + } + return Ui::Controls::SwipeHandlerFinishData(); + }; + return { .widget = this, .update = update, .init = init }; +} + +void Suggestions::reinstallSwipe(not_null scroll) { + _swipeLifetime.destroy(); + + auto args = generateIncompleteSwipeArgs(); + args.scroll = scroll; + args.onLifetime = &_swipeLifetime; + + Ui::Controls::SetupSwipeHandler(std::move(args)); +} + void Suggestions::selectJump(Qt::Key direction, int pageSize) { switch (_key.current().tab) { case Tab::Chats: selectJumpChats(direction, pageSize); return; @@ -1977,6 +2027,18 @@ void Suggestions::finishShow() { _appsScroll->setVisible(key == Key{ Tab::Apps }); for (const auto &[mediaKey, list] : _mediaLists) { list.wrap->setVisible(key == mediaKey); + if (key == mediaKey) { + _swipeLifetime.destroy(); + auto incomplete = generateIncompleteSwipeArgs(); + list.wrap->replaceSwipeHandler(&incomplete); + } + } + if (key == Key{ Tab::Chats }) { + reinstallSwipe(_chatsScroll.get()); + } else if (key == Key{ Tab::Channels }) { + reinstallSwipe(_channelsScroll.get()); + } else if (key == Key{ Tab::Apps }) { + reinstallSwipe(_appsScroll.get()); } } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index 0d8d1df729..f269085f13 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "base/timer.h" #include "dialogs/ui/top_peers_strip.h" +#include "ui/controls/swipe_handler_data.h" #include "ui/effects/animations.h" #include "ui/rp_widget.h" @@ -32,6 +33,9 @@ enum class SharedMediaType : signed char; } // namespace Storage namespace Ui { +namespace Controls { +struct SwipeHandlerArgs; +} // namespace Controls class BoxContent; class ScrollArea; class ElasticScroll; @@ -157,6 +161,9 @@ private: void setupChats(); void setupChannels(); void setupApps(); + void reinstallSwipe(not_null); + [[nodiscard]] auto generateIncompleteSwipeArgs() + -> Ui::Controls::SwipeHandlerArgs; void selectJumpChats(Qt::Key direction, int pageSize); void selectJumpChannels(Qt::Key direction, int pageSize); @@ -253,6 +260,9 @@ private: QPixmap _slideLeft; QPixmap _slideRight; + Ui::Controls::SwipeBackResult _swipeBackData; + rpl::lifetime _swipeLifetime; + int _slideLeftTop = 0; int _slideRightTop = 0; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 6e1ca060b3..f4c0c788da 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1704,6 +1704,15 @@ ServiceAction ParseServiceAction( .giftId = uint64(gift.vid().v), }; }); + }, [&](const MTPDmessageActionPaidMessagesRefunded &data) { + result.content = ActionPaidMessagesRefunded{ + .messages = data.vcount().v, + .stars = int64(data.vstars().v), + }; + }, [&](const MTPDmessageActionPaidMessagesPrice &data) { + result.content = ActionPaidMessagesPrice{ + .stars = int(data.vstars().v), + }; }, [](const MTPDmessageActionEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index d460ee196a..a7518ba056 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -662,6 +662,15 @@ struct ActionStarGift { bool limited = false; }; +struct ActionPaidMessagesRefunded { + int messages = 0; + int64 stars = 0; +}; + +struct ActionPaidMessagesPrice { + int stars = 0; +}; + struct ServiceAction { std::variant< v::null_t, @@ -707,7 +716,9 @@ struct ServiceAction { ActionPaymentRefunded, ActionGiftStars, ActionPrizeStars, - ActionStarGift> content; + ActionStarGift, + ActionPaidMessagesRefunded, + ActionPaidMessagesPrice> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 0ed94264ec..c58bc06f8c 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1367,6 +1367,26 @@ auto HtmlWriter::Wrap::pushMessage( + " sent you a gift of " + QByteArray::number(data.stars) + " Telegram Stars."; + }, [&](const ActionPaidMessagesRefunded &data) { + auto result = message.out + ? ("You refunded " + + QString::number(data.stars).toUtf8() + + " Stars for " + + QString::number(data.messages).toUtf8() + + " messages to " + + peers.wrapPeerName(dialog.peerId)) + : (peers.wrapPeerName(dialog.peerId) + + " refunded " + + QString::number(data.stars).toUtf8() + + " Stars for " + + QString::number(data.messages).toUtf8() + + " messages to you"); + return result; + }, [&](const ActionPaidMessagesPrice &data) { + auto result = "Price per messages changed to " + + QString::number(data.stars).toUtf8() + + " Telegram Stars."; + return result; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { @@ -1560,7 +1580,9 @@ auto HtmlWriter::Wrap::pushMessage( block.append(popTag()); } if (!message.reactions.empty()) { - block.append(pushDiv("reactions")); + block.append(pushTag("span", { + { "class", "reactions" }, + })); for (const auto &reaction : message.reactions) { auto reactionClass = QByteArray("reaction"); for (const auto &recent : reaction.recent) { @@ -1574,10 +1596,10 @@ auto HtmlWriter::Wrap::pushMessage( reactionClass += " paid"; } - block.append(pushTag("div", { + block.append(pushTag("span", { { "class", reactionClass }, })); - block.append(pushTag("div", { + block.append(pushTag("span", { { "class", "emoji" }, })); switch (reaction.type) { @@ -1596,7 +1618,7 @@ auto HtmlWriter::Wrap::pushMessage( } block.append(popTag()); if (!reaction.recent.empty()) { - block.append(pushTag("div", { + block.append(pushTag("span", { { "class", "userpics" }, })); for (const auto &recent : reaction.recent) { @@ -1617,7 +1639,7 @@ auto HtmlWriter::Wrap::pushMessage( } if (reaction.recent.empty() || (reaction.count > reaction.recent.size())) { - block.append(pushTag("div", { + block.append(pushTag("span", { { "class", "count" }, })); block.append(NumberToString(reaction.count)); @@ -2321,10 +2343,12 @@ MediaData HtmlWriter::Wrap::prepareMediaData( } else if (data.isVideoFile) { // At least try to pushVideoFileMedia. } else if (data.isAudioFile) { - result.title = (data.songPerformer.isEmpty() - || data.songTitle.isEmpty()) - ? QByteArray("Audio file") - : data.songPerformer + " \xe2\x80\x93 " + data.songTitle; + result.title = (!data.songPerformer.isEmpty() + && !data.songTitle.isEmpty()) + ? (data.songPerformer + " \xe2\x80\x93 " + data.songTitle) + : !data.name.isEmpty() + ? data.name + : QByteArray("Audio file"); result.status = FormatDuration(data.duration); if (!hasFile) { result.status += ", " + FormatFileSize(data.file.size); diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index dee8d1e947..1f482db782 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -663,6 +663,15 @@ QByteArray SerializeMessage( push("is_limited", data.limited); push("is_anonymous", data.anonymous); pushBare("gift_text", SerializeText(context, data.text)); + }, [&](const ActionPaidMessagesRefunded &data) { + pushActor(); + pushAction("paid_messages_refund"); + push("messages_count", data.messages); + push("stars_count", data.stars); + }, [&](const ActionPaidMessagesPrice &data) { + pushActor(); + pushAction("paid_messages_price_change"); + push("price_stars", data.stars); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index e6bb0716de..0b9ac614d0 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -1039,6 +1039,16 @@ void InnerWidget::restoreScrollPosition() { _scrollToSignal.fire_copy(newVisibleTop); } +Ui::ChatPaintContext InnerWidget::preparePaintContext(QRect clip) const { + return _controller->preparePaintContext({ + .theme = _theme.get(), + .clip = clip, + .visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)), + .visibleAreaTop = _visibleTop, + .visibleAreaWidth = width(), + }); +} + void InnerWidget::paintEvent(QPaintEvent *e) { if (_controller->contentOverlapped(this, e)) { return; @@ -1051,13 +1061,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - auto context = _controller->preparePaintContext({ - .theme = _theme.get(), - .clip = clip, - .visibleAreaPositionGlobal = mapToGlobal(QPoint(0, _visibleTop)), - .visibleAreaTop = _visibleTop, - .visibleAreaWidth = width(), - }); + auto context = preparePaintContext(clip); if (_items.empty() && _upLoaded && _downLoaded) { paintEmpty(p, context.st); } else { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index 209cc0384e..ed23bdad44 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -36,6 +36,7 @@ class PopupMenu; class ChatStyle; class ChatTheme; struct PeerUserpicView; +struct ChatPaintContext; } // namespace Ui namespace Window { @@ -69,6 +70,8 @@ public: return _channel; } + Ui::ChatPaintContext preparePaintContext(QRect clip) const; + // Set the correct scroll position after being resized. void restoreScrollPosition(); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp index 417223ae30..56fce9db19 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/admin_log/history_admin_log_filter.h" #include "profile/profile_back_button.h" #include "core/shortcuts.h" +#include "ui/chat/chat_style.h" +#include "ui/controls/swipe_handler.h" #include "ui/effects/animations.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" @@ -341,6 +343,7 @@ Widget::Widget( }); setupShortcuts(); + setupSwipeReply(); } void Widget::showFilter() { @@ -416,6 +419,44 @@ void Widget::setupShortcuts() { }, lifetime()); } +void Widget::setupSwipeReply() { + auto update = [=](Ui::Controls::SwipeContextData data) { + if (data.translation > 0) { + if (!_swipeBackData.callback) { + _swipeBackData = Ui::Controls::SetupSwipeBack( + this, + [=]() -> std::pair { + auto context = _inner->preparePaintContext({}); + return { + context.st->msgServiceBg()->c, + context.st->msgServiceFg()->c, + }; + }); + } + _swipeBackData.callback(data); + return; + } else if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } + }; + + auto init = [=](int, Qt::LayoutDirection direction) { + if (direction == Qt::RightToLeft) { + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + controller()->showBackFromStack(); + }); + } + return Ui::Controls::SwipeHandlerFinishData(); + }; + + Ui::Controls::SetupSwipeHandler({ + .widget = _inner.data(), + .scroll = _scroll.data(), + .update = std::move(update), + .init = std::move(init), + }); +} + std::shared_ptr Widget::createMemento() { auto result = std::make_shared(channel()); saveState(result.get()); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h index 8a547ee817..9643694f82 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/section_memento.h" #include "history/admin_log/history_admin_log_item.h" #include "history/admin_log/history_admin_log_filter_value.h" +#include "ui/controls/swipe_handler_data.h" #include "mtproto/sender.h" namespace Ui { @@ -74,6 +75,7 @@ private: void saveState(not_null memento); void restoreState(not_null memento); void setupShortcuts(); + void setupSwipeReply(); object_ptr _scroll; QPointer _inner; @@ -81,6 +83,8 @@ private: object_ptr _fixedBarShadow; object_ptr _whatIsThis; + Ui::Controls::SwipeBackResult _swipeBackData; + }; class SectionMemento : public Window::SectionMemento { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0fd4377125..6f85bf3716 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -29,7 +29,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_emoji_interactions.h" #include "history/history_item_components.h" #include "history/history_item_text.h" -#include "history/history_view_swipe.h" #include "payments/payments_reaction_process.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/menu/menu_multiline_action.h" @@ -42,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/edit_factcheck_box.h" #include "ui/boxes/report_box_graphics.h" #include "ui/controls/delete_message_context_action.h" +#include "ui/controls/swipe_handler.h" #include "ui/inactive_press.h" #include "ui/painter.h" #include "ui/rect.h" @@ -491,7 +491,7 @@ HistoryInner::HistoryInner( lifetime()); setupSharingDisallowed(); - setupSwipeReply(); + setupSwipeReplyAndBack(); } void HistoryInner::reactionChosen(const ChosenReaction &reaction) { @@ -574,12 +574,30 @@ void HistoryInner::setupSharingDisallowed() { }, lifetime()); } -void HistoryInner::setupSwipeReply() { - if (_peer && _peer->isChannel() && !_peer->isMegagroup()) { +void HistoryInner::setupSwipeReplyAndBack() { + if (!_peer) { return; } - HistoryView::SetupSwipeHandler(this, _scroll, [=, history = _history]( - HistoryView::ChatPaintGestureHorizontalData data) { + const auto peer = _peer; + + auto update = [=, history = _history](Ui::Controls::SwipeContextData data) { + if (data.translation > 0) { + if (!_swipeBackData.callback) { + _swipeBackData = Ui::Controls::SetupSwipeBack( + _widget, + [=]() -> std::pair { + auto context = preparePaintContext({}); + return { + context.st->msgServiceBg()->c, + context.st->msgServiceFg()->c, + }; + }); + } + _swipeBackData.callback(data); + return; + } else if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } const auto changed = (_gestureHorizontal.msgBareId != data.msgBareId) || (_gestureHorizontal.translation != data.translation) || (_gestureHorizontal.reachRatio != data.reachRatio); @@ -592,9 +610,19 @@ void HistoryInner::setupSwipeReply() { repaintItem(item); } } - }, [=, show = _controller->uiShow()](int cursorTop) { - auto result = HistoryView::SwipeHandlerFinishData(); - if (inSelectionMode().inSelectionMode) { + }; + + auto init = [=, show = _controller->uiShow()]( + int cursorTop, + Qt::LayoutDirection direction) { + if (direction == Qt::RightToLeft) { + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + _controller->showBackFromStack(); + }); + } + auto result = Ui::Controls::SwipeHandlerFinishData(); + if (inSelectionMode().inSelectionMode + || (peer->isChannel() && !peer->isMegagroup())) { return result; } enumerateItems([&]( @@ -632,11 +660,21 @@ void HistoryInner::setupSwipeReply() { return false; }); return result; - }, _touchMaybeSelecting.value()); + }; + + Ui::Controls::SetupSwipeHandler({ + .widget = this, + .scroll = _scroll, + .update = std::move(update), + .init = std::move(init), + .dontStart = _touchMaybeSelecting.value(), + }); } bool HistoryInner::hasSelectRestriction() const { - if (!_sharingDisallowed.current()) { + if (session().frozen()) { + return true; + } else if (!_sharingDisallowed.current()) { return false; } else if (const auto chat = _peer->asChat()) { return !chat->canDeleteMessages(); @@ -1550,6 +1588,11 @@ void HistoryInner::touchEvent(QTouchEvent *e) { } break; case QEvent::TouchUpdate: { + LOG(("UPDATE: %1,%2 -> %3,%4" + ).arg(_touchStart.x() + ).arg(_touchStart.y() + ).arg(_touchPos.x() + ).arg(_touchPos.y())); if (!_touchInProgress) { return; } else if (_touchSelect) { @@ -2194,7 +2237,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } const auto link = ClickHandler::getActive(); - if (link + if (_controller->showFrozenError()) { + return; + } else if (link && !link->property( kSendReactionEmojiProperty).value().empty() && _reactionsManager->showContextMenu( diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 1a30e42027..d6364607f3 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -10,11 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "data/data_report.h" #include "ui/rp_widget.h" +#include "ui/controls/swipe_handler_data.h" #include "ui/effects/animations.h" #include "ui/dragging_scroll_manager.h" #include "ui/widgets/tooltip.h" #include "ui/widgets/scroll_area.h" -#include "history/history_view_swipe_data.h" #include "history/view/history_view_top_bar_widget.h" #include @@ -428,7 +428,7 @@ private: void reactionChosen(const ChosenReaction &reaction); void setupSharingDisallowed(); - void setupSwipeReply(); + void setupSwipeReplyAndBack(); [[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const; [[nodiscard]] bool hasCopyMediaRestriction( not_null item) const; @@ -543,7 +543,8 @@ private: crl::time _touchTime = 0; base::Timer _touchScrollTimer; - HistoryView::ChatPaintGestureHorizontalData _gestureHorizontal; + Ui::Controls::SwipeContextData _gestureHorizontal; + Ui::Controls::SwipeBackResult _swipeBackData; // _menu must be destroyed before _whoReactedMenuLifetime. rpl::lifetime _whoReactedMenuLifetime; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 0e50060f76..9bf27a91b0 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -311,7 +311,7 @@ std::unique_ptr HistoryItem::CreateMedia( return document->match([&](const MTPDdocument &document) -> Result { const auto list = media.valt_documents(); const auto owner = &item->history()->owner(); - const auto data = owner->processDocument(document); + const auto data = owner->processDocument(document, list); using Args = Data::MediaFile::Args; return std::make_unique(item, data, Args{ .ttlSeconds = media.vttl_seconds().value_or_empty(), @@ -2495,7 +2495,8 @@ bool HistoryItem::canDeleteForEveryone(TimeId now) const { } else if (const auto user = peer->asUser()) { // Bots receive all messages and there is no sense in revoking them. // See https://github.com/telegramdesktop/tdesktop/issues/3818 - if (user->isBot() && !user->isSupport()) { + if ((user->isBot() && !user->isSupport()) + || user->isInaccessible()) { return false; } } @@ -5751,6 +5752,45 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto preparePaidMessagesRefunded = [&](const MTPDmessageActionPaidMessagesRefunded &action) { + auto result = PreparedServiceText(); + if (_from->isSelf()) { + result.links.push_back(_history->peer->createOpenLink()); + result.text = tr::lng_action_paid_message_refund_self( + tr::now, + lt_count, + action.vstars().v, + lt_name, + Ui::Text::Link(_history->peer->shortName(), 1), + Ui::Text::WithEntities); + } else { + result.links.push_back(_from->createOpenLink()); + result.text = tr::lng_action_paid_message_refund( + tr::now, + lt_count, + action.vstars().v, + lt_from, + Ui::Text::Link(_from->shortName(), 1), + Ui::Text::WithEntities); + } + return result; + }; + + auto preparePaidMessagesPrice = [&](const MTPDmessageActionPaidMessagesPrice &action) { + const auto stars = action.vstars().v; + auto result = PreparedServiceText(); + result.text = stars + ? tr::lng_action_message_price_paid( + tr::now, + lt_count, + stars, + Ui::Text::WithEntities) + : tr::lng_action_message_price_free( + tr::now, + Ui::Text::WithEntities); + return result; + }; + setServiceText(action.match( prepareChatAddUserText, prepareChatJoinedByLink, @@ -5798,6 +5838,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareGiftPrize, prepareStarGift, prepareStarGiftUnique, + preparePaidMessagesRefunded, + preparePaidMessagesPrice, PrepareEmptyText, PrepareErrorText)); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 4fa927cb0f..38cf1c09a6 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -708,7 +708,8 @@ ClickHandlerPtr HideSponsoredClickHandler() { if (session.premium()) { using Result = Data::SponsoredReportResult; session.sponsoredMessages().createReportCallback( - my.itemId)(Result::Id("-1"), [](const auto &) {}); + my.itemId + ).callback(Result::Id("-1"), [](const auto &) {}); } else { ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); } diff --git a/Telegram/SourceFiles/history/history_view_swipe.cpp b/Telegram/SourceFiles/history/history_view_swipe.cpp deleted file mode 100644 index cbe9d65a14..0000000000 --- a/Telegram/SourceFiles/history/history_view_swipe.cpp +++ /dev/null @@ -1,260 +0,0 @@ -/* -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 "history/history_view_swipe.h" - -#include "base/platform/base_platform_haptic.h" -#include "base/platform/base_platform_info.h" -#include "base/qt/qt_common_adapters.h" -#include "base/event_filter.h" -#include "history/history_view_swipe_data.h" -#include "ui/chat/chat_style.h" -#include "ui/ui_utility.h" -#include "ui/widgets/elastic_scroll.h" -#include "ui/widgets/scroll_area.h" - -#include - -namespace HistoryView { -namespace { - -constexpr auto kSwipeSlow = 0.2; - -} // namespace - -void SetupSwipeHandler( - not_null widget, - not_null scroll, - Fn update, - Fn generateFinishByTop, - rpl::producer dontStart) { - constexpr auto kThresholdWidth = 50; - constexpr auto kMaxRatio = 1.5; - const auto threshold = style::ConvertFloatScale(kThresholdWidth); - struct UpdateArgs { - QPoint globalCursor; - QPointF position; - QPointF delta; - bool touch = false; - }; - struct State { - base::unique_qptr filter; - Ui::Animations::Simple animationReach; - Ui::Animations::Simple animationEnd; - ChatPaintGestureHorizontalData data; - SwipeHandlerFinishData finishByTopData; - std::optional orientation; - QPointF startAt; - QPointF delta; - int cursorTop = 0; - bool dontStart = false; - bool started = false; - bool reached = false; - bool touch = false; - - rpl::lifetime lifetime; - }; - const auto state = widget->lifetime().make_state(); - std::move( - dontStart - ) | rpl::start_with_next([=](bool dontStart) { - state->dontStart = dontStart; - }, state->lifetime); - - const auto updateRatio = [=](float64 ratio) { - ratio = std::max(ratio, 0.); - state->data.ratio = ratio; - const auto overscrollRatio = std::max(ratio - 1., 0.); - const auto translation = int( - base::SafeRound(-std::min(ratio, 1.) * threshold) - ) + Ui::OverscrollFromAccumulated(int( - base::SafeRound(-overscrollRatio * threshold) - )); - state->data.msgBareId = state->finishByTopData.msgBareId; - state->data.translation = translation; - state->data.cursorTop = state->cursorTop; - update(state->data); - }; - const auto setOrientation = [=](std::optional o) { - state->orientation = o; - const auto isHorizontal = (o == Qt::Horizontal); - scroll->viewport()->setAttribute( - Qt::WA_AcceptTouchEvents, - !isHorizontal); - scroll->disableScroll(isHorizontal); - }; - const auto processEnd = [=](std::optional delta = {}) { - if (state->orientation == Qt::Horizontal) { - const auto ratio = std::clamp( - delta.value_or(state->delta).x() / threshold, - 0., - kMaxRatio); - if ((ratio >= 1) && state->finishByTopData.callback) { - Ui::PostponeCall( - widget, - state->finishByTopData.callback); - } - state->animationEnd.stop(); - state->animationEnd.start( - updateRatio, - ratio, - 0., - std::min(1., ratio) * st::slideWrapDuration); - } - setOrientation(std::nullopt); - state->started = false; - state->reached = false; - }; - scroll->scrolls() | rpl::start_with_next([=] { - if (state->orientation != Qt::Vertical) { - processEnd(); - } - }, state->lifetime); - const auto animationReachCallback = [=](float64 value) { - state->data.reachRatio = value; - update(state->data); - }; - const auto updateWith = [=](UpdateArgs args) { - if (!state->started || state->touch != args.touch) { - state->started = true; - state->touch = args.touch; - state->startAt = args.position; - state->delta = QPointF(); - state->cursorTop = widget->mapFromGlobal(args.globalCursor).y(); - state->finishByTopData = generateFinishByTop( - state->cursorTop); - if (!state->finishByTopData.callback) { - setOrientation(Qt::Vertical); - } - } else if (!state->orientation) { - state->delta = args.delta; - const auto diffXtoY = std::abs(args.delta.x()) - - std::abs(args.delta.y()); - constexpr auto kOrientationThreshold = 1.; - if (diffXtoY > kOrientationThreshold) { - if (!state->dontStart) { - setOrientation(Qt::Horizontal); - } - } else if (diffXtoY < -kOrientationThreshold) { - setOrientation(Qt::Vertical); - } else { - setOrientation(std::nullopt); - } - } else if (*state->orientation == Qt::Horizontal) { - state->delta = args.delta; - const auto ratio = args.delta.x() / threshold; - updateRatio(ratio); - constexpr auto kResetReachedOn = 0.95; - constexpr auto kBounceDuration = crl::time(500); - if (!state->reached && ratio >= 1.) { - state->reached = true; - state->animationReach.stop(); - state->animationReach.start( - animationReachCallback, - 0., - 1., - kBounceDuration); - base::Platform::Haptic(); - } else if (state->reached - && ratio < kResetReachedOn) { - state->reached = false; - } - } - }; - const auto filter = [=](not_null e) { - const auto type = e->type(); - switch (type) { - case QEvent::Leave: { - if (state->orientation == Qt::Horizontal) { - processEnd(); - } - } break; - case QEvent::MouseMove: { - if (state->orientation == Qt::Horizontal) { - const auto m = static_cast(e.get()); - if (std::abs(m->pos().y() - state->cursorTop) - > QApplication::startDragDistance()) { - processEnd(); - } - } - } break; - case QEvent::TouchBegin: - case QEvent::TouchUpdate: - case QEvent::TouchEnd: - case QEvent::TouchCancel: { - const auto t = static_cast(e.get()); - const auto touchscreen = t->device() - && (t->device()->type() == base::TouchDevice::TouchScreen); - if (!touchscreen) { - break; - } else if (type == QEvent::TouchBegin) { - // Reset state in case we lost some TouchEnd. - processEnd(); - } - const auto &touches = t->touchPoints(); - const auto released = [&](int index) { - return (touches.size() > index) - && (int(touches.at(index).state()) - & int(Qt::TouchPointReleased)); - }; - const auto cancel = released(0) - || released(1) - || (touches.size() != (touchscreen ? 1 : 2)) - || (type == QEvent::TouchEnd) - || (type == QEvent::TouchCancel); - if (cancel) { - processEnd(touches.empty() - ? std::optional() - : (state->startAt - touches[0].pos())); - } else { - const auto args = UpdateArgs{ - .globalCursor = (touchscreen - ? touches[0].screenPos().toPoint() - : QCursor::pos()), - .position = touches[0].pos(), - .delta = state->startAt - touches[0].pos(), - .touch = true, - }; - updateWith(args); - } - return (touchscreen && state->orientation != Qt::Horizontal) - ? base::EventFilterResult::Continue - : base::EventFilterResult::Cancel; - } break; - case QEvent::Wheel: { - const auto w = static_cast(e.get()); - const auto phase = w->phase(); - if (phase == Qt::NoScrollPhase) { - break; - } else if (phase == Qt::ScrollBegin) { - // Reset state in case we lost some TouchEnd. - processEnd(); - } - const auto cancel = w->buttons() - || (phase == Qt::ScrollEnd) - || (phase == Qt::ScrollMomentum); - if (cancel) { - processEnd(); - } else { - const auto invert = (w->inverted() ? -1 : 1); - const auto delta = Ui::ScrollDeltaF(w) * invert; - updateWith({ - .globalCursor = w->globalPosition().toPoint(), - .position = QPointF(), - .delta = state->delta + delta * kSwipeSlow, - .touch = false, - }); - } - } break; - } - return base::EventFilterResult::Continue; - }; - state->filter = base::make_unique_q( - base::install_event_filter(widget, filter)); -} - -} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/history_view_swipe_back_session.cpp b/Telegram/SourceFiles/history/history_view_swipe_back_session.cpp new file mode 100644 index 0000000000..b757a0aad0 --- /dev/null +++ b/Telegram/SourceFiles/history/history_view_swipe_back_session.cpp @@ -0,0 +1,63 @@ +/* +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 "history/history_view_swipe_back_session.h" + +#include "history/view/history_view_list_widget.h" +#include "ui/chat/chat_style.h" +#include "ui/controls/swipe_handler.h" +#include "ui/controls/swipe_handler_data.h" +#include "window/window_session_controller.h" + +namespace Window { + +void SetupSwipeBackSection( + not_null parent, + not_null scroll, + not_null list) { + const auto swipeBackData + = list->lifetime().make_state(); + auto update = [=](Ui::Controls::SwipeContextData data) { + if (data.translation > 0) { + if (!swipeBackData->callback) { + const auto color = [=]() -> std::pair { + const auto c = list->delegate()->listPreparePaintContext({ + .theme = list->delegate()->listChatTheme(), + }); + return { + c.st->msgServiceBg()->c, + c.st->msgServiceFg()->c, + }; + }; + (*swipeBackData) = Ui::Controls::SetupSwipeBack( + parent, + color); + } + swipeBackData->callback(data); + return; + } else if (swipeBackData->lifetime) { + (*swipeBackData) = {}; + } + }; + auto init = [=](int, Qt::LayoutDirection direction) { + if (direction != Qt::RightToLeft) { + return Ui::Controls::SwipeHandlerFinishData(); + } + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + list->controller()->showBackFromStack(); + }); + }; + Ui::Controls::SetupSwipeHandler({ + .widget = list, + .scroll = scroll, + .update = std::move(update), + .init = std::move(init), + .dontStart = list->touchMaybeSelectingValue(), + }); +} + +} // namespace Window diff --git a/Telegram/SourceFiles/history/history_view_swipe.h b/Telegram/SourceFiles/history/history_view_swipe_back_session.h similarity index 55% rename from Telegram/SourceFiles/history/history_view_swipe.h rename to Telegram/SourceFiles/history/history_view_swipe_back_session.h index fe9ec2ea02..8d65f684e9 100644 --- a/Telegram/SourceFiles/history/history_view_swipe.h +++ b/Telegram/SourceFiles/history/history_view_swipe_back_session.h @@ -7,25 +7,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +class WindowListDelegate; + +namespace HistoryView { +class ListWidget; +} // namespace HistoryView + namespace Ui { class RpWidget; class ScrollArea; } // namespace Ui -namespace HistoryView { +namespace Window { -struct ChatPaintGestureHorizontalData; +class SectionWidget; -struct SwipeHandlerFinishData { - Fn callback; - int64 msgBareId = 0; -}; - -void SetupSwipeHandler( - not_null widget, +void SetupSwipeBackSection( + not_null parent, not_null scroll, - Fn update, - Fn generateFinishByTop, - rpl::producer dontStart = nullptr); + not_null list); -} // namespace HistoryView +} // namespace Window diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index d869fcff6e..9e7453d4e0 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_editing.h" #include "api/api_bot.h" #include "api/api_chat_participants.h" +#include "api/api_global_privacy.h" #include "api/api_report.h" #include "api/api_sending.h" #include "api/api_send_progress.h" @@ -517,6 +518,9 @@ HistoryWidget::HistoryWidget( if (_ttlInfo) { _ttlInfo->setVisible(!hide && settings->showAutoDeleteButtonInMessageField); } + if (_giftToUser) { + _giftToUser->setVisible(!hide); + } if (_scheduled) { _scheduled->setVisible(!hide); } @@ -875,8 +879,17 @@ HistoryWidget::HistoryWidget( | PeerUpdateFlag::ChatThemeEmoji | PeerUpdateFlag::FullInfo | PeerUpdateFlag::StarsPerMessage + | PeerUpdateFlag::GiftSettings ) | rpl::filter([=](const Data::PeerUpdate &update) { - return (update.peer.get() == _peer); + if (update.peer.get() == _peer) { + return true; + } else if (update.peer->isSelf() + && (update.flags & PeerUpdateFlag::GiftSettings)) { + refreshSendGiftToggle(); + updateControlsVisibility(); + updateControlsGeometry(); + } + return false; }) | rpl::map([](const Data::PeerUpdate &update) { return update.flags; }) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) { @@ -914,7 +927,11 @@ HistoryWidget::HistoryWidget( updateFieldPlaceholder(); updateSendButtonType(); } - if (flags & PeerUpdateFlag::BotStartToken) { + if (flags & PeerUpdateFlag::GiftSettings) { + refreshSendGiftToggle(); + } + if (flags & (PeerUpdateFlag::BotStartToken + | PeerUpdateFlag::GiftSettings)) { updateControlsVisibility(); updateControlsGeometry(); } @@ -2594,6 +2611,7 @@ void HistoryWidget::showHistory( updateNotifyControls(); } refreshScheduledToggle(); + refreshSendGiftToggle(); refreshSendAsToggle(); if (_showAtMsgId == ShowAtUnreadMsgId) { @@ -2681,6 +2699,18 @@ void HistoryWidget::showHistory( } sendBotStartCommand(); } + } else { + Info::Profile::BirthdayValue( + user + ) | rpl::map( + Data::IsBirthdayTodayValue + ) | rpl::flatten_latest( + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=] { + refreshSendGiftToggle(); + updateControlsVisibility(); + updateControlsGeometry(); + }, _list->lifetime()); } } if (!_history->folderKnown()) { @@ -3098,6 +3128,35 @@ void HistoryWidget::refreshScheduledToggle() { } } +void HistoryWidget::refreshSendGiftToggle() { + using Type = Api::DisallowedGiftType; + const auto user = _peer ? _peer->asUser() : nullptr; + const auto disallowed = user ? user->disallowedGiftTypes() : Type(); + const auto all = Type::Premium + | Type::Unlimited + | Type::Limited + | Type::Unique; + const auto has = user + && _canSendMessages + && !user->isServiceUser() + && !user->isSelf() + && !user->isBot() + && ((disallowed & Type::SendHide) + || (session().user()->disallowedGiftTypes() & Type::SendHide) + || Data::IsBirthdayToday(user->birthday())) + && ((disallowed & all) != all); + if (!_giftToUser && has) { + _giftToUser.create(this, st::historyGiftToUser); + _giftToUser->show(); + _giftToUser->addClickHandler([=] { + Ui::ShowStarGiftBox(controller(), _peer); + }); + orderWidgets(); // Raise drag areas to the top. + } else if (_giftToUser && !has) { + _giftToUser.destroy(); + } +} + void HistoryWidget::setupSendAsToggle() { session().sendAsPeers().updated( ) | rpl::filter([=](not_null peer) { @@ -3268,6 +3327,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_giftToUser) { + _giftToUser->hide(); + } if (_ttlInfo) { _ttlInfo->hide(); } @@ -3380,6 +3442,14 @@ void HistoryWidget::updateControlsVisibility() { rightButtonsChanged = true; } } + if (_giftToUser) { + const auto was = _giftToUser->isVisible(); + const auto now = (!_editMsgId) && (!hideExtraButtons); + if (was != now) { + _giftToUser->setVisible(now); + rightButtonsChanged = true; + } + } if (_ttlInfo) { const auto was = _ttlInfo->isVisible(); const auto now = (!_editMsgId) && (!hideExtraButtons) && settings->showAutoDeleteButtonInMessageField; @@ -3431,6 +3501,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_giftToUser) { + _giftToUser->hide(); + } if (_ttlInfo) { _ttlInfo->hide(); } @@ -5805,7 +5878,7 @@ void HistoryWidget::moveFieldControls() { } // (_botMenu.button) (_attachToggle|_replaceMedia) (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel -// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_silent|_cmdStart|_kbShow) (_kbHide|_tabbedSelectorToggle) _send +// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_kbHide|_tabbedSelectorToggle) _send // (_botStart|_unblock|_joinChannel|_muteUnmute|_reportMessages) auto buttonsBottom = bottom - _attachToggle->height(); @@ -5849,6 +5922,10 @@ void HistoryWidget::moveFieldControls() { if (kbShowShown || (_cmdStartShown && settings->showCommandsButtonInMessageField) || _silent) { right += _botCommandStart->width(); } + if (_giftToUser) { + _giftToUser->moveToRight(right, buttonsBottom); + right += _giftToUser->width(); + } if (_scheduled) { _scheduled->moveToRight(right, buttonsBottom); right += _scheduled->width(); @@ -5909,10 +5986,13 @@ void HistoryWidget::updateFieldSize() { if (_cmdStartShown && settings->showCommandsButtonInMessageField) { fieldWidth -= _botCommandStart->width(); } - if (_silent && _silent->isVisible()) { + if (_silent && !_silent->isHidden()) { fieldWidth -= _silent->width(); } - if (_scheduled && _scheduled->isVisible()) { + if (_giftToUser && !_giftToUser->isHidden()) { + fieldWidth -= _giftToUser->width(); + } + if (_scheduled && !_scheduled->isHidden()) { fieldWidth -= _scheduled->width(); } if (_ttlInfo && _ttlInfo->isVisible() && settings->showAutoDeleteButtonInMessageField) { @@ -6679,21 +6759,24 @@ void HistoryWidget::updateSendRestriction() { _sendRestrictionKey = restriction.text; if (!restriction) { _sendRestriction = nullptr; + } else if (restriction.frozen) { + const auto show = controller()->uiShow(); + _sendRestriction = FrozenWriteRestriction( + this, + show, + FrozenWriteRestrictionType::MessageField); } else if (restriction.premiumToLift) { _sendRestriction = PremiumRequiredSendRestriction( this, _peer->asUser(), controller()); } else if (const auto lifting = restriction.boostsToLift) { - auto button = base::make_unique_q( + const auto show = controller()->uiShow(); + _sendRestriction = BoostsToLiftWriteRestriction( this, - restriction.text, - st::historyComposeButton); - const auto channel = _peer->asChannel(); - button->setClickedCallback([=] { - controller()->resolveBoostState(channel, lifting); - }); - _sendRestriction = std::move(button); + show, + _peer, + lifting); } else { _sendRestriction = TextErrorSendRestriction(this, restriction.text); } @@ -8826,6 +8909,7 @@ bool HistoryWidget::updateCanSendMessage() { cancelReply(); } refreshScheduledToggle(); + refreshSendGiftToggle(); refreshSilentToggle(); return true; } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 8a25d18331..b52b49bbb8 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -675,6 +675,7 @@ private: void setupScheduledToggle(); void refreshScheduledToggle(); + void refreshSendGiftToggle(); void setupSendAsToggle(); void refreshSendAsToggle(); void refreshAttachBotsMenu(); @@ -817,6 +818,7 @@ private: object_ptr _botKeyboardShow; object_ptr _botKeyboardHide; object_ptr _botCommandStart; + object_ptr _giftToUser = { nullptr }; object_ptr _silent = { nullptr }; object_ptr _scheduled = { nullptr }; std::unique_ptr _ttlInfo; @@ -826,7 +828,7 @@ private: bool _cmdStartShown = false; object_ptr _field; base::unique_qptr _fieldDisabled; - base::unique_qptr _sendRestriction; + std::unique_ptr _sendRestriction; using CharactersLimitLabel = HistoryView::Controls::CharactersLimitLabel; base::unique_qptr _charsLimitation; QString _sendRestrictionKey; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 59a1e555ef..ea4bcb3178 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -40,6 +40,7 @@ enum class WriteRestrictionType { None, Rights, PremiumRequired, + Frozen, }; struct WriteRestriction { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index c97e367a9e..6478c20505 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -2324,15 +2324,17 @@ void SetupRestrictionView( ) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](Controls::WriteRestriction value) { using Type = Controls::WriteRestriction::Type; - if (const auto lifting = value.boostsToLift) { - state->button = std::make_unique( + if (value.type == Type::Frozen) { + state->button = FrozenWriteRestriction( widget, - tr::lng_restricted_boost_group(tr::now), - st::historyComposeButton); - state->button->setClickedCallback([=] { - const auto window = show->resolveWindow(); - window->resolveBoostState(peer->asChannel(), lifting); - }); + show, + FrozenWriteRestrictionType::MessageField); + } else if (const auto lifting = value.boostsToLift) { + state->button = BoostsToLiftWriteRestriction( + widget, + show, + peer, + lifting); } else if (value.type == Type::Rights) { state->icon = nullptr; state->unlock = nullptr; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index 5e85a9bb16..ff6b67032c 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" +#include "core/ui_integration.h" #include "lang/lang_keys.h" #include "history/history_item_components.h" #include "history/history_item.h" @@ -449,15 +450,18 @@ void BottomInfo::layoutDateText() { : name.isEmpty() ? (deleted + date) : (deleted + name + afterAuthor); - auto marked = TextWithEntities{ full }; + auto marked = TextWithEntities(); if (const auto count = _data.stars) { - marked.append(Ui::Text::IconEmoji(&st::starIconEmoji)); - marked.append(Lang::FormatCountToShort(count).string); + marked.append( + Ui::Text::IconEmoji(&st::starIconEmojiSmall) + ).append(Lang::FormatCountToShort(count).string).append(u", "_q); } + marked.append(full); _authorEditedDate.setMarkedText( st::msgDateTextStyle, marked, - Ui::NameTextOptions()); + Ui::NameTextOptions(), + Core::TextContext({ .session = &_reactionsOwner->session() })); } else { TextWithEntities deleted; if (_data.flags & Data::Flag::AyuDeleted) { @@ -694,7 +698,7 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { if (item->isSending() || item->hasFailed()) { result.flags |= Flag::Sending; } - if (item->out() && !item->history()->peer->isUser()) { + if (!item->history()->peer->isUser()) { const auto media = message->media(); const auto mine = PaidInformation{ .messages = 1, diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index dcfb00b509..f9fd54ae2b 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1573,14 +1573,6 @@ bool Element::unwrapped() const { return true; } -bool Element::hasFastReply() const { - return false; -} - -bool Element::displayFastReply() const { - return false; -} - std::optional Element::rightActionSize() const { return std::nullopt; } diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index de00c0f48d..af62b3f3b6 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -484,8 +484,6 @@ public: [[nodiscard]] virtual int minWidthForMedia() const { return 0; } - [[nodiscard]] virtual bool hasFastReply() const; - [[nodiscard]] virtual bool displayFastReply() const; [[nodiscard]] virtual std::optional rightActionSize() const; virtual void drawRightAction( Painter &p, diff --git a/Telegram/SourceFiles/history/view/history_view_item_preview.h b/Telegram/SourceFiles/history/view/history_view_item_preview.h index b29da9442d..9656d701ff 100644 --- a/Telegram/SourceFiles/history/view/history_view_item_preview.h +++ b/Telegram/SourceFiles/history/view/history_view_item_preview.h @@ -16,6 +16,9 @@ struct ItemPreviewImage { [[nodiscard]] bool hasSpoiler() const { return (cacheKey & 1); } + [[nodiscard]] bool isEllipse() const { + return (cacheKey & 2); + } explicit operator bool() const { return !data.isNull(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 18740f5011..4d18d32b5e 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1654,8 +1654,9 @@ bool ListWidget::showCopyRestrictionForSelected() { } bool ListWidget::hasSelectRestriction() const { - return _delegate->listSelectRestrictionType() - != CopyRestrictionType::None; + return session().frozen() + || (_delegate->listSelectRestrictionType() + != CopyRestrictionType::None); } Element *ListWidget::lookupItemByY(int y) const { @@ -2821,7 +2822,9 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } const auto link = ClickHandler::getActive(); - if (link + if (controller()->showFrozenError()) { + return; + } else if (link && !link->property( kSendReactionEmojiProperty).value().empty() && _reactionsManager diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index b6683b8e32..4580335c78 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -339,10 +339,19 @@ int KeyboardStyle::minButtonWidth( return result; } +QString FastForwardText() { + return u"Forward"_q; +} + QString FastReplyText() { return tr::lng_fast_reply(tr::now); } +bool ShowFastForwardFor(const QString &username) { + return !username.compare(u"ReviewInsightsBot"_q, Qt::CaseInsensitive) + || !username.compare(u"reviews_bot"_q, Qt::CaseInsensitive); +} + [[nodiscard]] ClickHandlerPtr MakeTopicButtonLink( not_null topic, MsgId messageId) { @@ -993,7 +1002,9 @@ QSize Message::performCountOptimalSize() { namew += st::msgServiceFont->spacew + via->maxWidth + (_fromNameStatus ? st::msgServiceFont->spacew : 0); } - const auto replyWidth = hasFastReply() + const auto replyWidth = hasFastForward() + ? st::msgFont->width(FastForwardText()) + : hasFastReply() ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { @@ -1820,8 +1831,12 @@ void Message::paintFromName( const auto badgeWidth = _rightBadge.isEmpty() ? 0 : _rightBadgeIsChannel ? context.messageStyle()->channelBadgeIcon.width() : _rightBadge.maxWidth(); const auto replyWidth = [&] { - if (isUnderCursor() && displayFastReply()) { - return st::msgFont->width(FastReplyText()); + if (isUnderCursor()) { + if (displayFastForward()) { + return st::msgFont->width(FastForwardText()); + } else if (displayFastReply()) { + return st::msgFont->width(FastReplyText()); + } } return 0; }(); @@ -1920,7 +1935,7 @@ void Message::paintFromName( p.drawText( trect.left() + trect.width() - rightWidth, trect.top() + st::msgFont->ascent, - FastReplyText()); + hasFastForward() ? FastForwardText() : FastReplyText()); } else { if (_rightBadgeIsChannel) { stm->channelBadgeIcon.paint( @@ -2841,8 +2856,12 @@ bool Message::getStateFromName( return false; } const auto replyWidth = [&] { - if (isUnderCursor() && displayFastReply()) { - return st::msgFont->width(FastReplyText()); + if (isUnderCursor()) { + if (displayFastForward()) { + return st::msgFont->width(FastForwardText()); + } else if (displayFastReply()) { + return st::msgFont->width(FastReplyText()); + } } return 0; }(); @@ -3881,6 +3900,22 @@ bool Message::hasFastReply() const { return !hasOutLayout() && (peer->isChat() || peer->isMegagroup()); } +bool Message::hasFastForward() const { + if (context() != Context::History) { + return false; + } + const auto item = data(); + const auto from = item->from()->asUser(); + if (!from || !from->isBot() || !ShowFastForwardFor(from->username())) { + return false; + } + const auto peer = item->history()->peer; + if (!peer->isChat() && !peer->isMegagroup()) { + return false; + } + return !hasOutLayout(); +} + bool Message::displayFastReply() const { const auto canSendAnything = [&] { const auto item = data(); @@ -3897,6 +3932,12 @@ bool Message::displayFastReply() const { && !delegate()->elementInSelectionMode(this).inSelectionMode; } +bool Message::displayFastForward() const { + return hasFastForward() + && data()->allowsForward() + && !delegate()->elementInSelectionMode(this).inSelectionMode; +} + bool Message::displayRightActionComments() const { return !isPinnedContext() && (context() != Context::SavedSublist) @@ -4169,9 +4210,22 @@ ClickHandlerPtr Message::fastReplyLink() const { return _fastReplyLink; } const auto itemId = data()->fullId(); - _fastReplyLink = std::make_shared([=] { - delegate()->elementReplyTo({ itemId }); - }); + const auto sessionId = data()->history()->session().uniqueId(); + _fastReplyLink = hasFastForward() + ? std::make_shared([=](ClickContext context) { + const auto controller = ExtractController(context); + const auto session = controller + ? &controller->session() + : nullptr; + if (!session || session->uniqueId() != sessionId) { + return; + } else if (const auto item = session->data().message(itemId)) { + FastShareMessage(controller, item); + } + }) + : std::make_shared(crl::guard(this, [=] { + delegate()->elementReplyTo({ itemId }); + })); return _fastReplyLink; } @@ -4260,7 +4314,9 @@ void Message::updateMediaInBubbleState() { void Message::fromNameUpdated(int width) const { const auto item = data(); - const auto replyWidth = hasFastReply() + const auto replyWidth = hasFastForward() + ? st::msgFont->width(FastForwardText()) + : hasFastReply() ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 1885dbab1c..a2996d8c38 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -129,8 +129,6 @@ public: TopicButton *displayedTopicButton() const override; bool unwrapped() const override; int minWidthForMedia() const override; - bool hasFastReply() const override; - bool displayFastReply() const override; bool displayRightActionComments() const; std::optional rightActionSize() const override; void drawRightAction( @@ -275,6 +273,10 @@ private: [[nodiscard]] int visibleMediaTextLength() const; [[nodiscard]] bool needInfoDisplay() const; [[nodiscard]] bool invertMedia() const; + [[nodiscard]] bool hasFastReply() const; + [[nodiscard]] bool hasFastForward() const; + [[nodiscard]] bool displayFastReply() const; + [[nodiscard]] bool displayFastForward() const; [[nodiscard]] bool isPinnedContext() const; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 7cd506b107..5a3006e327 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item_components.h" #include "history/history_item.h" +#include "history/history_view_swipe_back_session.h" #include "ui/boxes/confirm_box.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" @@ -181,6 +182,7 @@ PinnedWidget::PinnedWidget( setupClearButton(); setupTranslateBar(); + Window::SetupSwipeBackSection(this, _scroll.get(), _inner); } PinnedWidget::~PinnedWidget() = default; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 9f4ca309d3..0cef51408d 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -25,9 +25,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_drag_area.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" // GetErrorForSending. -#include "history/history_view_swipe.h" #include "ui/chat/pinned_bar.h" #include "ui/chat/chat_style.h" +#include "ui/controls/swipe_handler.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" @@ -415,7 +415,7 @@ RepliesWidget::RepliesWidget( setupTopicViewer(); setupComposeControls(); - setupSwipeReply(); + setupSwipeReplyAndBack(); orderWidgets(); if (_pinnedBar) { @@ -679,12 +679,22 @@ void RepliesWidget::setupComposeControls() { : tr::lng_forum_topic_closed(tr::now); }); auto writeRestriction = rpl::combine( + session().frozenValue(), session().changes().peerFlagsValue( _history->peer, Data::PeerUpdate::Flag::Rights), Data::CanSendAnythingValue(_history->peer), std::move(topicWriteRestrictions) - ) | rpl::map([=](auto, auto, Data::SendError topicRestriction) { + ) | rpl::map([=]( + const Main::FreezeInfo &info, + auto, + auto, + Data::SendError topicRestriction) { + if (info) { + return Controls::WriteRestriction{ + .type = Controls::WriteRestrictionType::Frozen, + }; + } const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; const auto canSendAnything = _topic @@ -885,7 +895,7 @@ void RepliesWidget::setupComposeControls() { } } -void RepliesWidget::setupSwipeReply() { +void RepliesWidget::setupSwipeReplyAndBack() { const auto can = [=](not_null still) { const auto canSendReply = _topic ? Data::CanSendAnything(_topic) @@ -898,8 +908,27 @@ void RepliesWidget::setupSwipeReply() { } return false; }; - HistoryView::SetupSwipeHandler(_inner, _scroll.get(), [=]( - HistoryView::ChatPaintGestureHorizontalData data) { + + auto update = [=](Ui::Controls::SwipeContextData data) { + if (data.translation > 0) { + if (!_swipeBackData.callback) { + _swipeBackData = Ui::Controls::SetupSwipeBack( + this, + [=]() -> std::pair { + const auto context = listPreparePaintContext({ + .theme = listChatTheme(), + }); + return { + context.st->msgServiceBg()->c, + context.st->msgServiceFg()->c, + }; + }); + } + _swipeBackData.callback(data); + return; + } else if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } const auto changed = (_gestureHorizontal.msgBareId != data.msgBareId) || (_gestureHorizontal.translation != data.translation) || (_gestureHorizontal.reachRatio != data.reachRatio); @@ -912,8 +941,17 @@ void RepliesWidget::setupSwipeReply() { _history->owner().requestItemRepaint(item); } } - }, [=, show = controller()->uiShow()](int cursorTop) { - auto result = HistoryView::SwipeHandlerFinishData(); + }; + + auto init = [=, show = controller()->uiShow()]( + int cursorTop, + Qt::LayoutDirection direction) { + if (direction == Qt::RightToLeft) { + return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + controller()->showBackFromStack(); + }); + } + auto result = Ui::Controls::SwipeHandlerFinishData(); if (_inner->elementInSelectionMode(nullptr).inSelectionMode) { return result; } @@ -944,7 +982,15 @@ void RepliesWidget::setupSwipeReply() { }); }; return result; - }, _inner->touchMaybeSelectingValue()); + }; + + Ui::Controls::SetupSwipeHandler({ + .widget = _inner, + .scroll = _scroll.get(), + .update = std::move(update), + .init = std::move(init), + .dontStart = _inner->touchMaybeSelectingValue(), + }); } void RepliesWidget::chooseAttach( diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 5d7758b7fb..3bbe7f5c59 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -12,8 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_corner_buttons.h" #include "history/view/history_view_list_widget.h" #include "history/history_item_helpers.h" -#include "history/history_view_swipe_data.h" #include "data/data_messages.h" +#include "ui/controls/swipe_handler_data.h" #include "base/timer.h" class History; @@ -234,7 +234,7 @@ private: void finishSending(); void setupComposeControls(); - void setupSwipeReply(); + void setupSwipeReplyAndBack(); void setupRoot(); void setupRootView(); @@ -393,7 +393,8 @@ private: HistoryView::CornerButtons _cornerButtons; rpl::lifetime _topicLifetime; - HistoryView::ChatPaintGestureHorizontalData _gestureHorizontal; + Ui::Controls::SwipeContextData _gestureHorizontal; + Ui::Controls::SwipeBackResult _swipeBackData; SendPaymentHelper _sendPayment; diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 84283bced9..47b6bb89d1 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_drag_area.h" #include "history/history_item_helpers.h" // GetErrorForSending. +#include "history/history_view_swipe_back_session.h" #include "menu/menu_send.h" // SendMenu::Type. #include "ui/widgets/buttons.h" #include "ui/widgets/tooltip.h" @@ -253,6 +254,7 @@ ScheduledWidget::ScheduledWidget( _inner->setEmptyInfoWidget(std::move(emptyInfo)); } setupComposeControls(); + Window::SetupSwipeBackSection(this, _scroll, _inner); } ScheduledWidget::~ScheduledWidget() = default; @@ -274,15 +276,22 @@ void ScheduledWidget::setupComposeControls() { : tr::lng_forum_topic_closed(tr::now); }); return rpl::combine( + session().frozenValue(), session().changes().peerFlagsValue( _history->peer, Data::PeerUpdate::Flag::Rights), Data::CanSendAnythingValue(_history->peer), std::move(topicWriteRestrictions) ) | rpl::map([=]( + const Main::FreezeInfo &info, auto, auto, Data::SendError topicRestriction) { + if (info) { + return Controls::WriteRestriction{ + .type = Controls::WriteRestrictionType::Frozen, + }; + } const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; const auto canSendAnything = Data::CanSendAnyOf( @@ -309,11 +318,17 @@ void ScheduledWidget::setupComposeControls() { }() : [&] { return rpl::combine( + session().frozenValue(), session().changes().peerFlagsValue( _history->peer, Data::PeerUpdate::Flag::Rights), Data::CanSendAnythingValue(_history->peer) - ) | rpl::map([=] { + ) | rpl::map([=](const Main::FreezeInfo &info, auto, auto) { + if (info) { + return Controls::WriteRestriction{ + .type = Controls::WriteRestrictionType::Frozen, + }; + } const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; const auto canSendAnything = Data::CanSendAnyOf( diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp index a7a89c76d0..0262545cef 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_list_widget.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_view_swipe_back_session.h" #include "lang/lang_keys.h" #include "ui/chat/chat_style.h" #include "ui/widgets/buttons.h" @@ -154,6 +155,7 @@ SublistWidget::SublistWidget( setupShortcuts(); setupTranslateBar(); + Window::SetupSwipeBackSection(this, _scroll.get(), _inner); } SublistWidget::~SublistWidget() = default; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 6033ec9293..d5f7ff3140 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -300,7 +300,9 @@ void TopBarWidget::refreshLang() { } void TopBarWidget::call() { - if (const auto peer = _activeChat.key.peer()) { + if (_controller->showFrozenError()) { + return; + } else if (const auto peer = _activeChat.key.peer()) { if (const auto user = peer->asUser()) { Core::App().calls().startOutgoingCall(user, false); } @@ -308,7 +310,9 @@ void TopBarWidget::call() { } void TopBarWidget::groupCall() { - if (const auto peer = _activeChat.key.peer()) { + if (_controller->showFrozenError()) { + return; + } else if (const auto peer = _activeChat.key.peer()) { if (HasGroupCallMenu(peer)) { showGroupCallMenu(peer); } else { diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 700d6a1237..f9a9b031ca 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -479,7 +479,8 @@ ClickHandlerPtr OpenStarGiftLink(not_null item) { Settings::SavedStarGiftBox, window, owner, - *parsed)); + *parsed, + nullptr)); } } }).fail([=](const MTP::Error &error) { diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style index 5d68e501b0..86fa3772d8 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style +++ b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style @@ -116,7 +116,7 @@ channelEarnFadeDuration: 60; channelEarnLearnDescription: FlatLabel(defaultFlatLabel) { maxHeight: 0px; - minWidth: 280px; + minWidth: 264px; align: align(top); } diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index 9187b0251d..f392e132ed 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/controls/swipe_handler.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/fields/input_field.h" #include "ui/wrap/padding_wrap.h" @@ -161,6 +162,8 @@ Ui::RpWidget *ContentWidget::doSetInnerWidget( _innerWrap ? _innerWrap->padding() : style::margins())); _innerWrap->move(0, 0); + setupSwipeHandler(_innerWrap); + // MSVC BUG + REGRESSION rpl::mappers::tuple :( rpl::combine( _scroll->scrollTopValue(), @@ -176,6 +179,24 @@ Ui::RpWidget *ContentWidget::doSetInnerWidget( _scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0)); }, _innerWrap->lifetime()); + rpl::combine( + _scroll->heightValue(), + _innerWrap->entity()->heightValue(), + _controller->wrapValue() + ) | rpl::start_with_next([=]( + int scrollHeight, + int innerHeight, + Wrap wrap) { + const auto added = (wrap == Wrap::Layer) + ? 0 + : std::max(scrollHeight - innerHeight, 0); + if (_addedHeight != added) { + _addedHeight = added; + updateInnerPadding(); + } + }, _innerWrap->lifetime()); + updateInnerPadding(); + return _innerWrap->entity(); } @@ -205,11 +226,19 @@ rpl::producer ContentWidget::scrollHeightValue() const { } void ContentWidget::applyAdditionalScroll(int additionalScroll) { - if (_innerWrap) { - _innerWrap->setPadding({ 0, 0, 0, additionalScroll }); + if (_additionalScroll != additionalScroll) { + _additionalScroll = additionalScroll; + if (_innerWrap) { + updateInnerPadding(); + } } } +void ContentWidget::updateInnerPadding() { + const auto addedToBottom = std::max(_additionalScroll, _addedHeight); + _innerWrap->setPadding({ 0, 0, 0, addedToBottom }); +} + void ContentWidget::applyMaxVisibleHeight(int maxVisibleHeight) { if (_maxVisibleHeight != maxVisibleHeight) { _maxVisibleHeight = maxVisibleHeight; @@ -381,6 +410,58 @@ not_null ContentWidget::scroll() const { return _scroll.data(); } +void ContentWidget::replaceSwipeHandler( + Ui::Controls::SwipeHandlerArgs *incompleteArgs) { + _swipeHandlerLifetime.destroy(); + auto args = std::move(*incompleteArgs); + args.widget = _innerWrap; + args.scroll = _scroll.data(); + args.onLifetime = &_swipeHandlerLifetime; + Ui::Controls::SetupSwipeHandler(std::move(args)); +} + +void ContentWidget::setupSwipeHandler(not_null widget) { + _swipeHandlerLifetime.destroy(); + + auto update = [=](Ui::Controls::SwipeContextData data) { + if (data.translation > 0) { + if (!_swipeBackData.callback) { + _swipeBackData = Ui::Controls::SetupSwipeBack( + this, + []() -> std::pair { + return { + st::historyForwardChooseBg->c, + st::historyForwardChooseFg->c, + }; + }); + } + _swipeBackData.callback(data); + return; + } else if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } + }; + + auto init = [=](int, Qt::LayoutDirection direction) { + return (direction == Qt::RightToLeft && _controller->hasBackButton()) + ? Ui::Controls::DefaultSwipeBackHandlerFinishData([=] { + checkBeforeClose(crl::guard(this, [=] { + _controller->parentController()->hideLayer(); + _controller->showBackFromStack(); + })); + }) + : Ui::Controls::SwipeHandlerFinishData(); + }; + + Ui::Controls::SetupSwipeHandler({ + .widget = widget, + .scroll = _scroll.data(), + .update = std::move(update), + .init = std::move(init), + .onLifetime = &_swipeHandlerLifetime, + }); +} + Key ContentMemento::key() const { if (const auto topic = this->topic()) { return Key(topic); diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 0fafdfe01c..359f46787a 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_wrap_widget.h" #include "info/statistics/info_statistics_tag.h" +#include "ui/controls/swipe_handler_data.h" namespace Api { struct WhoReadList; @@ -23,6 +24,9 @@ enum class SharedMediaType : signed char; } // namespace Storage namespace Ui { +namespace Controls { +struct SwipeHandlerArgs; +} // namespace Controls class RoundRect; class ScrollArea; class InputField; @@ -134,6 +138,8 @@ public: [[nodiscard]] virtual auto desiredBottomShadowVisibility() -> rpl::producer; + void replaceSwipeHandler(Ui::Controls::SwipeHandlerArgs *incompleteArgs); + protected: template Widget *setInnerWidget(object_ptr inner) { @@ -167,6 +173,8 @@ private: RpWidget *doSetInnerWidget(object_ptr inner); void updateControlsGeometry(); void refreshSearchField(bool shown); + void setupSwipeHandler(not_null widget); + void updateInnerPadding(); virtual std::shared_ptr doCreateMemento() = 0; @@ -181,6 +189,8 @@ private: base::unique_qptr _searchWrap = nullptr; QPointer _searchField; int _innerDesiredHeight = 0; + int _additionalScroll = 0; + int _addedHeight = 0; int _maxVisibleHeight = 0; bool _isStackBottom = false; @@ -190,6 +200,9 @@ private: // To paint round edges from content. style::margins _paintPadding; + Ui::Controls::SwipeBackResult _swipeBackData; + rpl::lifetime _swipeHandlerLifetime; + }; class ContentMemento { diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index dc51c31e15..b1ec130dc5 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -358,6 +358,10 @@ void Controller::setSection(not_null memento) { updateSearchControllers(memento); } +bool Controller::hasBackButton() const { + return _widget->hasBackButton(); +} + void Controller::updateSearchControllers( not_null memento) { using Type = Section::Type; diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 9815c3bc0d..c82a8b9f5a 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -311,6 +311,7 @@ public: [[nodiscard]] rpl::producer wrapValue() const; [[nodiscard]] not_null wrapWidget() const; void setSection(not_null memento); + [[nodiscard]] bool hasBackButton() const; Ui::SearchFieldController *searchFieldController() const { return _searchFieldController.get(); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 81fec3a719..cce6dee136 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -1063,6 +1063,11 @@ bool WrapWidget::willHaveBackButton( return (wrap() == Wrap::Narrow) || willHaveStack; } +void WrapWidget::replaceSwipeHandler( + Ui::Controls::SwipeHandlerArgs *incompleteArgs) { + _content->replaceSwipeHandler(std::move(incompleteArgs)); +} + WrapWidget::~WrapWidget() = default; } // namespace Info diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index 815765154a..40d4459d44 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -15,6 +15,9 @@ enum class SharedMediaType : signed char; } // namespace Storage namespace Ui { +namespace Controls { +struct SwipeHandlerArgs; +} // namespace Controls class FadeShadow; class PlainShadow; class PopupMenu; @@ -122,6 +125,7 @@ public: object_ptr createTopBarSurrogate(QWidget *parent); + [[nodiscard]] bool hasBackButton() const; [[nodiscard]] bool closeByOutsideClick() const; void updateGeometry( @@ -139,6 +143,8 @@ public: return _removeRequests.events(); } + void replaceSwipeHandler(Ui::Controls::SwipeHandlerArgs *incompleteArgs); + ~WrapWidget(); protected: @@ -181,7 +187,6 @@ private: void highlightTopBar(); void setupShortcuts(); - [[nodiscard]] bool hasBackButton() const; [[nodiscard]] bool willHaveBackButton( const Window::SectionShow ¶ms) const; diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index 12d7827f4b..1531602855 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -294,6 +294,9 @@ not_null AddPeerGiftsButton( }, tracker)->entity(); result->addClickHandler([=] { + if (navigation->showFrozenError()) { + return; + } navigation->showSection( std::make_shared( peer, diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index 62099db0eb..ec5dc252c9 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -67,7 +67,9 @@ Type Provider::type() { } bool Provider::hasSelectRestriction() { - if (_peer->allowsForwarding()) { + if (_peer->session().frozen()) { + return true; + } else if (_peer->allowsForwarding()) { return false; } else if (const auto chat = _peer->asChat()) { return !chat->canDeleteMessages(); diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index 803e7c4330..fb014da15a 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -7,20 +7,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/peer_gifts/info_peer_gifts_common.h" +#include "boxes/send_credits_box.h" // SetButtonMarkedLabel #include "boxes/star_gift_box.h" #include "boxes/sticker_set_box.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "chat_helpers/stickers_lottie.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" +#include "data/data_credits.h" // CreditsHistoryEntry #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_credits_graphics.h" +#include "ui/layers/generic_box.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" #include "ui/effects/premium_graphics.h" @@ -137,7 +142,9 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { _price.setMarkedText( st::semiboldTextStyle, (unique - ? tr::lng_gift_price_unique(tr::now, Ui::Text::WithEntities) + ? tr::lng_gift_transfer_button( + tr::now, + Ui::Text::WithEntities) : _delegate->star().append( ' ' + Lang::FormatCountDecimal(data.info.stars))), kMarkupTextOptions, @@ -240,6 +247,79 @@ void GiftButton::setGeometry(QRect inner, QMargins extend) { AbstractButton::setGeometry(inner.marginsAdded(extend)); } +QMargins GiftButton::currentExtend() const { + const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + const auto added = anim::interpolate(0, st::giftBoxSelectSkip, progress); + return _extend + QMargins(added, added, added, added); +} + +void GiftButton::toggleSelected(bool selected) { + if (_selected == selected) { + return; + } + const auto duration = st::defaultRoundCheckbox.duration; + _selected = selected; + _selectedAnimation.start([=] { + update(); + }, selected ? 0. : 1., selected ? 1. : 0., duration, anim::easeOutCirc); +} + +void GiftButton::paintBackground(QPainter &p, const QImage &background) { + const auto removed = currentExtend() - _extend; + const auto x = removed.left(); + const auto y = removed.top(); + const auto width = this->width() - x - removed.right(); + const auto height = this->height() - y - removed.bottom(); + const auto dpr = int(background.devicePixelRatio()); + const auto bwidth = background.width() / dpr; + const auto bheight = background.height() / dpr; + const auto fillRow = [&](int yfrom, int ytill, int bfrom) { + const auto fill = [&](int xto, int wto, int xfrom, int wfrom = 0) { + const auto fheight = ytill - yfrom; + p.drawImage( + QRect(x + xto, y + yfrom, wto, fheight), + background, + QRect( + QPoint(xfrom, bfrom) * dpr, + QSize((wfrom ? wfrom : wto), fheight) * dpr)); + }; + if (width < bwidth) { + const auto xhalf = width / 2; + fill(0, xhalf, 0); + fill(xhalf, width - xhalf, bwidth - (width - xhalf)); + } else if (width == bwidth) { + fill(0, width, 0); + } else { + const auto half = bwidth / (2 * dpr); + fill(0, half, 0); + fill(width - half, half, bwidth - half); + fill(half, width - 2 * half, half, 1); + } + }; + if (height < bheight) { + fillRow(0, height / 2, 0); + fillRow(height / 2, height, bheight - (height - (height / 2))); + } else { + fillRow(0, height, 0); + } + + auto hq = PainterHighQualityEnabler(p); + const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + if (progress < 0.01) { + return; + } + const auto pwidth = progress * st::defaultRoundCheckbox.width; + p.setPen(QPen(st::defaultRoundCheckbox.bgActive->c, pwidth)); + p.setBrush(Qt::NoBrush); + const auto rounded = rect().marginsRemoved(_extend); + const auto phalf = pwidth / 2.; + const auto extended = QRectF(rounded).marginsRemoved( + { phalf, phalf, phalf, phalf }); + const auto xradius = removed.left() + st::giftBoxGiftRadius - phalf; + const auto yradius = removed.top() + st::giftBoxGiftRadius - phalf; + p.drawRoundedRect(extended, xradius, yradius); +} + void GiftButton::resizeEvent(QResizeEvent *e) { if (!_button.isEmpty()) { _button.moveLeft((width() - _button.width()) / 2); @@ -267,9 +347,10 @@ void GiftButton::cacheUniqueBackground( [[maybe_unused]] const auto preload = _uniquePatternEmoji->ready(); } const auto outer = QRect(0, 0, width, height); + const auto extend = currentExtend(); const auto inner = outer.marginsRemoved( - _extend - ).translated(-_extend.left(), -_extend.top()); + extend + ).translated(-extend.left(), -extend.top()); const auto ratio = style::DevicePixelRatio(); if (_uniqueBackgroundCache.size() != inner.size() * ratio) { _uniqueBackgroundCache = QImage( @@ -313,32 +394,15 @@ void GiftButton::paintEvent(QPaintEvent *e) { : nullptr; const auto hidden = v::is(_descriptor) && v::get(_descriptor).hidden;; - const auto position = QPoint(_extend.left(), _extend.top()); + const auto extend = currentExtend(); + const auto position = QPoint(extend.left(), extend.top()); const auto background = _delegate->background(); - const auto dpr = int(background.devicePixelRatio()); const auto width = this->width(); - if (width * dpr <= background.width()) { - p.drawImage(0, 0, background); - } else { - const auto full = background.width(); - const auto half = ((full / 2) / dpr) * dpr; - const auto height = background.height(); - p.drawImage( - QRect(0, 0, half / dpr, height / dpr), - background, - QRect(0, 0, half, height)); - p.drawImage( - QRect(width - (half / dpr), 0, half / dpr, height / dpr), - background, - QRect(full - half, 0, half, height)); - p.drawImage( - QRect(half / dpr, 0, width - 2 * (half / dpr), height / dpr), - background, - QRect(half, 0, 1, height)); - } + const auto dpr = int(background.devicePixelRatio()); + paintBackground(p, background); if (unique) { cacheUniqueBackground(unique, width, background.height() / dpr); - p.drawImage(_extend.left(), _extend.top(), _uniqueBackgroundCache); + p.drawImage(extend.left(), extend.top(), _uniqueBackgroundCache); } if (_userpic) { @@ -348,7 +412,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { } const auto image = _userpic->image(st::giftBoxUserpicSize); const auto skip = st::giftBoxUserpicSkip; - p.drawImage(_extend.left() + skip, _extend.top() + skip, image); + p.drawImage(extend.left() + skip, extend.top() + skip, image); } auto frame = QImage(); @@ -401,7 +465,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { auto hq = PainterHighQualityEnabler(p); const auto premium = v::is(_descriptor); - const auto singlew = width - _extend.left() - _extend.right(); + const auto singlew = width - extend.left() - extend.right(); const auto font = st::semiboldFont; p.setFont(font); @@ -420,11 +484,17 @@ void GiftButton::paintEvent(QPaintEvent *e) { } return GiftBadge(); }, [&](const GiftTypeStars &data) { - if (const auto count = data.info.limitedCount) { - const auto soldOut = !data.userpic && !data.info.limitedLeft; + const auto count = data.info.limitedCount; + const auto pinned = data.pinned || data.pinnedSelection; + if (count || pinned) { + const auto soldOut = !pinned + && !data.userpic + && !data.info.limitedLeft; return GiftBadge{ .text = (soldOut ? tr::lng_gift_stars_sold_out(tr::now) + : (unique && pinned) + ? ('#' + QString::number(unique->number)) : (!data.userpic && !data.info.unique) ? tr::lng_gift_stars_limited(tr::now) : (count == 1) @@ -452,7 +522,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { if (badge) { const auto rubberOut = st::lineWidth; - const auto inner = rect().marginsRemoved(_extend); + const auto inner = rect().marginsRemoved(extend); p.setClipRect(inner.marginsAdded( { rubberOut, rubberOut, rubberOut, rubberOut })); @@ -474,7 +544,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { p.setPen(Qt::NoPen); p.setBrush(unique->backdrop.patternColor); const auto rect = QRect( - QPoint(_extend.left() + skip, _extend.top() + skip), + QPoint(extend.left() + skip, extend.top() + skip), QSize(icon.width() + 2 * add, icon.height() + 2 * add)); p.drawEllipse(rect); icon.paintInCenter(p, rect); @@ -547,12 +617,10 @@ void GiftButton::paintEvent(QPaintEvent *e) { } } -Delegate::Delegate( - not_null window, - GiftButtonMode mode) -: _window(window) +Delegate::Delegate(not_null session, GiftButtonMode mode) +: _session(session) , _hiddenMark(std::make_unique( - &window->session(), + _session, st::giftBoxHiddenMark, RectPart::Center)) , _mode(mode) { @@ -563,18 +631,17 @@ Delegate::Delegate(Delegate &&other) = default; Delegate::~Delegate() = default; TextWithEntities Delegate::star() { - const auto owner = &_window->session().data(); - return owner->customEmojiManager().creditsEmoji(); + return _session->data().customEmojiManager().creditsEmoji(); } TextWithEntities Delegate::ministar() { - const auto owner = &_window->session().data(); + const auto owner = &_session->data(); const auto top = st::giftBoxByStarsStarTop; return owner->customEmojiManager().ministarEmoji({ 0, top, 0, 0 }); } Ui::Text::MarkedContext Delegate::textContext() { - return Core::TextContext({ .session = &_window->session() }); + return Core::TextContext({ .session = _session }); } QSize Delegate::buttonSize() { @@ -601,7 +668,7 @@ auto Delegate::buttonPatternEmoji( not_null unique, Fn repaint) -> std::unique_ptr { - return _window->session().data().customEmojiManager().create( + return _session->data().customEmojiManager().create( unique->pattern.document, repaint, Data::CustomEmojiSizeTag::Large); @@ -658,7 +725,7 @@ QImage Delegate::background() { rpl::producer> Delegate::sticker( const GiftDescriptor &descriptor) { - return GiftStickerValue(&_window->session(), descriptor); + return GiftStickerValue(_session, descriptor); } not_null Delegate::hiddenMark() { @@ -784,4 +851,143 @@ QImage ValidateRotatedBadge(const GiftBadge &badge, int added) { return result; } +void SelectGiftToUnpin( + std::shared_ptr show, + const std::vector &pinned, + Fn chosen) { + show->show(Box([=](not_null box) { + struct State { + explicit State(not_null session) + : delegate(session, GiftButtonMode::Minimal) { + } + + Delegate delegate; + rpl::variable selected = -1; + std::vector> buttons; + }; + const auto session = &show->session(); + const auto state = box->lifetime().make_state(session); + + box->setStyle(st::giftTooManyPinnedBox); + box->setWidth(st::boxWideWidth); + + box->addRow( + object_ptr( + box, + tr::lng_gift_many_pinned_title(), + st::giftBoxSubtitle), + st::giftBoxSubtitleMargin); + box->addRow( + object_ptr( + box, + tr::lng_gift_many_pinned_choose(), + st::giftTooManyPinnedChoose), + st::giftBoxAboutMargin); + + const auto gifts = box->addRow( + object_ptr(box), + QMargins( + st::giftBoxPadding.left(), + st::giftTooManyPinnedBox.buttonPadding.top(), + st::giftBoxPadding.right(), + 0)); + for (const auto &entry : pinned) { + const auto index = int(state->buttons.size()); + state->buttons.push_back( + Ui::CreateChild(gifts, &state->delegate)); + const auto button = state->buttons.back(); + button->setDescriptor(GiftTypeStars{ + .info = { + .id = entry.stargiftId, + .unique = entry.uniqueGift, + .document = entry.uniqueGift->model.document, + }, + .pinnedSelection = true, + }, GiftButton::Mode::Minimal); + button->setClickedCallback([=] { + const auto now = state->selected.current(); + state->selected = (now == index) ? -1 : index; + }); + } + + state->selected.value( + ) | rpl::combine_previous( + ) | rpl::start_with_next([=](int old, int now) { + if (old >= 0) state->buttons[old]->toggleSelected(false); + if (now >= 0) state->buttons[now]->toggleSelected(true); + }, gifts->lifetime()); + + gifts->widthValue() | rpl::start_with_next([=](int width) { + const auto singleMin = state->delegate.buttonSize(); + if (width < singleMin.width()) { + return; + } + const auto count = int(state->buttons.size()); + const auto skipw = st::giftBoxGiftSkip.x(); + const auto skiph = st::giftBoxGiftSkip.y(); + const auto perRow = std::min( + (width + skipw) / (singleMin.width() + skipw), + std::max(count, 1)); + if (perRow <= 0) { + return; + } + const auto single = (width - (perRow - 1) * skipw) / perRow; + const auto height = singleMin.height(); + const auto rows = (count + perRow - 1) / perRow; + for (auto row = 0; row != rows; ++row) { + const auto y = row * (height + skiph); + for (auto column = 0; column != perRow; ++column) { + const auto index = row * perRow + column; + if (index >= count) { + break; + } + const auto &button = state->buttons[index]; + const auto x = column * (single + skipw); + button->setGeometry( + QRect(x, y, single, height), + state->delegate.buttonExtend()); + } + } + gifts->resize(width, rows * (height + skiph) - skiph); + }, gifts->lifetime()); + + const auto button = box->addButton(rpl::single(QString()), [=] { + const auto index = state->selected.current(); + if (index < 0) { + return; + } + Assert(index < int(pinned.size())); + const auto &entry = pinned[index]; + const auto weak = Ui::MakeWeak(box); + chosen(::Settings::EntryToSavedStarGiftId(session, entry)); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + const auto label = Ui::SetButtonMarkedLabel( + button, + tr::lng_context_unpin_from_top(Ui::Text::WithEntities), + &show->session(), + st::creditsBoxButtonLabel, + &st::giftTooManyPinnedBox.button.textFg); + + state->selected.value() | rpl::start_with_next([=](int value) { + const auto has = (value >= 0); + label->setOpacity(has ? 1. : 0.5); + button->setAttribute(Qt::WA_TransparentForMouseEvents, !has); + }, box->lifetime()); + + const auto buttonPadding = st::giftTooManyPinnedBox.buttonPadding; + const auto buttonWidth = st::boxWideWidth + - buttonPadding.left() + - buttonPadding.right(); + button->resizeToWidth(buttonWidth); + button->widthValue() | rpl::start_with_next([=](int width) { + if (width != buttonWidth) { + button->resizeToWidth(buttonWidth); + } + }, button->lifetime()); + })); +} + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h index 0d93a8bf21..aa852493a1 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -15,8 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class StickerPremiumMark; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { struct UniqueGift; +struct CreditsHistoryEntry; +class SavedStarGiftId; } // namespace Data namespace HistoryView { @@ -54,13 +60,15 @@ struct GiftTypePremium { }; struct GiftTypeStars { + Data::SavedStarGiftId transferId; Data::StarGift info; PeerData *from = nullptr; TimeId date = 0; - bool userpic = false; - bool pinned = false; - bool hidden = false; - bool mine = false; + bool pinnedSelection : 1 = false; + bool userpic : 1 = false; + bool pinned : 1 = false; + bool hidden : 1 = false; + bool mine : 1 = false; [[nodiscard]] friend inline bool operator==( const GiftTypeStars&, @@ -128,6 +136,8 @@ public: void setDescriptor(const GiftDescriptor &descriptor, Mode mode); void setGeometry(QRect inner, QMargins extend); + void toggleSelected(bool selected); + [[nodiscard]] rpl::producer contextMenuRequests() const { return _contextMenuRequests.events(); } @@ -137,6 +147,7 @@ private: void resizeEvent(QResizeEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; + void paintBackground(QPainter &p, const QImage &background); void cacheUniqueBackground( not_null unique, int width, @@ -144,6 +155,7 @@ private: void setDocument(not_null document); [[nodiscard]] bool documentResolved() const; + [[nodiscard]] QMargins currentExtend() const; void unsubscribe(); @@ -159,8 +171,10 @@ private: std::unique_ptr _uniquePatternEmoji; base::flat_map _uniquePatternCache; std::optional _stars; + Ui::Animations::Simple _selectedAnimation; bool _subscribed = false; bool _patterned = false; + bool _selected = false; bool _small = false; QRect _button; @@ -173,9 +187,7 @@ private: class Delegate final : public GiftButtonDelegate { public: - Delegate( - not_null window, - GiftButtonMode mode); + Delegate(not_null session, GiftButtonMode mode); Delegate(Delegate &&other); ~Delegate(); @@ -195,7 +207,7 @@ public: QImage cachedBadge(const GiftBadge &badge) override; private: - const not_null _window; + const not_null _session; std::unique_ptr _hiddenMark; base::flat_map _badges; QSize _single; @@ -214,4 +226,9 @@ private: [[nodiscard]] QImage ValidateRotatedBadge(const GiftBadge &badge, int added); +void SelectGiftToUnpin( + std::shared_ptr show, + const std::vector &pinned, + Fn chosen); + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 683501acf6..88745c6b92 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -112,6 +112,9 @@ private: int resizeGetHeight(int width) override; + [[nodiscard]] auto pinnedSavedGifts() + -> Fn()>; + const not_null _window; rpl::variable _filter; Delegate _delegate; @@ -152,7 +155,7 @@ InnerWidget::InnerWidget( : BoxContentDivider(parent) , _window(controller->parentController()) , _filter(std::move(filter)) -, _delegate(_window, GiftButtonMode::Minimal) +, _delegate(&_window->session(), GiftButtonMode::Minimal) , _controller(controller) , _peer(peer) , _totalCount(_peer->peerGiftsCount()) @@ -225,6 +228,9 @@ void InnerWidget::subscribeToUpdates() { return; } refreshButtons(); + if (update.action == Action::Pin) { + _scrollToTop.fire({}); + } }, lifetime()); } @@ -476,6 +482,41 @@ void InnerWidget::validateButtons() { std::swap(_views, views); } +auto InnerWidget::pinnedSavedGifts() +-> Fn()> { + struct Entry { + Data::SavedStarGiftId id; + std::shared_ptr unique; + }; + auto entries = std::vector(); + for (const auto &entry : _entries) { + if (entry.gift.pinned) { + Assert(entry.gift.info.unique != nullptr); + entries.push_back({ + entry.gift.manageId, + entry.gift.info.unique, + }); + } else { + break; + } + } + return [entries] { + auto result = std::vector(); + result.reserve(entries.size()); + for (const auto &entry : entries) { + const auto &id = entry.id; + result.push_back({ + .bareMsgId = uint64(id.userMessageId().bare), + .bareEntryOwnerId = id.chat() ? id.chat()->id.value : 0, + .giftChannelSavedId = id.chatSavedId(), + .uniqueGift = entry.unique, + .stargift = true, + }); + } + return result; + }; +} + void InnerWidget::showMenuFor(not_null button, QPoint point) { if (_menu) { return; @@ -495,27 +536,7 @@ void InnerWidget::showMenuFor(not_null button, QPoint point) { auto entry = ::Settings::SavedStarGiftEntry( _peer, _entries[index].gift); - auto pinnedIds = std::vector(); - for (const auto &entry : _entries) { - if (entry.gift.pinned) { - pinnedIds.push_back(entry.gift.manageId); - } else { - break; - } - } - entry.pinnedSavedGifts = [pinnedIds] { - auto result = std::vector(); - result.reserve(pinnedIds.size()); - for (const auto &id : pinnedIds) { - result.push_back({ - .bareMsgId = uint64(id.userMessageId().bare), - .bareEntryOwnerId = id.chat() ? id.chat()->id.value : 0, - .giftChannelSavedId = id.chatSavedId(), - .stargift = true, - }); - } - return result; - }; + entry.pinnedSavedGifts = pinnedSavedGifts(); _menu = base::make_unique_q(this, st::popupMenuWithIcons); ::Settings::FillSavedStarGiftMenu( _controller->uiShow(), @@ -535,7 +556,8 @@ void InnerWidget::showGift(int index) { ::Settings::SavedStarGiftBox, _window, _peer, - _entries[index].gift)); + _entries[index].gift, + pinnedSavedGifts())); } void InnerWidget::refreshAbout() { diff --git a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp index 18fdcce09f..24afb84377 100644 --- a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp +++ b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/polls/info_polls_results_widget.h" #include "lang/lang_keys.h" +#include "core/ui_integration.h" #include "data/data_peer.h" #include "data/data_poll.h" #include "data/data_session.h" @@ -461,7 +462,9 @@ ListController *CreateAnswerRows( .append(QString::fromUtf8(" \xe2\x80\x94 ")) .append(QString::number(percent)) .append('%')), - st::boxDividerLabel), + st::boxDividerLabel, + st::defaultPopupMenu, + Core::TextContext({ .session = session })), style::margins( st::pollResultsHeaderPadding.left(), st::pollResultsHeaderPadding.top(), @@ -610,7 +613,9 @@ void InnerWidget::setupContent() { object_ptr( _content, rpl::single(_poll->question), - st::pollResultsQuestion), + st::pollResultsQuestion, + st::defaultPopupMenu, + Core::TextContext({ .session = &_controller->session() })), st::boxRowPadding); Ui::AddSkip(_content, st::boxLittleSkip / 2); _content->add( diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index b3e41b57d0..034b784775 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -1819,25 +1819,28 @@ object_ptr DetailsFiller::setupPersonalChannel( const auto preview = Ui::CreateChild(line); auto &lifetime = preview->lifetime(); using namespace Dialogs::Ui; - const auto previewView = lifetime.make_state(); - preview->resize(0, st::infoLabeled.style.font->height); - const auto prepare = [previewView, preview]( - not_null item) { - previewView->prepare( - item, - nullptr, - [=] { preview->update(); }, - {}, - []{}); + struct State { + MessageView view; + HistoryItem *item = nullptr; + rpl::lifetime lifetime; }; - if (!previewView->dependsOn(item)) { - prepare(item); - } + const auto state = lifetime.make_state(); + state->item = item; + item->history()->session().changes().realtimeMessageUpdates( + Data::MessageUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + if (update.item == state->item) { + state->lifetime.destroy(); + state->item = nullptr; + preview->update(); + } + }, state->lifetime); + + preview->resize(0, st::infoLabeled.style.font->height); preview->paintRequest( - ) | rpl::start_with_next([=, fullId = item->fullId()]( - const QRect &rect) { + ) | rpl::start_with_next([=] { auto p = Painter(preview); - const auto item = user->session().data().message(fullId); + const auto item = state->item; if (!item) { p.setPen(st::infoPersonalChannelDateLabel.textFg); p.setBrush(Qt::NoBrush); @@ -1848,22 +1851,14 @@ object_ptr DetailsFiller::setupPersonalChannel( style::al_left); return; } - if (previewView->prepared(item, nullptr)) { - previewView->paint(p, preview->rect(), { - .st = &st::defaultDialogRow, - .currentBg = st::boxBg->b, - }); - } else if (!previewView->dependsOn(item)) { - p.setPen(st::infoPersonalChannelDateLabel.textFg); - p.setBrush(Qt::NoBrush); - p.setFont(st::infoPersonalChannelDateLabel.style.font); - p.drawText( - preview->rect(), - tr::lng_contacts_loading(tr::now), - style::al_left); - prepare(item); - preview->update(); + if (!state->view.prepared(item, nullptr)) { + const auto repaint = [=] { preview->update(); }; + state->view.prepare(item, nullptr, repaint, {}); } + state->view.paint(p, preview->rect(), { + .st = &st::defaultDialogRow, + .currentBg = st::boxBg->b, + }); }, preview->lifetime()); line->sizeValue() | rpl::filter_size( @@ -2462,11 +2457,17 @@ void ActionsFiller::addShareContactAction(not_null user) { void ActionsFiller::addEditContactAction(not_null user) { const auto controller = _controller->parentController(); + const auto edit = [=] { + if (controller->showFrozenError()) { + return; + } + controller->window().show(Box(EditContactBox, controller, user)); + }; AddActionButton( _wrap, tr::lng_info_edit_contact(), IsContactValue(user), - [=] { controller->window().show(Box(EditContactBox, controller, user)); }, + edit, &st::infoIconEdit); } diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp index 6c9a7dabb3..f6863bee8a 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -75,7 +75,9 @@ Type Provider::type() { } bool Provider::hasSelectRestriction() { - if (const auto channel = _peer->asChannel()) { + if (_peer->session().frozen()) { + return true; + } else if (const auto channel = _peer->asChannel()) { return !channel->canEditStories() && !channel->canDeleteStories(); } return !_peer->isSelf(); diff --git a/Telegram/SourceFiles/intro/intro_code.cpp b/Telegram/SourceFiles/intro/intro_code.cpp index e48e8bb59e..d26625ae95 100644 --- a/Telegram/SourceFiles/intro/intro_code.cpp +++ b/Telegram/SourceFiles/intro/intro_code.cpp @@ -293,6 +293,9 @@ void CodeWidget::callDone(const MTPauth_SentCode &result) { } }, [&](const MTPDauth_sentCodeSuccess &data) { finish(data.vauthorization()); + }, [](const MTPDauth_sentCodePaymentRequired &) { + LOG(("API Error: Unexpected auth.sentCodePaymentRequired " + "(CodeWidget::callDone).")); }); } @@ -408,6 +411,9 @@ void CodeWidget::noTelegramCodeDone(const MTPauth_SentCode &result) { updateDescText(); }, [&](const MTPDauth_sentCodeSuccess &data) { finish(data.vauthorization()); + }, [](const MTPDauth_sentCodePaymentRequired &) { + LOG(("API Error: Unexpected auth.sentCodePaymentRequired " + "(CodeWidget::noTelegramCodeDone).")); }); } diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 5e82616742..ae74136c8c 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -245,6 +245,9 @@ void PhoneWidget::phoneSubmitDone(const MTPauth_SentCode &result) { goNext(); }, [&](const MTPDauth_sentCodeSuccess &data) { finish(data.vauthorization()); + }, [](const MTPDauth_sentCodePaymentRequired &) { + LOG(("API Error: Unexpected auth.sentCodePaymentRequired " + "(PhoneWidget::phoneSubmitDone).")); }); } diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 4326d7559c..594bad12da 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -24,6 +24,7 @@ AppConfig::AppConfig(not_null account) : _account(account) { ) | rpl::filter([=](Session *session) { return (session != nullptr); }) | rpl::start_with_next([=] { + _lastFrozenRefresh = 0; refresh(); }, _lifetime); } @@ -35,6 +36,18 @@ void AppConfig::start() { ) | rpl::start_with_next([=](not_null instance) { _api.emplace(instance); refresh(); + + _frozenTrackLifetime = instance->frozenErrorReceived( + ) | rpl::start_with_next([=] { + if (!get(u"freeze_since_date"_q, 0)) { + const auto now = crl::now(); + if (!_lastFrozenRefresh + || now > _lastFrozenRefresh + kRefreshTimeout) { + _lastFrozenRefresh = now; + refresh(); + } + } + }); }, _lifetime); } diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 308656bbe4..1cf3aa89cd 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -124,6 +124,9 @@ private: std::vector _startRefPrefixes; + crl::time _lastFrozenRefresh = 0; + rpl::lifetime _frozenTrackLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 66a573f531..4a2d4a18c7 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -202,15 +202,6 @@ Session::Session( } }, _lifetime); -#ifndef OS_MAC_STORE - appConfig().value( - ) | rpl::start_with_next([=] { - _premiumPossible = !appConfig().get( - u"premium_purchase_blocked"_q, - true); - }, _lifetime); -#endif // OS_MAC_STORE - if (_settings->hadLegacyCallsPeerToPeerNobody()) { api().userPrivacy().save( Api::UserPrivacy::Key::CallsPeer2Peer, @@ -247,9 +238,30 @@ Session::Session( Core::App().downloadManager().trackSession(this); + appConfig().value( + ) | rpl::start_with_next([=] { + appConfigRefreshed(); + }, _lifetime); + InitializeBlockedPeers(this); } +void Session::appConfigRefreshed() { + const auto &config = appConfig(); + + _frozen = FreezeInfo{ + .since = config.get(u"freeze_since_date"_q, 0), + .until = config.get(u"freeze_until_date"_q, 0), + .appealUrl = config.get(u"freeze_appeal_url"_q, QString()), + }; + +#ifndef OS_MAC_STORE + _premiumPossible = !config.get( + u"premium_purchase_blocked"_q, + true); +#endif // OS_MAC_STORE +} + void Session::setTmpPassword(const QByteArray &password, TimeId validUntil) { if (_tmpPassword.isEmpty() || validUntil > _tmpPasswordValidUntil) { _tmpPassword = password; @@ -492,6 +504,14 @@ Support::FastButtonsBots &Session::fastButtonsBots() const { return *_fastButtonsBots; } +FreezeInfo Session::frozen() const { + return _frozen.current(); +} + +rpl::producer Session::frozenValue() const { + return _frozen.value(); +} + void Session::addWindow(not_null controller) { _windows.emplace(controller); controller->lifetime().add([=] { diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 677405d28c..65543d0969 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -80,6 +80,19 @@ class Domain; class SessionSettings; class SendAsPeers; +struct FreezeInfo { + TimeId since = 0; + TimeId until = 0; + QString appealUrl; + + explicit operator bool() const { + return since != 0; + } + friend inline bool operator==( + const FreezeInfo &, + const FreezeInfo &) = default; +}; + class Session final : public base::has_weak_ptr { public: Session( @@ -236,12 +249,17 @@ public: [[nodiscard]] Support::Templates &supportTemplates() const; [[nodiscard]] Support::FastButtonsBots &fastButtonsBots() const; + [[nodiscard]] FreezeInfo frozen() const; + [[nodiscard]] rpl::producer frozenValue() const; + [[nodiscard]] auto colorIndicesValue() -> rpl::producer; private: static constexpr auto kDefaultSaveDelay = crl::time(1000); + void appConfigRefreshed(); + const UserId _userId; const not_null _account; @@ -288,6 +306,8 @@ private: base::flat_set> _windows; base::Timer _saveSettingsTimer; + rpl::variable _frozen; + QByteArray _tmpPassword; TimeId _tmpPasswordValidUntil = 0; diff --git a/Telegram/SourceFiles/main/session/session_show.cpp b/Telegram/SourceFiles/main/session/session_show.cpp index a9914e87d8..1f740d4413 100644 --- a/Telegram/SourceFiles/main/session/session_show.cpp +++ b/Telegram/SourceFiles/main/session/session_show.cpp @@ -7,6 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "main/session/session_show.h" +#include "chat_helpers/message_field.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/layers/generic_box.h" + namespace Main { namespace { @@ -70,6 +75,14 @@ Session &SimpleSessionShow::session() const { } // namespace +bool SessionShow::showFrozenError() { + if (!session().frozen()) { + return false; + } + showBox(Box(FrozenInfoBox, &session(), FreezeInfoStyleOverride())); + return true; +} + std::shared_ptr MakeSessionShow( std::shared_ptr show, not_null session) { diff --git a/Telegram/SourceFiles/main/session/session_show.h b/Telegram/SourceFiles/main/session/session_show.h index ae764e1254..16f8e6f913 100644 --- a/Telegram/SourceFiles/main/session/session_show.h +++ b/Telegram/SourceFiles/main/session/session_show.h @@ -16,6 +16,9 @@ class Session; class SessionShow : public Ui::Show { public: [[nodiscard]] virtual Main::Session &session() const = 0; + + bool showFrozenError(); + }; [[nodiscard]] std::shared_ptr MakeSessionShow( diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 21ae900503..4587335876 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -841,7 +841,9 @@ void ReplyArea::show( peer ) | rpl::map([=](bool can) { using namespace HistoryView::Controls; - return (can + return peer->session().frozen() + ? WriteRestriction{ .type = WriteRestrictionType::Frozen } + : (can || !user || !user->requiresPremiumToWrite() || user->session().premium()) diff --git a/Telegram/SourceFiles/menu/menu_sponsored.cpp b/Telegram/SourceFiles/menu/menu_sponsored.cpp index 231da1ee00..0f431f538d 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.cpp +++ b/Telegram/SourceFiles/menu/menu_sponsored.cpp @@ -42,15 +42,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Menu { namespace { +[[nodiscard]] SponsoredPhrases PhrasesForMessage(FullMsgId fullId) { + return peerIsChannel(fullId.peer) + ? SponsoredPhrases::Channel + : SponsoredPhrases::Bot; +} + void AboutBox( not_null box, std::shared_ptr show, - const FullMsgId &fullId) { + SponsoredPhrases phrases, + const Data::SponsoredMessages::Details &details, + Data::SponsoredReportAction report) { constexpr auto kUrl = "https://promote.telegram.org"_cs; - box->setNoContentMargin(true); + box->setWidth(st::boxWideWidth); - const auto isChannel = peerIsChannel(fullId.peer); + const auto isChannel = (phrases == SponsoredPhrases::Channel); + const auto isSearch = (phrases == SponsoredPhrases::Search); const auto session = &show->session(); const auto content = box->verticalLayout().get(); @@ -138,20 +147,24 @@ void AboutBox( tr::lng_sponsored_revenued_info1_title(), (isChannel ? tr::lng_sponsored_revenued_info1_description + : isSearch + ? tr::lng_sponsored_revenued_info1_search_description : tr::lng_sponsored_revenued_info1_bot_description)( Ui::Text::RichLangValue), st::sponsoredAboutPrivacyIcon); - Ui::AddSkip(content); - Ui::AddSkip(content); - addEntry( - (isChannel - ? tr::lng_sponsored_revenued_info2_title - : tr::lng_sponsored_revenued_info2_bot_title)(), - (isChannel - ? tr::lng_sponsored_revenued_info2_description - : tr::lng_sponsored_revenued_info2_bot_description)( - Ui::Text::RichLangValue), - st::sponsoredAboutSplitIcon); + if (!isSearch) { + Ui::AddSkip(content); + Ui::AddSkip(content); + addEntry( + (isChannel + ? tr::lng_sponsored_revenued_info2_title + : tr::lng_sponsored_revenued_info2_bot_title)(), + (isChannel + ? tr::lng_sponsored_revenued_info2_description + : tr::lng_sponsored_revenued_info2_bot_description)( + Ui::Text::RichLangValue), + st::sponsoredAboutSplitIcon); + } Ui::AddSkip(content); Ui::AddSkip(content); auto link = tr::lng_settings_privacy_premium_link( @@ -160,17 +173,32 @@ void AboutBox( }); addEntry( tr::lng_sponsored_revenued_info3_title(), - isChannel + (isChannel ? tr::lng_sponsored_revenued_info3_description( lt_count, rpl::single(float64(levels)), lt_link, std::move(link), Ui::Text::RichLangValue) + : isSearch + ? tr::lng_sponsored_revenued_info3_search_description( + lt_link, + tr::lng_sponsored_revenued_info3_search_link( + lt_arrow, + rpl::single( + Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + Ui::Text::WithEntities + ) | rpl::map([](TextWithEntities &&link) { + return Ui::Text::Wrapped( + std::move(link), + EntityType::CustomUrl, + u"internal:"_q); + }), + Ui::Text::RichLangValue) : tr::lng_sponsored_revenued_info3_bot_description( lt_link, std::move(link), - Ui::Text::RichLangValue), + Ui::Text::RichLangValue)), st::sponsoredAboutRemoveIcon)->setClickHandlerFilter([=]( const auto &...) { ShowPremiumPreviewBox(show, PremiumFeature::NoAds); @@ -200,6 +228,8 @@ void AboutBox( content, (isChannel ? tr::lng_sponsored_revenued_footer_description + : isSearch + ? tr::lng_sponsored_revenued_footer_search_description : tr::lng_sponsored_revenued_footer_bot_description)( lt_link, tr::lng_channel_earn_about_link( @@ -256,7 +286,9 @@ void AboutBox( top, Ui::Menu::CreateAddActionCallback(menu->get()), show, - fullId, + phrases, + details, + report, false, true); const auto global = top->mapToGlobal( @@ -269,14 +301,11 @@ void AboutBox( return true; }); } - } void ShowReportSponsoredBox( std::shared_ptr show, - const FullMsgId &fullId) { - auto &sponsoredMessages = show->session().sponsoredMessages(); - const auto report = sponsoredMessages.createReportCallback(fullId); + Data::SponsoredReportAction report) { const auto guideLink = Ui::Text::Link( tr::lng_report_sponsored_reported_link(tr::now), u"https://promote.telegram.org/guidelines"_q); @@ -284,7 +313,7 @@ void ShowReportSponsoredBox( auto performRequest = [=]( const auto &repeatRequest, Data::SponsoredReportResult::Id id) -> void { - report(id, [=](const Data::SponsoredReportResult &result) { + report.callback(id, [=](const Data::SponsoredReportResult &result) { if (!result.error.isEmpty()) { show->showToast(result.error); } @@ -360,11 +389,12 @@ void FillSponsored( not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, - const FullMsgId &fullId, + SponsoredPhrases phrases, + const Data::SponsoredMessages::Details &details, + Data::SponsoredReportAction report, bool mediaViewer, bool skipAbout) { const auto session = &show->session(); - const auto details = session->sponsoredMessages().lookupDetails(fullId); const auto &info = details.info; if (!mediaViewer && !info.empty()) { @@ -408,12 +438,12 @@ void FillSponsored( if (details.canReport) { if (!skipAbout) { addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] { - show->show(Box(AboutBox, show, fullId)); + show->show(Box(AboutBox, show, phrases, details, report)); }, (mediaViewer ? &st::mediaMenuIconInfo : &st::menuIconInfo)); } addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] { - ShowReportSponsoredBox(show, fullId); + ShowReportSponsoredBox(show, report); }, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock)); addAction({ @@ -426,14 +456,32 @@ void FillSponsored( addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { if (session->premium()) { using Result = Data::SponsoredReportResult; - session->sponsoredMessages().createReportCallback( - fullId)(Result::Id("-1"), [](const auto &) {}); + report.callback(Result::Id("-1"), [](const auto &) {}); } else { ShowPremiumPreviewBox(show, PremiumFeature::NoAds); } }, (mediaViewer ? &st::mediaMenuIconCancel : &st::menuIconCancel)); } +void FillSponsored( + not_null parent, + const Ui::Menu::MenuCallback &addAction, + std::shared_ptr show, + const FullMsgId &fullId, + bool mediaViewer, + bool skipAbout) { + const auto session = &show->session(); + FillSponsored( + parent, + addAction, + show, + PhrasesForMessage(fullId), + session->sponsoredMessages().lookupDetails(fullId), + session->sponsoredMessages().createReportCallback(fullId), + mediaViewer, + skipAbout); +} + void ShowSponsored( not_null parent, std::shared_ptr show, @@ -455,8 +503,14 @@ void ShowSponsored( void ShowSponsoredAbout( std::shared_ptr show, const FullMsgId &fullId) { + const auto session = &show->session(); show->showBox(Box([=](not_null box) { - AboutBox(box, show, fullId); + AboutBox( + box, + show, + PhrasesForMessage(fullId), + session->sponsoredMessages().lookupDetails(fullId), + session->sponsoredMessages().createReportCallback(fullId)); })); } diff --git a/Telegram/SourceFiles/menu/menu_sponsored.h b/Telegram/SourceFiles/menu/menu_sponsored.h index 4061271a98..106509e2e6 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.h +++ b/Telegram/SourceFiles/menu/menu_sponsored.h @@ -11,6 +11,11 @@ namespace ChatHelpers { class Show; } // namespace ChatHelpers +namespace Data { +struct SponsoredMessageDetails; +struct SponsoredReportAction; +} // namespace Data + namespace Ui { class RpWidget; namespace Menu { @@ -22,6 +27,22 @@ class HistoryItem; namespace Menu { +enum class SponsoredPhrases { + Channel, + Bot, + Search, +}; + +void FillSponsored( + not_null parent, + const Ui::Menu::MenuCallback &addAction, + std::shared_ptr show, + SponsoredPhrases phrases, + const Data::SponsoredMessageDetails &details, + Data::SponsoredReportAction report, + bool mediaViewer, + bool skipAbout); + void FillSponsored( not_null parent, const Ui::Menu::MenuCallback &addAction, diff --git a/Telegram/SourceFiles/menu/menu_ttl_validator.cpp b/Telegram/SourceFiles/menu/menu_ttl_validator.cpp index b388e70160..99d78f80ee 100644 --- a/Telegram/SourceFiles/menu/menu_ttl_validator.cpp +++ b/Telegram/SourceFiles/menu/menu_ttl_validator.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_user.h" #include "lang/lang_keys.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "menu/menu_ttl.h" #include "ui/layers/generic_box.h" @@ -133,6 +134,9 @@ const style::icon *TTLValidator::icon() const { } void TTLValidator::showBox() const { + if (Main::MakeSessionShow(_show, &_peer->session())->showFrozenError()) { + return; + } _show->showBox(Box(TTLBox, createArgs())); } diff --git a/Telegram/SourceFiles/mtproto/mtp_instance.cpp b/Telegram/SourceFiles/mtproto/mtp_instance.cpp index ee10958370..1890c4485d 100644 --- a/Telegram/SourceFiles/mtproto/mtp_instance.cpp +++ b/Telegram/SourceFiles/mtproto/mtp_instance.cpp @@ -100,6 +100,7 @@ public: [[nodiscard]] auto nonPremiumDelayedRequests() const -> rpl::producer; + [[nodiscard]] rpl::producer<> frozenErrorReceived() const; void restart(); void restart(ShiftedDcId shiftedDcId); @@ -286,6 +287,7 @@ private: Fn _sessionResetHandler; rpl::event_stream _nonPremiumDelayedRequests; + rpl::event_stream<> _frozenErrorReceived; base::Timer _checkDelayedTimer; @@ -562,6 +564,10 @@ auto Instance::Private::nonPremiumDelayedRequests() const return _nonPremiumDelayedRequests.events(); } +rpl::producer<> Instance::Private::frozenErrorReceived() const { + return _frozenErrorReceived.events(); +} + void Instance::Private::requestConfigIfOld() { const auto timeout = _config->values().blockedMode ? kConfigBecomesOldForBlockedIn @@ -1593,6 +1599,8 @@ bool Instance::Private::onErrorDefault( return true; } else if (type == u"CONNECTION_LANG_CODE_INVALID"_q) { Lang::CurrentCloudManager().resetToDefault(); + } else if (type == u"FROZEN_METHOD_INVALID"_q) { + _frozenErrorReceived.fire({}); } if (badGuestDc) _badGuestDcRequests.erase(requestId); return false; @@ -1920,6 +1928,10 @@ rpl::producer Instance::nonPremiumDelayedRequests() const { return _private->nonPremiumDelayedRequests(); } +rpl::producer<> Instance::frozenErrorReceived() const { + return _private->frozenErrorReceived(); +} + void Instance::requestConfigIfOld() { _private->requestConfigIfOld(); } diff --git a/Telegram/SourceFiles/mtproto/mtp_instance.h b/Telegram/SourceFiles/mtproto/mtp_instance.h index 55401065f6..eeb56d29cc 100644 --- a/Telegram/SourceFiles/mtproto/mtp_instance.h +++ b/Telegram/SourceFiles/mtproto/mtp_instance.h @@ -142,6 +142,7 @@ public: [[nodiscard]] auto nonPremiumDelayedRequests() const -> rpl::producer; + [[nodiscard]] rpl::producer<> frozenErrorReceived() const; void syncHttpUnixtime(); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 830b263966..37fb71c23f 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -185,6 +185,8 @@ messageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long c messageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long transaction_id:string boost_peer:Peer giveaway_msg_id:int = MessageAction; messageActionStarGift#4717e8a4 flags:# name_hidden:flags.0?true saved:flags.2?true converted:flags.3?true upgraded:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true gift:StarGift message:flags.1?TextWithEntities convert_stars:flags.4?long upgrade_msg_id:flags.5?int upgrade_stars:flags.8?long from_id:flags.11?Peer peer:flags.12?Peer saved_id:flags.12?long = MessageAction; messageActionStarGiftUnique#acdfcb81 flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true gift:StarGift can_export_at:flags.3?int transfer_stars:flags.4?long from_id:flags.6?Peer peer:flags.7?Peer saved_id:flags.7?long = MessageAction; +messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction; +messageActionPaidMessagesPrice#bcd71419 stars:long = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -204,6 +206,7 @@ geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radiu auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; auth.sentCodeSuccess#2390fe44 authorization:auth.Authorization = auth.SentCode; +auth.sentCodePaymentRequired#d7cef980 store_product:string phone_code_hash:string = auth.SentCode; auth.authorization#2ea2c0d4 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int future_auth_token:flags.2?bytes user:User = auth.Authorization; auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; @@ -236,7 +239,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#d2234ea0 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long = UserFull; +userFull#99e78045 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -427,6 +430,7 @@ updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long conne updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update; updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; +updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1311,7 +1315,7 @@ statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInvite stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector top_admins:Vector top_inviters:Vector users:Vector = stats.MegagroupStats; -globalPrivacySettings#c9d8df1c flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true noncontact_peers_paid_stars:flags.5?long = GlobalPrivacySettings; +globalPrivacySettings#fe41b34f flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true display_gifts_button:flags.7?true noncontact_peers_paid_stars:flags.5?long disallowed_gifts:flags.6?DisallowedGiftsSettings = GlobalPrivacySettings; help.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector patterns:flags.1?Vector = help.CountryCode; @@ -1495,6 +1499,7 @@ inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?t inputStorePaymentStarsTopup#dddd0f56 stars:long currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentStarsGiveaway#751f08fa flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true stars:long boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long users:int = InputStorePaymentPurpose; +inputStorePaymentAuthCode#9bb2636d flags:# restore:flags.0?true phone_number:string phone_code_hash:string currency:string amount:long = InputStorePaymentPurpose; paymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod; @@ -1753,7 +1758,7 @@ inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; messages.quickReplies#c68d6695 quick_replies:Vector messages:Vector chats:Vector users:Vector = messages.QuickReplies; messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; -connectedBot#bd068601 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessBotRecipients = ConnectedBot; +connectedBot#cd64636c flags:# bot_id:long recipients:BusinessBotRecipients rights:BusinessBotRights = ConnectedBot; account.connectedBots#17d7f87b connected_bots:Vector users:Vector = account.ConnectedBots; @@ -1761,7 +1766,7 @@ messages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector birthday#6c8e1e06 flags:# day:int month:int year:flags.0?int = Birthday; -botBusinessConnection#896433b4 flags:# can_reply:flags.0?true disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int = BotBusinessConnection; +botBusinessConnection#8f34b2f5 flags:# disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int rights:flags.2?BusinessBotRights = BotBusinessConnection; inputBusinessIntro#9c469cd flags:# title:string description:string sticker:flags.0?InputDocument = InputBusinessIntro; @@ -1945,6 +1950,15 @@ requirementToContactEmpty#50a9839 = RequirementToContact; requirementToContactPremium#e581e4e9 = RequirementToContact; requirementToContactPaidMessages#b4f67e93 stars_amount:long = RequirementToContact; +businessBotRights#a0624cf7 flags:# reply:flags.0?true read_messages:flags.1?true delete_sent_messages:flags.2?true delete_received_messages:flags.3?true edit_name:flags.4?true edit_bio:flags.5?true edit_profile_photo:flags.6?true edit_username:flags.7?true view_gifts:flags.8?true sell_gifts:flags.9?true change_gift_settings:flags.10?true transfer_and_upgrade_gifts:flags.11?true transfer_stars:flags.12?true manage_stories:flags.13?true = BusinessBotRights; + +disallowedGiftsSettings#71f276c4 flags:# disallow_unlimited_stargifts:flags.0?true disallow_limited_stargifts:flags.1?true disallow_unique_stargifts:flags.2?true disallow_premium_gifts:flags.3?true = DisallowedGiftsSettings; + +sponsoredPeer#c69708d3 flags:# random_id:bytes peer:Peer sponsor_info:flags.0?string additional_info:flags.1?string = SponsoredPeer; + +contacts.sponsoredPeersEmpty#ea32b4b1 = contacts.SponsoredPeers; +contacts.sponsoredPeers#eb032884 peers:Vector chats:Vector users:Vector = contacts.SponsoredPeers; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2079,7 +2093,7 @@ account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?Bus account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool; account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool; account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool; -account.updateConnectedBot#43d8521d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessBotRecipients = Updates; +account.updateConnectedBot#66a08c7e flags:# deleted:flags.1?true rights:flags.0?BusinessBotRights bot:InputUser recipients:InputBusinessBotRecipients = Updates; account.getConnectedBots#4ea4c80f = account.ConnectedBots; account.getBotBusinessConnection#76a86270 connection_id:string = Updates; account.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool; @@ -2130,6 +2144,7 @@ contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector limit:int = Bool; contacts.getBirthdays#daeda864 = contacts.ContactBirthdays; +contacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; @@ -2348,9 +2363,9 @@ messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:fla messages.sendPaidReaction#58bbcb50 flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?PaidReactionPrivacy = Updates; messages.togglePaidReactionPrivacy#435885b5 peer:InputPeer msg_id:int private:PaidReactionPrivacy = Bool; messages.getPaidReactionPrivacy#472455aa = Updates; -messages.viewSponsoredMessage#673ad8f1 peer:InputPeer random_id:bytes = Bool; -messages.clickSponsoredMessage#f093465 flags:# media:flags.0?true fullscreen:flags.1?true peer:InputPeer random_id:bytes = Bool; -messages.reportSponsoredMessage#1af3dbb8 peer:InputPeer random_id:bytes option:bytes = channels.SponsoredMessageReportResult; +messages.viewSponsoredMessage#269e3643 random_id:bytes = Bool; +messages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool; +messages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult; messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages; messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector = messages.BotPreparedInlineMessage; messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; @@ -2506,7 +2521,6 @@ payments.getBankCardData#2e79d779 number:string = payments.BankCardData; payments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoice; payments.assignAppStoreTransaction#80ed747d receipt:bytes purpose:InputStorePaymentPurpose = Updates; payments.assignPlayMarketTransaction#dffd50d3 receipt:DataJSON purpose:InputStorePaymentPurpose = Updates; -payments.canPurchasePremium#9fc19eb6 purpose:InputStorePaymentPurpose = Bool; payments.getPremiumGiftCodeOptions#2757ba54 flags:# boost_peer:flags.0?InputPeer = Vector; payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode; payments.applyGiftCode#f6e26854 slug:string = Updates; @@ -2544,6 +2558,7 @@ payments.getSavedStarGift#b455a106 stargift:Vector = payment payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl; payments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool; payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector = Bool; +payments.canPurchaseStore#4fdc5ea7 purpose:InputStorePaymentPurpose = Bool; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2664,4 +2679,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 200 +// LAYER 201 diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index 6f5fd49165..a82cecffbb 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -2229,6 +2229,9 @@ void FormController::startPhoneVerification(not_null value) { }, [](const MTPDauth_sentCodeSuccess &) { LOG(("API Error: Unexpected auth.sentCodeSuccess " "(FormController::startPhoneVerification).")); + }, [](const MTPDauth_sentCodePaymentRequired &) { + LOG(("API Error: Unexpected auth.sentCodePaymentRequired " + "(FormController::startPhoneVerification).")); }); }).fail([=](const MTP::Error &error) { value->verification.requestId = 0; diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index 47b92fb728..c100e00e06 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/options.h" #include "base/platform/base_platform_info.h" #include "base/platform/linux/base_linux_dbus_utilities.h" +#include "platform/platform_specific.h" #include "core/application.h" #include "core/sandbox.h" #include "core/core_settings.h" @@ -27,6 +28,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include + #include #include @@ -547,7 +550,7 @@ void Manager::Private::showNotification( notification.set_body(info.message.toStdString()); notification.set_icon( - Gio::ThemedIcon::new_(base::IconName().toStdString())); + Gio::ThemedIcon::new_(ApplicationIconName().toStdString())); // for chat messages, according to // https://docs.gtk.org/gio/enum.NotificationPriority.html @@ -761,7 +764,7 @@ void Manager::Private::showNotification( AppName.data(), 0, (!hasImage - ? base::IconName().toStdString() + ? ApplicationIconName().toStdString() : std::string()).c_str(), (hasBodyMarkup || info.subtitle.isEmpty() ? info.title.toStdString() diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 193fa258d9..76bea01808 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -484,23 +484,45 @@ void InstallLauncher() { const auto icons = QStandardPaths::writableLocation( QStandardPaths::GenericDataLocation) + u"/icons/"_q; - if (!QDir(icons).exists()) QDir().mkpath(icons); + const auto appIcons = icons + u"/hicolor/256x256/apps/"_q; + if (!QDir(appIcons).exists()) QDir().mkpath(appIcons); - const auto icon = icons + base::IconName() + u".png"_q; + const auto icon = appIcons + ApplicationIconName() + u".png"_q; QFile::remove(icon); + QFile::remove(icons + u"telegram.png"_q); if (QFile::copy(u":/gui/art/logo_256.png"_q, icon)) { DEBUG_LOG(("App Info: Icon copied to '%1'").arg(icon)); } + const auto symbolicIcons = icons + u"/hicolor/symbolic/apps/"_q; + if (!QDir().exists(symbolicIcons)) QDir().mkpath(symbolicIcons); + + const auto monochromeIcons = { + QString(), + u"attention"_q, + u"mute"_q, + }; + + for (const auto &icon : monochromeIcons) { + QFile::copy( + u":/gui/icons/tray/monochrome%1.svg"_q.arg( + !icon.isEmpty() ? u"_"_q + icon : QString()), + symbolicIcons + + ApplicationIconName() + + (!icon.isEmpty() ? u"-"_q + icon : QString()) + + u"-symbolic.svg"_q); + } + QProcess::execute("update-desktop-database", { applicationsPath }); } -[[nodiscard]] QByteArray HashForSocketPath(const QByteArray &data) { +[[nodiscard]] QByteArray HashForSocketPath() { constexpr auto kHashForSocketPathLength = 24; - const auto binary = openssl::Sha256(bytes::make_span(data)); + const auto binary = openssl::Sha256( + bytes::make_span(Core::Launcher::Instance().instanceHash())); const auto base64 = QByteArray( reinterpret_cast(binary.data()), binary.size()).toBase64(QByteArray::Base64UrlEncoding); @@ -656,10 +678,6 @@ int psFixPrevious() { namespace Platform { void start() { - const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath()); - char h[33] = { 0 }; - hashMd5Hex(d.constData(), d.size(), h); - QGuiApplication::setDesktopFileName([&] { if (KSandbox::isFlatpak()) { return qEnvironmentVariable("FLATPAK_ID"); @@ -672,18 +690,8 @@ void start() { } if (!Core::UpdaterDisabled()) { - QByteArray md5Hash(h); - if (!Core::Launcher::Instance().customWorkingDir()) { - const auto exePath = QFile::encodeName( - cExeDir() + cExeName()); - - hashMd5Hex( - exePath.constData(), - exePath.size(), - md5Hash.data()); - } - - return u"com.ayugram.desktop._%1"_q.arg(md5Hash.constData()); + return u"com.ayugram.desktop._%1"_q.arg( + Core::Launcher::Instance().instanceHash().constData()); } return u"com.ayugram.desktop"_q; @@ -697,14 +705,16 @@ void start() { } qputenv("PULSE_PROP_application.name", AppName.utf8()); - qputenv("PULSE_PROP_application.icon_name", base::IconName().toLatin1()); + qputenv( + "PULSE_PROP_application.icon_name", + ApplicationIconName().toUtf8()); GLib::set_prgname(cExeName().toStdString()); GLib::set_application_name(AppName.data()); Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-%4"_q.arg( QDir::tempPath(), - HashForSocketPath(d), + HashForSocketPath(), u"TD"_q,//QCoreApplication::applicationName(), - make path smaller. u"%1"_q).toStdString()); @@ -768,6 +778,14 @@ QImage DefaultApplicationIcon() { return Window::Logo(); } +QString ApplicationIconName() { + static const auto Result = KSandbox::isSnap() + ? u"snap.%1."_q.arg(qEnvironmentVariable("SNAP_INSTANCE_NAME")) + : QGuiApplication::desktopFileName().remove( + u"._"_q + Core::Launcher::Instance().instanceHash()); + return Result; +} + namespace ThirdParty { void start() { diff --git a/Telegram/SourceFiles/platform/linux/tray_linux.cpp b/Telegram/SourceFiles/platform/linux/tray_linux.cpp index 2cc92f331a..37ef31e259 100644 --- a/Telegram/SourceFiles/platform/linux/tray_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/tray_linux.cpp @@ -30,11 +30,11 @@ namespace { using namespace gi::repository; [[nodiscard]] QString PanelIconName(int counter, bool muted) { - return (counter > 0) + return ApplicationIconName() + ((counter > 0) ? (muted - ? u"telegram-mute-panel"_q - : u"telegram-attention-panel"_q) - : u"telegram-panel"_q; + ? u"-mute"_q + : u"-attention"_q) + : QString()) + u"-symbolic"_q; } } // namespace @@ -88,7 +88,7 @@ QIcon IconGraphic::systemIcon() const { const auto candidates = { _new.monochrome ? PanelIconName(_new.counter, _new.muted) : QString(), - base::IconName(), + ApplicationIconName(), }; for (const auto &candidate : candidates) { @@ -279,10 +279,10 @@ void Tray::createIcon() { _iconGraphic = std::make_unique(); } - const auto showXEmbed = [=] { + const auto showCustom = [=] { _aboutToShowRequests.fire({}); - InvokeQueued(_menuXEmbed.get(), [=] { - _menuXEmbed->popup(QCursor::pos()); + InvokeQueued(_menuCustom.get(), [=] { + _menuCustom->popup(QCursor::pos()); }); }; @@ -296,7 +296,7 @@ void Tray::createIcon() { &QSystemTrayIcon::activated ) | rpl::start_with_next([=](Reason reason) { if (reason == QSystemTrayIcon::Context) { - showXEmbed(); + showCustom(); } else { _iconClicks.fire({}); } @@ -309,7 +309,7 @@ void Tray::createIcon() { QCoreApplication::instance()); _eventFilter->contextMenuFilters( ) | rpl::start_with_next([=] { - showXEmbed(); + showCustom(); }, _lifetime); } } @@ -337,14 +337,14 @@ void Tray::createMenu() { if (!_menu) { _menu = base::make_unique_q(nullptr); } - if (!_menuXEmbed) { - _menuXEmbed = base::make_unique_q(nullptr); - _menuXEmbed->deleteOnHide(false); + if (!_menuCustom) { + _menuCustom = base::make_unique_q(nullptr); + _menuCustom->deleteOnHide(false); } } void Tray::destroyMenu() { - _menuXEmbed = nullptr; + _menuCustom = nullptr; if (_menu) { _menu->clear(); } @@ -352,12 +352,12 @@ void Tray::destroyMenu() { } void Tray::addAction(rpl::producer text, Fn &&callback) { - if (_menuXEmbed) { - const auto XEAction = _menuXEmbed->addAction(QString(), callback); + if (_menuCustom) { + const auto action = _menuCustom->addAction(QString(), callback); rpl::duplicate( text ) | rpl::start_with_next([=](const QString &text) { - XEAction->setText(text); + action->setText(text); }, _actionsLifetime); } diff --git a/Telegram/SourceFiles/platform/linux/tray_linux.h b/Telegram/SourceFiles/platform/linux/tray_linux.h index 7125927770..181de22b98 100644 --- a/Telegram/SourceFiles/platform/linux/tray_linux.h +++ b/Telegram/SourceFiles/platform/linux/tray_linux.h @@ -60,7 +60,7 @@ private: base::unique_qptr _icon; base::unique_qptr _menu; - base::unique_qptr _menuXEmbed; + base::unique_qptr _menuCustom; base::unique_qptr _eventFilter; diff --git a/Telegram/SourceFiles/platform/mac/specific_mac.h b/Telegram/SourceFiles/platform/mac/specific_mac.h index 55d29ca4a0..6c95b507b4 100644 --- a/Telegram/SourceFiles/platform/mac/specific_mac.h +++ b/Telegram/SourceFiles/platform/mac/specific_mac.h @@ -40,6 +40,10 @@ inline uint64 ActivationWindowId(not_null window) { inline void ActivateOtherProcess(uint64 processId, uint64 windowId) { } +inline QString ApplicationIconName() { + return {}; +} + inline QString ExecutablePathForShortcuts() { return cExeDir() + cExeName(); } diff --git a/Telegram/SourceFiles/platform/platform_specific.h b/Telegram/SourceFiles/platform/platform_specific.h index 0e423c2292..55561ef2fe 100644 --- a/Telegram/SourceFiles/platform/platform_specific.h +++ b/Telegram/SourceFiles/platform/platform_specific.h @@ -47,6 +47,7 @@ void AutostartToggle(bool enabled, Fn done = nullptr); void WriteCrashDumpDetails(); void NewVersionLaunched(int oldVersion); [[nodiscard]] QImage DefaultApplicationIcon(); +[[nodiscard]] QString ApplicationIconName(); [[nodiscard]] bool PreventsQuit(Core::QuitReason reason); [[nodiscard]] QString ExecutablePathForShortcuts(); diff --git a/Telegram/SourceFiles/platform/win/specific_win.h b/Telegram/SourceFiles/platform/win/specific_win.h index 99ebc36ad3..12a2f4cb5a 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.h +++ b/Telegram/SourceFiles/platform/win/specific_win.h @@ -43,6 +43,10 @@ void SetWindowPriority(not_null window, uint32 priority); // Activate window with windowId (if found) or the largest priority. void ActivateOtherProcess(uint64 processId, uint64 windowId); +inline QString ApplicationIconName() { + return {}; +} + inline QString ExecutablePathForShortcuts() { return cExeDir() + cExeName(); } diff --git a/Telegram/SourceFiles/platform/win/windows_app_user_model_id.cpp b/Telegram/SourceFiles/platform/win/windows_app_user_model_id.cpp index 7faeab8e2d..59b1f01b97 100644 --- a/Telegram/SourceFiles/platform/win/windows_app_user_model_id.cpp +++ b/Telegram/SourceFiles/platform/win/windows_app_user_model_id.cpp @@ -493,14 +493,7 @@ const std::wstring &Id() { return BaseId; } static const auto PortableId = [] { - std::string h(32, 0); - if (Core::Launcher::Instance().customWorkingDir()) { - const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath()); - hashMd5Hex(d.constData(), d.size(), h.data()); - } else { - const auto exePath = QFile::encodeName(cExeDir() + cExeName()); - hashMd5Hex(exePath.constData(), exePath.size(), h.data()); - } + const auto h = Core::Launcher::Instance().instanceHash(); return BaseId + L'.' + std::wstring(h.begin(), h.end()); }(); return PortableId; diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index e85df4a996..02362ec772 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/business/settings_chatbots.h" #include "apiwrap.h" +#include "boxes/peers/edit_peer_permissions_box.h" #include "boxes/peers/prepare_short_info_box.h" #include "boxes/peer_list_box.h" #include "core/application.h" @@ -47,6 +48,25 @@ struct BotState { LookupState state = LookupState::Empty; }; +[[nodiscard]] constexpr Data::ChatbotsPermissions Defaults() { + using Flag = Data::ChatbotsPermission; + return Flag::ViewMessages + | Flag::ReplyToMessages + | Flag::MarkAsRead + | Flag::DeleteSent + | Flag::DeleteReceived + | Flag::EditName + | Flag::EditBio + | Flag::EditUserpic + | Flag::EditUsername + | Flag::ViewGifts + | Flag::SellGifts + | Flag::GiftSettings + | Flag::TransferGifts + | Flag::TransferStars + | Flag::ManageStories; +} + class Chatbots final : public BusinessSection { public: Chatbots( @@ -70,7 +90,8 @@ private: rpl::variable _recipients; rpl::variable _usernameValue; rpl::variable _botValue; - rpl::variable _repliesAllowed = true; + rpl::variable _permissions = Defaults(); + Fn _resolvePermissions; }; @@ -410,7 +431,7 @@ void Chatbots::setupContent( const auto current = controller->session().data().chatbots().current(); _recipients = Data::BusinessRecipients::MakeValid(current.recipients); - _repliesAllowed = current.repliesAllowed; + _permissions = current.permissions; AddDividerTextWithLottie(content, { .lottie = u"robot"_q, @@ -472,21 +493,14 @@ void Chatbots::setupContent( Ui::AddSkip(content); Ui::AddSubsectionTitle(content, tr::lng_chatbots_permissions_title()); - content->add(object_ptr( - content, - tr::lng_chatbots_reply(), - st::settingsButtonNoIcon - ))->toggleOn(_repliesAllowed.value())->toggledChanges( - ) | rpl::start_with_next([=](bool value) { - _repliesAllowed = value; - }, content->lifetime()); - Ui::AddSkip(content); - Ui::AddDividerText( + auto permissions = CreateEditChatbotPermissions( content, - tr::lng_chatbots_reply_about(), - st::settingsChatbotsBottomTextMargin, - RectPart::Top); + _permissions.current()); + content->add(std::move(permissions.widget)); + _resolvePermissions = permissions.value; + + Ui::AddSkip(content); Ui::ResizeFitChild(this, content); } @@ -503,7 +517,7 @@ void Chatbots::save() { controller()->session().data().chatbots().save({ .bot = _botValue.current().bot, .recipients = _recipients.current(), - .repliesAllowed = _repliesAllowed.current(), + .permissions = _resolvePermissions(), }, [=] { }, fail); } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index a64cc6e92a..3e199f2588 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -682,3 +682,10 @@ settingsChatLinkField: InputField(defaultInputField) { style: defaultTextStyle; } + +settingsQuickDialogActionsTriggerFont: font(11px); + +settingsGiftIconEmoji: IconEmoji { + icon: icon{{ "settings/mini_gift", windowFg }}; + padding: margins(1px, 2px, 1px, 0px); +} diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index b1bee49426..a59c30b63f 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -148,7 +148,6 @@ using Order = std::vector; tr::lng_business_subtitle_chat_intro(), tr::lng_business_about_chat_intro(), PremiumFeature::ChatIntro, - true, }, }, { @@ -158,7 +157,6 @@ using Order = std::vector; tr::lng_business_subtitle_chat_links(), tr::lng_business_about_chat_links(), PremiumFeature::ChatLinks, - true, }, }, { @@ -168,7 +166,6 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_filter_tags(), tr::lng_premium_summary_about_filter_tags(), PremiumFeature::FilterTags, - true, }, }, }; diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index a6005c1209..6ff71fcb95 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -23,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/background_preview_box.h" #include "boxes/download_path_box.h" #include "boxes/local_storage_box.h" +#include "dialogs/ui/dialogs_quick_action_context.h" +#include "dialogs/dialogs_quick_action.h" #include "ui/boxes/choose_font_box.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" @@ -39,11 +41,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/image/image.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/vertical_list.h" #include "ui/ui_utility.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "history/view/history_view_quick_action.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" #include "export/export_manager.h" #include "window/themes/window_theme.h" #include "window/themes/window_themes_embedded.h" @@ -76,6 +80,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_window.h" +#include "styles/style_dialogs.h" // AyuGram includes #include "ayu/ui/settings/settings_ayu.h" @@ -712,7 +717,6 @@ void ChooseFromFile( void SetupStickersEmoji( not_null controller, not_null container) { - Ui::AddDivider(container); Ui::AddSkip(container); Ui::AddSubsectionTitle(container, tr::lng_settings_stickers_emoji()); @@ -1279,6 +1283,234 @@ void SetupChatBackground( }, adaptive->lifetime()); } +void SetupChatListQuickAction( + not_null controller, + not_null container) { + Ui::AddDivider(container); + Ui::AddSkip(container); + Ui::AddSubsectionTitle( + container, + tr::lng_settings_quick_dialog_action_title()); + + using Type = Dialogs::Ui::QuickDialogAction; + using LabelType = Dialogs::Ui::QuickDialogActionLabel; + const auto group = std::make_shared>( + Core::App().settings().quickDialogAction()); + group->setChangedCallback([=](Type value) { + Core::App().settings().setQuickDialogAction(value); + Core::App().saveSettings(); + }); + + const auto actionToLabel = [](Type value) { + switch (value) { + case Type::Mute: return LabelType::Mute; + case Type::Pin: return LabelType::Pin; + case Type::Read: return LabelType::Read; + case Type::Archive: return LabelType::Archive; + case Type::Delete: return LabelType::Delete; + default: return LabelType::Disabled; + } + }; + static constexpr auto kDisabledIconRatio = 1.25; + + const auto addPreview = [=](not_null container) { + const auto widget = container->add( + object_ptr(container)); + widget->resize(0, st::dialogsRowHeight); + struct State { + std::unique_ptr icon; + }; + const auto state = widget->lifetime().make_state(); + group->value() | rpl::start_with_next([=](Type value) { + const auto label = actionToLabel(value); + state->icon = Lottie::MakeIcon({ + .name = Dialogs::ResolveQuickDialogLottieIconName(label), + .sizeOverride = Size((label == LabelType::Disabled) + ? int(st::dialogsQuickActionSize * kDisabledIconRatio) + : st::dialogsQuickActionSize), + }); + state->icon->animate( + [=] { widget->update(); }, + 0, + state->icon->framesCount() - 1); + widget->update(); + }, widget->lifetime()); + widget->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(widget); + + const auto height = st::dialogsRowHeight; + const auto actionWidth = st::dialogsQuickActionRippleSize * 0.75; + const auto rightOffset = st::dialogsQuickActionRippleSize + + st::dialogsQuickActionSize; + const auto rect = QRect( + widget->width() + - actionWidth + - st::boxRowPadding.right() + - rightOffset, + 0, + actionWidth, + height); + + auto path = QPainterPath(); + path.addRoundedRect( + QRect( + -actionWidth, + 0, + rect::right(rect) + actionWidth, + height), + st::roundRadiusLarge, + st::roundRadiusLarge); + p.setClipPath(path); + + const auto label = actionToLabel(group->current()); + const auto isDisabled = (label == LabelType::Disabled); + + auto hq = PainterHighQualityEnabler(p); + p.fillRect( + QRect(0, 0, rect::right(rect), st::lineWidth), + st::windowBgOver); + p.fillRect( + QRect( + 0, + rect::bottom(rect) - st::lineWidth, + rect::right(rect), + st::lineWidth), + st::windowBgOver); + p.fillRect(rect, Dialogs::ResolveQuickActionBg(label)); + if (state->icon) { + Dialogs::DrawQuickAction( + p, + rect, + state->icon.get(), + label, + isDisabled ? kDisabledIconRatio : 1., + isDisabled); + } + p.translate(-height / 2, 0); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgOver); + p.drawEllipse(Rect(Size(height)) - Margins(height / 6)); + + const auto h = st::normalFont->ascent / 1.5; + p.drawRoundedRect( + height, + height / 2 - h * 1.5, + st::dialogsQuickActionRippleSize * 0.6, + h, + h / 2, + h / 2); + p.drawRoundedRect( + height, + height / 2 + h, + st::dialogsQuickActionRippleSize * 1.0, + h, + h / 2, + h / 2); + + p.setClipping(false); + p.resetTransform(); + p.setFont(st::settingsQuickDialogActionsTriggerFont); + p.setPen(st::windowSubTextFg); + p.drawText( + QRect( + widget->width() + - st::dialogsQuickActionRippleSize + - st::boxRowPadding.right(), + 0, + st::dialogsQuickActionRippleSize, + height), + isDisabled + ? tr::lng_settings_quick_dialog_action_swipe(tr::now) + : tr::lng_settings_quick_dialog_action_both(tr::now), + style::al_center); + }, widget->lifetime()); + }; + + const auto &st = st::settingsButton; + const auto button = container->add( + object_ptr( + container, + group->value() | rpl::map([](Type value) { + return ((value == Dialogs::Ui::QuickDialogAction::Mute) + ? tr::lng_settings_quick_dialog_action_mute + : (value == Dialogs::Ui::QuickDialogAction::Pin) + ? tr::lng_settings_quick_dialog_action_pin + : (value == Dialogs::Ui::QuickDialogAction::Read) + ? tr::lng_settings_quick_dialog_action_read + : (value == Dialogs::Ui::QuickDialogAction::Archive) + ? tr::lng_settings_quick_dialog_action_archive + : tr::lng_settings_quick_dialog_action_disabled)(); + }) | rpl::flatten_latest(), + st)); + + { + const auto icon = button->lifetime().make_state(button); + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + icon->resize(st::menuIconArchive.size()); + icon->show(); + button->sizeValue( + ) | rpl::start_with_next([=, left = st.iconLeft](QSize size) { + icon->moveToLeft( + left, + (size.height() - icon->height()) / 2, + size.width()); + }, icon->lifetime()); + icon->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(icon); + const auto value = group->current(); + ((value == Dialogs::Ui::QuickDialogAction::Mute) + ? st::menuIconMute + : (value == Dialogs::Ui::QuickDialogAction::Pin) + ? st::menuIconPin + : (value == Dialogs::Ui::QuickDialogAction::Read) + ? st::menuIconMarkRead + : (value == Dialogs::Ui::QuickDialogAction::Delete) + ? st::menuIconDelete + : (value == Dialogs::Ui::QuickDialogAction::Archive) + ? st::menuIconArchive + : st::menuIconShowInFolder).paintInCenter(p, icon->rect()); + }, icon->lifetime()); + } + + button->setClickedCallback([=] { + controller->uiShow()->showBox(Box([=](not_null box) { + box->setTitle(tr::lng_settings_quick_dialog_action_title()); + const auto addRadio = [&](Type value, tr::phrase<> phrase) { + box->verticalLayout()->add( + object_ptr>( + box->verticalLayout(), + group, + value, + phrase(tr::now), + st::settingsSendType), + st::settingsSendTypePadding); + }; + addPreview(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + addRadio(Type::Mute, tr::lng_settings_quick_dialog_action_mute); + addRadio(Type::Pin, tr::lng_settings_quick_dialog_action_pin); + addRadio(Type::Read, tr::lng_settings_quick_dialog_action_read); + addRadio( + Type::Archive, + tr::lng_settings_quick_dialog_action_archive); + addRadio( + Type::Delete, + tr::lng_settings_quick_dialog_action_delete); + addRadio( + Type::Disabled, + tr::lng_settings_quick_dialog_action_disabled); + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); + })); + }); + Ui::AddSkip(container); + Ui::AddDividerText( + container, + tr::lng_settings_quick_dialog_action_about()); + Ui::AddSkip(container); +} + void SetupDefaultThemes( not_null window, not_null container) { @@ -1881,6 +2113,7 @@ void Chat::setupContent(not_null controller) { SetupThemeSettings(controller, content); SetupCloudThemes(controller, content); SetupChatBackground(controller, content); + SetupChatListQuickAction(controller, content); SetupStickersEmoji(controller, content); SetupMessages(controller, content); Ui::AddDivider(content); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 4056c3bfb6..9aee9306f5 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/bot/starref/info_bot_starref_common.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "info/channel_statistics/earn/info_channel_earn_widget.h" // Info::ChannelEarn::Make. +#include "info/peer_gifts/info_peer_gifts_common.h" #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "info/info_controller.h" @@ -173,18 +174,6 @@ private: }; -[[nodiscard]] Data::SavedStarGiftId EntryToSavedStarGiftId( - not_null session, - const Data::CreditsHistoryEntry &entry) { - return !entry.stargift - ? Data::SavedStarGiftId() - : (entry.bareEntryOwnerId && entry.giftChannelSavedId) - ? Data::SavedStarGiftId::Chat( - session->data().peer(PeerId(entry.bareEntryOwnerId)), - entry.giftChannelSavedId) - : Data::SavedStarGiftId::User(MsgId(entry.bareMsgId)); -} - void ToggleStarGiftSaved( std::shared_ptr show, Data::SavedStarGiftId savedId, @@ -226,7 +215,8 @@ void ToggleStarGiftPinned( Data::SavedStarGiftId savedId, std::vector already, bool pinned, - Fn done = nullptr) { + std::shared_ptr uniqueData = nullptr, + std::shared_ptr replacingData = nullptr) { already.erase(ranges::remove(already, savedId), end(already)); if (pinned) { already.insert(begin(already), savedId); @@ -256,16 +246,29 @@ void ToggleStarGiftPinned( .action = (pinned ? GiftAction::Pin : GiftAction::Unpin), }); - if (const auto onstack = done) { - onstack(true); - } if (pinned) { - show->showToast(tr::lng_gift_pinned_done(tr::now)); + show->showToast({ + .title = (uniqueData + ? tr::lng_gift_pinned_done_title( + tr::now, + lt_gift, + Data::UniqueGiftName(*uniqueData)) + : QString()), + .text = (replacingData + ? tr::lng_gift_pinned_done_replaced( + tr::now, + lt_gift, + TextWithEntities{ + Data::UniqueGiftName(*replacingData), + }, + Ui::Text::WithEntities) + : tr::lng_gift_pinned_done( + tr::now, + Ui::Text::WithEntities)), + .duration = Ui::Toast::kDefaultDuration * 2, + }); } }).fail([=](const MTP::Error &error) { - if (const auto onstack = done) { - onstack(false); - } show->showToast(error.type()); }).send(); } @@ -920,25 +923,61 @@ void FillUniqueGiftMenu( if (unique && canToggle && e.savedToProfile - && type == SavedStarGiftMenuType::List) { - const auto already = [session, entries = e.pinnedSavedGifts] { - Expects(entries != nullptr); - - auto list = entries(); + && e.pinnedSavedGifts) { + const auto pinned = e.pinnedSavedGifts; + const auto ids = [session]( + const std::vector &pinned) { auto result = std::vector(); - result.reserve(list.size()); - for (const auto &entry : list) { + result.reserve(pinned.size()); + for (const auto &entry : pinned) { result.push_back(EntryToSavedStarGiftId(session, entry)); } return result; }; if (e.giftPinned) { menu->addAction(tr::lng_context_unpin_from_top(tr::now), [=] { - ToggleStarGiftPinned(show, savedId, already(), false); + ToggleStarGiftPinned(show, savedId, ids(pinned()), false); }, st.unpin ? st.unpin : &st::menuIconUnpin); } else { menu->addAction(tr::lng_context_pin_to_top(tr::now), [=] { - ToggleStarGiftPinned(show, savedId, already(), true); + const auto list = pinned(); + const auto &appConfig = show->session().appConfig(); + const auto limit = appConfig.pinnedGiftsLimit(); + auto already = ids(list); + if (list.size() >= limit) { + Info::PeerGifts::SelectGiftToUnpin(show, list, [=]( + Data::SavedStarGiftId id) { + auto copy = already; + const auto i = ranges::find(copy, id); + const auto replaced = (i != end(copy)) + ? list[i - begin(copy)].uniqueGift + : nullptr; + if (i != end(copy)) { + copy.erase(i); + } + + using GiftAction = Data::GiftUpdate::Action; + show->session().data().notifyGiftUpdate({ + .id = id, + .action = GiftAction::Unpin, + }); + + ToggleStarGiftPinned( + show, + savedId, + already, + true, + unique, + replaced); + }); + } else { + ToggleStarGiftPinned( + show, + savedId, + already, + true, + unique); + } }, st.pin ? st.pin : &st::menuIconPin); } } @@ -2031,15 +2070,30 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( }; } +Data::SavedStarGiftId EntryToSavedStarGiftId( + not_null session, + const Data::CreditsHistoryEntry &entry) { + return !entry.stargift + ? Data::SavedStarGiftId() + : (entry.bareEntryOwnerId && entry.giftChannelSavedId) + ? Data::SavedStarGiftId::Chat( + session->data().peer(PeerId(entry.bareEntryOwnerId)), + entry.giftChannelSavedId) + : Data::SavedStarGiftId::User(MsgId(entry.bareMsgId)); +} + void SavedStarGiftBox( not_null box, not_null controller, not_null owner, - const Data::SavedStarGift &data) { + const Data::SavedStarGift &data, + Fn()> pinned) { + auto entry = SavedStarGiftEntry(owner, data); + entry.pinnedSavedGifts = std::move(pinned); Settings::ReceiptCreditsBox( box, controller, - SavedStarGiftEntry(owner, data), + std::move(entry), Data::SubscriptionEntry()); } diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 8a1ee55bcd..aa43eff741 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -24,6 +24,7 @@ struct SubscriptionEntry; struct GiftCode; struct CreditTopupOption; struct SavedStarGift; +class SavedStarGiftId; struct StarGift; } // namespace Data @@ -158,11 +159,15 @@ void GlobalStarGiftBox( [[nodiscard]] Data::CreditsHistoryEntry SavedStarGiftEntry( not_null owner, const Data::SavedStarGift &data); +[[nodiscard]] Data::SavedStarGiftId EntryToSavedStarGiftId( + not_null session, + const Data::CreditsHistoryEntry &entry); void SavedStarGiftBox( not_null box, not_null controller, not_null owner, - const Data::SavedStarGift &data); + const Data::SavedStarGift &data, + Fn()> pinned = nullptr); enum class SavedStarGiftMenuType { List, View, diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index 65f5356a84..93d989cb77 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -453,12 +453,18 @@ void SetupRows( Ui::AddSkip(container); + const auto showEditName = [=] { + if (controller->showFrozenError()) { + return; + } + controller->show(Box(self)); + }; AddRow( container, tr::lng_settings_name_label(), Info::Profile::NameValue(self) | Ui::Text::ToWithEntities(), tr::lng_profile_copy_fullname(tr::now), - [=] { controller->show(Box(self)); }, + showEditName, { &st::menuIconProfile }); const auto copyPhone = [=] { @@ -507,6 +513,9 @@ void SetupRows( std::move(value), tr::lng_context_copy_mention(tr::now), [=] { + if (controller->showFrozenError()) { + return; + } const auto box = controller->show( Box(UsernamesBox, session->user())); box->boxClosing( diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 266d6a6788..7ab065c2a3 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -248,6 +248,9 @@ void Cover::initViewers() { }, lifetime()); _username->overrideLinkClickHandler([=] { + if (_controller->showFrozenError()) { + return; + } const auto username = _user->username(); if (username.isEmpty()) { _controller->show(Box(UsernamesBox, _user)); diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index a843d7f046..c64c2c5ccc 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -616,8 +616,8 @@ object_ptr PhoneNumberPrivacyController::setupMiddleWidget( } void PhoneNumberPrivacyController::saveAdditional() { - if (_saveAdditional) { - _saveAdditional(); + if (const auto onstack = _saveAdditional) { + onstack(); } } @@ -1296,8 +1296,8 @@ object_ptr ProfilePhotoPrivacyController::setupMiddleWidget( } void ProfilePhotoPrivacyController::saveAdditional() { - if (_saveAdditional) { - _saveAdditional(); + if (const auto onstack = _saveAdditional) { + onstack(); } } @@ -1581,6 +1581,13 @@ object_ptr BirthdayPrivacyController::setupAboveWidget( return result; } +struct GiftsAutoSavePrivacyController::AdditionalState { + Api::DisallowedGiftTypes disallowed; + rpl::event_stream<> disables; + Fn promo; + Fn save; +}; + UserPrivacy::Key GiftsAutoSavePrivacyController::key() const { return Key::GiftsAutoSave; } @@ -1626,4 +1633,145 @@ bool GiftsAutoSavePrivacyController::allowMiniAppsToggle( return true; } +void GiftsAutoSavePrivacyController::ensureAdditionalState( + not_null controller, + rpl::lifetime &on) { + if (_state) { + return; + } + const auto session = &controller->session(); + const auto globalPrivacy = &session->api().globalPrivacy(); + + _state = on.make_state(); + _state->disallowed = globalPrivacy->disallowedGiftTypesCurrent(); + _state->promo = [=] { + _state->disables.fire({}); + const auto link = Ui::Text::Bold( + tr::lng_settings_generic_subscribe_link(tr::now)); + Settings::ShowPremiumPromoToast( + controller->uiShow(), + tr::lng_settings_generic_subscribe( + tr::now, + lt_link, + Ui::Text::Link(link), + Ui::Text::WithEntities), + u"gifts_privacy"_q); + }; + _state->save = [=] { + const auto now = _state->disallowed; + if (!session->premium()) { + return; + } else if (globalPrivacy->disallowedGiftTypesCurrent() == now) { + return; + } else { + globalPrivacy->updateDisallowedGiftTypes(now); + } + }; +} + +object_ptr GiftsAutoSavePrivacyController::setupAboveWidget( + not_null controller, + not_null parent, + rpl::producer