Compare commits
222 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87895466dc | ||
|
|
72f8d3f485 | ||
|
|
5092d8fe63 | ||
|
|
01110a29ad | ||
|
|
e8c3df2abb | ||
|
|
db89de96a9 | ||
|
|
1eacaec66b | ||
|
|
10f58c2ac7 | ||
|
|
e0680fc2a5 | ||
|
|
fba7bd7807 | ||
|
|
6c0553f4d6 | ||
|
|
5d4e5ed527 | ||
|
|
2559c5d6f4 | ||
|
|
8d85dd7c19 | ||
|
|
8f0e23bb25 | ||
|
|
fc4ed2ff91 | ||
|
|
fcdc39c5f9 | ||
|
|
1ec6b4313d | ||
|
|
9be65f8812 | ||
|
|
30468746ad | ||
|
|
7947af665c | ||
|
|
160cd975ce | ||
|
|
c13d0e96ef | ||
|
|
fbf4f912c6 | ||
|
|
9a0023cc99 | ||
|
|
0aea9bc46f | ||
|
|
d2662ba1fd | ||
|
|
0f17a3b300 | ||
|
|
7c031a4fb6 | ||
|
|
834ee4eae7 | ||
|
|
9b59e74d66 | ||
|
|
ec8ddb047d | ||
|
|
037506c0b7 | ||
|
|
85b3672bc8 | ||
|
|
bf61f624c5 | ||
|
|
1baa833e8f | ||
|
|
bad2d8afd9 | ||
|
|
6d5bf53dd1 | ||
|
|
3001ad4b89 | ||
|
|
5a88b4f0b9 | ||
|
|
71ee981371 | ||
|
|
e9864bcf5b | ||
|
|
dd401a063b | ||
|
|
61f6851486 | ||
|
|
7da224d725 | ||
|
|
ce5c19dfe9 | ||
|
|
52000566cf | ||
|
|
fa8dd61b02 | ||
|
|
0d0a79b0b5 | ||
|
|
b22c65a8db | ||
|
|
416489a84f | ||
|
|
7424e6afcc | ||
|
|
5d1f55e29d | ||
|
|
130b8bc83c | ||
|
|
ef1a4e4ce3 | ||
|
|
9fed46fb6e | ||
|
|
50f87cce84 | ||
|
|
833ffe1784 | ||
|
|
1d3e76e1fe | ||
|
|
1cbb217210 | ||
|
|
70cdc05544 | ||
|
|
173564bcd5 | ||
|
|
e12689c8c1 | ||
|
|
e1f5e10764 | ||
|
|
250add3a96 | ||
|
|
827c950468 | ||
|
|
36ad24bfcd | ||
|
|
7410c1fc73 | ||
|
|
b2c84d675c | ||
|
|
ff9bf23461 | ||
|
|
417428b21d | ||
|
|
58733ba6ea | ||
|
|
1774b21e88 | ||
|
|
19455d44db | ||
|
|
34f7391ec9 | ||
|
|
274779c1c8 | ||
|
|
50c07bfc98 | ||
|
|
819cd4a099 | ||
|
|
144bad6c74 | ||
|
|
97fb310f54 | ||
|
|
1cce383d15 | ||
|
|
01ecf0ca93 | ||
|
|
40e90af76d | ||
|
|
7fa342b487 | ||
|
|
3862b3b90e | ||
|
|
5e10d97abe | ||
|
|
542abb26b9 | ||
|
|
7132ab5bf4 | ||
|
|
c7b1a37722 | ||
|
|
be1afb4781 | ||
|
|
8c7030378a | ||
|
|
7e89ed48c2 | ||
|
|
754dedc40e | ||
|
|
e5320b4b4e | ||
|
|
02ad5f2772 | ||
|
|
b58a977029 | ||
|
|
40fda9503f | ||
|
|
f63f0a7668 | ||
|
|
b396244606 | ||
|
|
b562a4a479 | ||
|
|
82d78e7c45 | ||
|
|
df0bca077e | ||
|
|
efde011f1c | ||
|
|
9b9531d279 | ||
|
|
d4bbbdb65c | ||
|
|
c90258664d | ||
|
|
dd01ece14a | ||
|
|
4895e5e110 | ||
|
|
c21125f9f2 | ||
|
|
ae74f7b3da | ||
|
|
8ed56bb4e4 | ||
|
|
3793f7c3c9 | ||
|
|
0d1b778612 | ||
|
|
b919a0627a | ||
|
|
6374d4eeda | ||
|
|
3967052375 | ||
|
|
89ccc95023 | ||
|
|
24f2ca7443 | ||
|
|
f90e13f8b1 | ||
|
|
606f5377d5 | ||
|
|
c698327b24 | ||
|
|
655731741c | ||
|
|
d5cdb5582b | ||
|
|
5cb081ca9a | ||
|
|
f1e0b36f61 | ||
|
|
ea9813825d | ||
|
|
36b6f70613 | ||
|
|
5e60b87cf9 | ||
|
|
ada22ee6cc | ||
|
|
bb016e1489 | ||
|
|
b115ea74d0 | ||
|
|
1008774aef | ||
|
|
73018ff958 | ||
|
|
e799fdaa3d | ||
|
|
7656a546b0 | ||
|
|
57f9ae4b2a | ||
|
|
cbdd86d398 | ||
|
|
2fe2105a5f | ||
|
|
a986d7a3d6 | ||
|
|
690c5df87c | ||
|
|
1e2759840d | ||
|
|
bad888496c | ||
|
|
4348ddf938 | ||
|
|
894d6028bd | ||
|
|
e8edbb16ae | ||
|
|
a0a71687e7 | ||
|
|
d042963a47 | ||
|
|
64b12bde55 | ||
|
|
49736cd879 | ||
|
|
e55581e0c9 | ||
|
|
574d915c23 | ||
|
|
2616659116 | ||
|
|
3d1f21bd05 | ||
|
|
dc631ef631 | ||
|
|
5277080115 | ||
|
|
1ccfcc824c | ||
|
|
97e8c0956f | ||
|
|
03a7131a1a | ||
|
|
2d906bddb2 | ||
|
|
dd7598a701 | ||
|
|
b6f17e1cea | ||
|
|
eb42a77eb7 | ||
|
|
ad761011d6 | ||
|
|
3fadf2ee54 | ||
|
|
15254599e2 | ||
|
|
1607752cf9 | ||
|
|
cf0cde6e83 | ||
|
|
8fffe7d128 | ||
|
|
a483eb98a1 | ||
|
|
838a3b23c7 | ||
|
|
a030911ad5 | ||
|
|
8ae1b10b91 | ||
|
|
adc8d6a6d1 | ||
|
|
1704cb345a | ||
|
|
0b85d0f185 | ||
|
|
348dfefbaa | ||
|
|
7508980f62 | ||
|
|
e11efe483e | ||
|
|
b23e4fa491 | ||
|
|
b6b7f5706f | ||
|
|
613bf98283 | ||
|
|
0bdb38753b | ||
|
|
d557e0f2b7 | ||
|
|
e81f4e8545 | ||
|
|
3b7d5d3c80 | ||
|
|
0c37990ccd | ||
|
|
0fbea454bc | ||
|
|
d4d688d494 | ||
|
|
b3892f49fa | ||
|
|
daa3a2f62f | ||
|
|
5affb168a2 | ||
|
|
99af2a7058 | ||
|
|
b9acea9cef | ||
|
|
8fb6ece796 | ||
|
|
15a9842b9f | ||
|
|
b28da30038 | ||
|
|
8ce0bd5575 | ||
|
|
5d68d224e5 | ||
|
|
373635a765 | ||
|
|
0ecd4d3b40 | ||
|
|
3fd62d51aa | ||
|
|
818624e051 | ||
|
|
a6eb241ec1 | ||
|
|
1d7fb6c4ce | ||
|
|
2af63ec48f | ||
|
|
0709bc6d70 | ||
|
|
3a34881488 | ||
|
|
39f9147790 | ||
|
|
19a5dcbffc | ||
|
|
e864aa2ff2 | ||
|
|
fe85a8256a | ||
|
|
f24b0c6237 | ||
|
|
3940d57c3d | ||
|
|
8da33113a2 | ||
|
|
f66cfb5684 | ||
|
|
d648d294ca | ||
|
|
0c8033414e | ||
|
|
28f29b51c0 | ||
|
|
8142e83395 | ||
|
|
e247be7e33 | ||
|
|
e594b75f4c | ||
|
|
28f857f763 |
21
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 45
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: false
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: true
|
||||
11
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Number of days of inactivity before an Issue is closed for lack of response
|
||||
daysUntilClose: 30
|
||||
# Label requiring a response
|
||||
responseRequiredLabel: waiting for answer
|
||||
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there has been no response
|
||||
to our request for more information from the original author. With only the
|
||||
information that is currently in the issue, we don't have enough information
|
||||
to take action. Please reach out if you have or find the answers we need so
|
||||
that we can investigate further.
|
||||
16
.github/workflows/copyright_year_updater.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Copyright year updater.
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: ["Restart copyright_year_updater workflow."]
|
||||
schedule:
|
||||
# At 03:00 on January 1.
|
||||
- cron: "0 3 1 1 *"
|
||||
|
||||
jobs:
|
||||
Copyright-year:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: desktop-app/action_code_updater@master
|
||||
with:
|
||||
type: "license-year"
|
||||
4
.github/workflows/mac.yml
vendored
@@ -102,6 +102,8 @@ jobs:
|
||||
cd Libraries/macos
|
||||
echo "LibrariesPath=`pwd`" >> $GITHUB_ENV
|
||||
|
||||
curl -o tg_owt-version.json https://api.github.com/repos/desktop-app/tg_owt/git/refs/heads/master
|
||||
|
||||
- name: Patches.
|
||||
run: |
|
||||
echo "Find necessary commit from doc."
|
||||
@@ -475,7 +477,7 @@ jobs:
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/tg_owt
|
||||
key: ${{ runner.OS }}-webrtc-${{ env.CACHE_KEY }}
|
||||
key: ${{ runner.OS }}-webrtc-${{ env.CACHE_KEY }}-${{ hashFiles('**/tg_owt-version.json') }}
|
||||
- name: WebRTC.
|
||||
if: steps.cache-webrtc.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
|
||||
144
.github/workflows/user_agent_updater.yml
vendored
@@ -5,152 +5,14 @@ on:
|
||||
types: ["Restart user_agent_updater workflow."]
|
||||
schedule:
|
||||
# At 00:00 on day-of-month 1.
|
||||
- cron: '0 0 1 * *'
|
||||
- cron: "0 0 1 * *"
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
User-agent:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
codeFile: "Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp"
|
||||
headBranchPrefix: "chrome_"
|
||||
baseBranch: "dev"
|
||||
isPull: "0"
|
||||
|
||||
steps:
|
||||
- name: Set env.
|
||||
if: startsWith(github.event_name, 'pull_request')
|
||||
run: |
|
||||
echo "isPull=1" >> $GITHUB_ENV
|
||||
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up git.
|
||||
run: |
|
||||
token=${{ secrets.TOKEN_FOR_MASTER_UPDATER }}
|
||||
if [ -z "${token}" ]; then
|
||||
echo "Token is unset. Nothing to do."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
url=https://x-access-token:$token@github.com/$GITHUB_REPOSITORY
|
||||
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
|
||||
git remote set-url origin $url
|
||||
|
||||
- name: Delete branch.
|
||||
env:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
if: |
|
||||
env.isPull == '1'
|
||||
&& github.event.action == 'closed'
|
||||
&& startsWith(env.ref, env.headBranchPrefix)
|
||||
run: |
|
||||
git push origin --delete $ref
|
||||
|
||||
- name: Write a new version of Google Chrome to the user-agent for DNS.
|
||||
if: env.isPull == '0'
|
||||
shell: python
|
||||
run: |
|
||||
import subprocess, os, re;
|
||||
|
||||
regExpVersion = "[0-9]+.[0-9]+.[0-9]+.[0-9]+";
|
||||
chrome = "Chrome/";
|
||||
|
||||
def newVersion():
|
||||
output = subprocess.check_output(["google-chrome", "--version"]);
|
||||
version = re.search(regExpVersion, output);
|
||||
if not version:
|
||||
print("Can't find a Chrome version.");
|
||||
exit();
|
||||
return version.group(0);
|
||||
|
||||
newChromeVersion = newVersion();
|
||||
print(newChromeVersion);
|
||||
|
||||
def setEnv(value):
|
||||
open(os.environ['GITHUB_ENV'], "a").write(value);
|
||||
|
||||
def writeUserAgent():
|
||||
p = os.environ['codeFile'];
|
||||
w = open(p, "r");
|
||||
content = w.read();
|
||||
w.close();
|
||||
|
||||
regExpChrome = chrome + regExpVersion;
|
||||
|
||||
version = re.search(regExpChrome, content);
|
||||
if not version:
|
||||
print("Can't find an user-agent in the code.");
|
||||
exit();
|
||||
content = re.sub(regExpChrome, chrome + newChromeVersion, content);
|
||||
|
||||
w = open(p, "w");
|
||||
w.write(content);
|
||||
|
||||
setEnv("ChromeVersion=" + newChromeVersion);
|
||||
|
||||
writeUserAgent();
|
||||
|
||||
- name: Push to a new branch.
|
||||
if: env.isPull == '0' && env.ChromeVersion != ''
|
||||
run: |
|
||||
git diff > git_diff.txt
|
||||
if [[ ! -s git_diff.txt ]]; then
|
||||
echo "Nothing to commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git checkout -b $headBranchPrefix$ChromeVersion
|
||||
git add $codeFile
|
||||
git commit -m "Update User-Agent for DNS to Chrome $ChromeVersion."
|
||||
|
||||
git push origin $headBranchPrefix$ChromeVersion
|
||||
echo "Done!"
|
||||
|
||||
- name: Close previous pull requests.
|
||||
if: env.isPull == '0' && env.ChromeVersion != ''
|
||||
uses: actions/github-script@0.4.0
|
||||
- uses: desktop-app/action_code_updater@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const common = {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
};
|
||||
|
||||
github.pulls.list(common).then(response => {
|
||||
response.data.forEach((item, _) => {
|
||||
if (item.head.ref.startsWith(process.env.headBranchPrefix)) {
|
||||
console.log(`Close ${item.title} #${item.number}.`);
|
||||
github.pulls.update({
|
||||
pull_number: item.number,
|
||||
state: "closed",
|
||||
...common
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
- name: Create a new pull request.
|
||||
if: env.isPull == '0' && env.ChromeVersion != ''
|
||||
uses: actions/github-script@0.4.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const version = process.env.ChromeVersion;
|
||||
const title = `Update User-Agent for DNS to Chrome ${version}.`;
|
||||
|
||||
github.pulls.create({
|
||||
title: title,
|
||||
body: "",
|
||||
head: `${process.env.headBranchPrefix}${version}`,
|
||||
base: process.env.baseBranch,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
type: "user-agent"
|
||||
|
||||
3
.github/workflows/win.yml
vendored
@@ -103,6 +103,7 @@ jobs:
|
||||
- name: Generate cache key.
|
||||
shell: bash
|
||||
run: |
|
||||
curl -o $LibrariesPath/tg_owt-version.json https://api.github.com/repos/desktop-app/tg_owt/git/refs/heads/master
|
||||
echo $MANUAL_CACHING >> CACHE_KEY.txt
|
||||
if [ "$AUTO_CACHING" == "1" ]; then
|
||||
thisFile=$REPO_NAME/.github/workflows/win.yml
|
||||
@@ -359,7 +360,7 @@ jobs:
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/tg_owt
|
||||
key: ${{ runner.OS }}-webrtc-${{ env.CACHE_KEY }}
|
||||
key: ${{ runner.OS }}-webrtc-${{ env.CACHE_KEY }}-${{ hashFiles('**/tg_owt-version.json') }}
|
||||
- name: WebRTC.
|
||||
if: steps.cache-webrtc.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
|
||||
2
LEGAL
@@ -1,7 +1,7 @@
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
Copyright (c) 2014-2020 The Telegram Desktop Authors.
|
||||
Copyright (c) 2014-2021 The Telegram Desktop Authors.
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -17,13 +17,17 @@ The latest version is available for
|
||||
|
||||
* [Windows 7 and above](https://telegram.org/dl/desktop/win) ([portable](https://telegram.org/dl/desktop/win_portable))
|
||||
* [macOS 10.12 and above](https://telegram.org/dl/desktop/mac)
|
||||
* [OS X 10.10 and 10.11](https://telegram.org/dl/desktop/osx)
|
||||
* [Linux static build for 64 bit](https://telegram.org/dl/desktop/linux) ([32 bit](https://telegram.org/dl/desktop/linux32))
|
||||
* [Linux static build for 64 bit](https://telegram.org/dl/desktop/linux)
|
||||
* [Snap](https://snapcraft.io/telegram-desktop)
|
||||
* [Flatpak](https://flathub.org/apps/details/org.telegram.desktop)
|
||||
|
||||
## Old system versions
|
||||
|
||||
Version **2.4.4** was the last that supports older systems
|
||||
|
||||
* [OS X 10.10 and 10.11](https://updates.tdesktop.com/tosx/tsetup-osx.2.4.4.dmg)
|
||||
* [Linux static build for 32 bit](https://updates.tdesktop.com/tlinux32/tsetup32.2.4.4.tar.xz)
|
||||
|
||||
Version **1.8.15** was the last that supports older systems
|
||||
|
||||
* [Windows XP and Vista](https://updates.tdesktop.com/tsetup/tsetup.1.8.15.exe) ([portable](https://updates.tdesktop.com/tsetup/tportable.1.8.15.zip))
|
||||
|
||||
@@ -67,6 +67,7 @@ PRIVATE
|
||||
desktop-app::external_auto_updates
|
||||
desktop-app::external_openssl
|
||||
desktop-app::external_openal
|
||||
desktop-app::external_xxhash
|
||||
)
|
||||
|
||||
if (LINUX)
|
||||
@@ -84,6 +85,13 @@ if (LINUX)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
desktop-app::external_kwayland
|
||||
)
|
||||
endif()
|
||||
|
||||
if (DESKTOP_APP_USE_PACKAGED
|
||||
AND NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION
|
||||
AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
|
||||
@@ -138,6 +146,8 @@ PRIVATE
|
||||
api/api_global_privacy.h
|
||||
api/api_hash.cpp
|
||||
api/api_hash.h
|
||||
api/api_invite_links.cpp
|
||||
api/api_invite_links.h
|
||||
api/api_media.cpp
|
||||
api/api_media.h
|
||||
api/api_self_destruct.cpp
|
||||
@@ -172,6 +182,10 @@ PRIVATE
|
||||
boxes/peers/edit_participants_box.h
|
||||
boxes/peers/edit_peer_info_box.cpp
|
||||
boxes/peers/edit_peer_info_box.h
|
||||
boxes/peers/edit_peer_invite_link.cpp
|
||||
boxes/peers/edit_peer_invite_link.h
|
||||
boxes/peers/edit_peer_invite_links.cpp
|
||||
boxes/peers/edit_peer_invite_links.h
|
||||
boxes/peers/edit_peer_type_box.cpp
|
||||
boxes/peers/edit_peer_type_box.h
|
||||
boxes/peers/edit_peer_history_visibility_box.cpp
|
||||
@@ -192,8 +206,6 @@ PRIVATE
|
||||
boxes/background_box.h
|
||||
boxes/background_preview_box.cpp
|
||||
boxes/background_preview_box.h
|
||||
boxes/calendar_box.cpp
|
||||
boxes/calendar_box.h
|
||||
boxes/change_phone_box.cpp
|
||||
boxes/change_phone_box.h
|
||||
boxes/confirm_box.cpp
|
||||
@@ -258,6 +270,7 @@ PRIVATE
|
||||
calls/calls_call.h
|
||||
calls/calls_group_call.cpp
|
||||
calls/calls_group_call.h
|
||||
calls/calls_group_common.h
|
||||
calls/calls_group_members.cpp
|
||||
calls/calls_group_members.h
|
||||
calls/calls_group_panel.cpp
|
||||
@@ -278,6 +291,8 @@ PRIVATE
|
||||
calls/calls_userpic.h
|
||||
calls/calls_video_bubble.cpp
|
||||
calls/calls_video_bubble.h
|
||||
calls/calls_volume_item.cpp
|
||||
calls/calls_volume_item.h
|
||||
chat_helpers/bot_keyboard.cpp
|
||||
chat_helpers/bot_keyboard.h
|
||||
chat_helpers/emoji_keywords.cpp
|
||||
@@ -812,8 +827,17 @@ PRIVATE
|
||||
platform/linux/linux_desktop_environment.h
|
||||
platform/linux/linux_gdk_helper.cpp
|
||||
platform/linux/linux_gdk_helper.h
|
||||
platform/linux/linux_libs.cpp
|
||||
platform/linux/linux_libs.h
|
||||
platform/linux/linux_gsd_media_keys.cpp
|
||||
platform/linux/linux_gsd_media_keys.h
|
||||
platform/linux/linux_gtk_file_dialog.cpp
|
||||
platform/linux/linux_gtk_file_dialog.h
|
||||
platform/linux/linux_gtk_integration_p.h
|
||||
platform/linux/linux_gtk_integration.cpp
|
||||
platform/linux/linux_gtk_integration.h
|
||||
platform/linux/linux_notification_service_watcher.cpp
|
||||
platform/linux/linux_notification_service_watcher.h
|
||||
platform/linux/linux_open_with_dialog.cpp
|
||||
platform/linux/linux_open_with_dialog.h
|
||||
platform/linux/linux_wayland_integration.cpp
|
||||
platform/linux/linux_wayland_integration.h
|
||||
platform/linux/linux_xlib_helper.cpp
|
||||
@@ -882,7 +906,6 @@ PRIVATE
|
||||
platform/win/windows_dlls.h
|
||||
platform/win/windows_event_filter.cpp
|
||||
platform/win/windows_event_filter.h
|
||||
platform/win/wrapper_wrl_implements_h.h
|
||||
platform/platform_audio.h
|
||||
platform/platform_file_utilities.h
|
||||
platform/platform_launcher.h
|
||||
@@ -952,6 +975,8 @@ PRIVATE
|
||||
storage/storage_account.h
|
||||
storage/storage_cloud_blob.cpp
|
||||
storage/storage_cloud_blob.h
|
||||
storage/storage_cloud_song_cover.cpp
|
||||
storage/storage_cloud_song_cover.h
|
||||
storage/storage_domain.cpp
|
||||
storage/storage_domain.h
|
||||
storage/storage_facade.cpp
|
||||
@@ -1101,11 +1126,46 @@ if (NOT LINUX)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (LINUX AND DESKTOP_APP_DISABLE_DBUS_INTEGRATION)
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/linux/linux_gsd_media_keys.cpp
|
||||
platform/linux/linux_gsd_media_keys.h
|
||||
platform/linux/linux_notification_service_watcher.cpp
|
||||
platform/linux/linux_notification_service_watcher.h
|
||||
platform/linux/notifications_manager_linux.cpp
|
||||
)
|
||||
|
||||
nice_target_sources(Telegram ${src_loc}
|
||||
PRIVATE
|
||||
platform/linux/notifications_manager_linux_dummy.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (LINUX AND DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
remove_target_sources(Telegram ${src_loc} platform/linux/linux_wayland_integration.cpp)
|
||||
nice_target_sources(Telegram ${src_loc} PRIVATE platform/linux/linux_wayland_integration_dummy.cpp)
|
||||
endif()
|
||||
|
||||
if (LINUX AND TDESKTOP_DISABLE_GTK_INTEGRATION)
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/linux/linux_gdk_helper.cpp
|
||||
platform/linux/linux_gdk_helper.h
|
||||
platform/linux/linux_gtk_file_dialog.cpp
|
||||
platform/linux/linux_gtk_file_dialog.h
|
||||
platform/linux/linux_gtk_integration_p.h
|
||||
platform/linux/linux_gtk_integration.cpp
|
||||
platform/linux/linux_open_with_dialog.cpp
|
||||
platform/linux/linux_open_with_dialog.h
|
||||
platform/linux/linux_xlib_helper.cpp
|
||||
platform/linux/linux_xlib_helper.h
|
||||
)
|
||||
|
||||
nice_target_sources(Telegram ${src_loc}
|
||||
PRIVATE
|
||||
platform/linux/linux_gtk_integration_dummy.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT DESKTOP_APP_USE_PACKAGED)
|
||||
nice_target_sources(Telegram ${src_loc} PRIVATE platform/mac/mac_iconv_helper.c)
|
||||
endif()
|
||||
|
||||
BIN
Telegram/Resources/icons/calls/volume/speaker.png
Normal file
|
After Width: | Height: | Size: 556 B |
BIN
Telegram/Resources/icons/calls/volume/speaker@2x.png
Normal file
|
After Width: | Height: | Size: 998 B |
BIN
Telegram/Resources/icons/calls/volume/speaker@3x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/calls/volume/speaker_small.png
Normal file
|
After Width: | Height: | Size: 310 B |
BIN
Telegram/Resources/icons/calls/volume/speaker_small@2x.png
Normal file
|
After Width: | Height: | Size: 521 B |
BIN
Telegram/Resources/icons/calls/volume/speaker_small@3x.png
Normal file
|
After Width: | Height: | Size: 734 B |
BIN
Telegram/Resources/icons/info/edit/dotsmini.png
Normal file
|
After Width: | Height: | Size: 260 B |
BIN
Telegram/Resources/icons/info/edit/dotsmini@2x.png
Normal file
|
After Width: | Height: | Size: 438 B |
BIN
Telegram/Resources/icons/info/edit/dotsmini@3x.png
Normal file
|
After Width: | Height: | Size: 643 B |
BIN
Telegram/Resources/icons/info/edit/group_manage_actions.png
Normal file
|
After Width: | Height: | Size: 430 B |
BIN
Telegram/Resources/icons/info/edit/group_manage_actions@2x.png
Normal file
|
After Width: | Height: | Size: 780 B |
BIN
Telegram/Resources/icons/info/edit/group_manage_actions@3x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_admins.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_admins@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_admins@3x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_links.png
Normal file
|
After Width: | Height: | Size: 933 B |
BIN
Telegram/Resources/icons/info/edit/group_manage_links@2x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_links@3x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_members.png
Normal file
|
After Width: | Height: | Size: 620 B |
BIN
Telegram/Resources/icons/info/edit/group_manage_members@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_members@3x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Telegram/Resources/icons/info/edit/group_manage_permissions.png
Normal file
|
After Width: | Height: | Size: 980 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
BIN
Telegram/Resources/icons/info/edit/links_copy.png
Normal file
|
After Width: | Height: | Size: 397 B |
BIN
Telegram/Resources/icons/info/edit/links_copy@2x.png
Normal file
|
After Width: | Height: | Size: 682 B |
BIN
Telegram/Resources/icons/info/edit/links_copy@3x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Telegram/Resources/icons/info/edit/links_link.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
Telegram/Resources/icons/info/edit/links_link@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Telegram/Resources/icons/info/edit/links_link@3x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Telegram/Resources/icons/info/edit/links_share.png
Normal file
|
After Width: | Height: | Size: 464 B |
BIN
Telegram/Resources/icons/info/edit/links_share@2x.png
Normal file
|
After Width: | Height: | Size: 859 B |
BIN
Telegram/Resources/icons/info/edit/links_share@3x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/info/edit/roundbtn_plus.png
Normal file
|
After Width: | Height: | Size: 289 B |
BIN
Telegram/Resources/icons/info/edit/roundbtn_plus@2x.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
Telegram/Resources/icons/info/edit/roundbtn_plus@3x.png
Normal file
|
After Width: | Height: | Size: 752 B |
@@ -126,6 +126,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_channel_status" = "channel";
|
||||
"lng_group_status" = "group";
|
||||
"lng_scam_badge" = "SCAM";
|
||||
"lng_fake_badge" = "FAKE";
|
||||
|
||||
"lng_flood_error" = "Too many tries. Please try again later.";
|
||||
"lng_gif_error" = "An error has occurred while reading GIF animation :(";
|
||||
@@ -327,6 +328,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_events_title" = "Events";
|
||||
"lng_settings_events_joined" = "Contact joined Telegram";
|
||||
"lng_settings_events_pinned" = "Pinned messages";
|
||||
"lng_settings_notifications_calls_title" = "Calls";
|
||||
|
||||
"lng_notification_preview" = "You have a new message";
|
||||
"lng_notification_reply" = "Reply";
|
||||
@@ -394,6 +396,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_call_stop_mic_test" = "Stop test";
|
||||
"lng_settings_call_section_other" = "Other settings";
|
||||
"lng_settings_call_open_system_prefs" = "Open system sound preferences";
|
||||
"lng_settings_call_accept_calls" = "Accept calls from this device";
|
||||
"lng_settings_call_device_default" = "Same as the System";
|
||||
"lng_settings_call_audio_ducking" = "Mute other sounds during calls";
|
||||
|
||||
@@ -932,6 +935,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_manage_peer_exceptions" = "Exceptions";
|
||||
"lng_manage_peer_removed_users" = "Removed users";
|
||||
"lng_manage_peer_permissions" = "Permissions";
|
||||
"lng_manage_peer_invite_links" = "Invite links";
|
||||
|
||||
"lng_manage_peer_group_type" = "Group type";
|
||||
"lng_manage_peer_channel_type" = "Channel type";
|
||||
@@ -975,6 +979,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_report_bot_title" = "Report bot";
|
||||
"lng_report_message_title" = "Report message";
|
||||
"lng_report_reason_spam" = "Spam";
|
||||
"lng_report_reason_fake" = "Fake Account";
|
||||
"lng_report_reason_violence" = "Violence";
|
||||
"lng_report_reason_child_abuse" = "Child Abuse";
|
||||
"lng_report_reason_pornography" = "Pornography";
|
||||
@@ -1164,14 +1169,53 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_invite_members#other" = "{count} members, among them:";
|
||||
"lng_channel_invite_private" = "This channel is private.\nPlease join it to continue viewing its content.";
|
||||
|
||||
"lng_group_invite_create" = "Create an invite link";
|
||||
"lng_group_invite_about" = "Telegram users will be able to join\nyour group by following this link.";
|
||||
"lng_group_invite_about_channel" = "Telegram users will be able to join\nyour channel by following this link.";
|
||||
"lng_group_invite_create_new" = "Revoke invite link";
|
||||
"lng_group_invite_create" = "Create an invite link"; // #TODO links legacy
|
||||
"lng_group_invite_about_new" = "Your previous link will be deactivated and we'll generate a new invite link for you.";
|
||||
"lng_group_invite_copied" = "Invite link copied to clipboard.";
|
||||
"lng_group_invite_no_room" = "Unable to join this group because there are too many members in it already.";
|
||||
|
||||
"lng_group_invite_permanent" = "Permanent link";
|
||||
"lng_group_invite_copy" = "Copy Link";
|
||||
"lng_group_invite_share" = "Share Link";
|
||||
"lng_group_invite_revoke" = "Revoke Link";
|
||||
"lng_group_invite_no_joined" = "No one joined yet";
|
||||
"lng_group_invite_joined#one" = "{count} person joined";
|
||||
"lng_group_invite_joined#other" = "{count} people joined";
|
||||
"lng_group_invite_about_permanent_group" = "Anyone who has Telegram installed will be able to join your group by following this link.";
|
||||
"lng_group_invite_about_permanent_channel" = "Anyone who has Telegram installed will be able to join your channel by following this link.";
|
||||
"lng_group_invite_manage" = "Manage Invite Links";
|
||||
"lng_group_invite_manage_about" = "You can create additional invite links that have limited time or number of usages.";
|
||||
"lng_group_invite_title" = "Invite links";
|
||||
"lng_group_invite_add" = "Create a New Link";
|
||||
"lng_group_invite_add_about" = "You can generate invite links that will expire after they've been used.";
|
||||
"lng_group_invite_expires_at" = "This link expires {when}.";
|
||||
"lng_group_invite_expired_already" = "This link has expired.";
|
||||
"lng_group_invite_created_by" = "Link created by";
|
||||
"lng_group_invite_context_copy" = "Copy";
|
||||
"lng_group_invite_context_share" = "Share";
|
||||
"lng_group_invite_context_edit" = "Edit";
|
||||
"lng_group_invite_context_revoke" = "Revoke";
|
||||
"lng_group_invite_context_delete" = "Delete";
|
||||
"lng_group_invite_context_delete_all" = "Delete all";
|
||||
"lng_group_invite_delete_sure" = "Are you sure you want to delete that revoked link?";
|
||||
"lng_group_invite_delete_all_sure" = "Are you sure you want to delete all revoked links? This action cannot be undone.";
|
||||
"lng_group_invite_revoked_title" = "Revoked links";
|
||||
"lng_group_invite_revoke_about" = "Are you sure you want to revoke that invite link?";
|
||||
"lng_group_invite_link_expired" = "Expired";
|
||||
"lng_group_invite_link_revoked" = "Revoked";
|
||||
"lng_group_invite_edit_title" = "Edit Link";
|
||||
"lng_group_invite_new_title" = "New Link";
|
||||
"lng_group_invite_expire_title" = "Limit by time period";
|
||||
"lng_group_invite_expire_about" = "You can make the link expire after a certain time.";
|
||||
"lng_group_invite_expire_never" = "No limit";
|
||||
"lng_group_invite_expire_custom" = "Custom";
|
||||
"lng_group_invite_usage_title" = "Limit number of uses";
|
||||
"lng_group_invite_usage_about" = "You can make the link expire after it has been used for a certain number of times.";
|
||||
"lng_group_invite_expire_after" = "Expire after";
|
||||
"lng_group_invite_custom_limit" = "Enter custom limit";
|
||||
"lng_group_invite_usage_any" = "No limit";
|
||||
"lng_group_invite_usage_custom" = "Custom";
|
||||
|
||||
"lng_channel_public_link_copied" = "Link copied to clipboard.";
|
||||
"lng_context_about_private_link" = "This link will only work for members of this chat.";
|
||||
|
||||
@@ -1183,10 +1227,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_forwarded_channel_via" = "Forwarded from {channel} via {inline_bot}";
|
||||
"lng_forwarded_signed" = "{channel} ({user})";
|
||||
"lng_forwarded_hidden" = "The account was hidden by the user.";
|
||||
"lng_forwarded_imported" = "This message was imported from another app. It may not be real.";
|
||||
"lng_signed_author" = "Author: {user}";
|
||||
"lng_in_reply_to" = "In reply to";
|
||||
"lng_edited" = "edited";
|
||||
"lng_edited_date" = "Edited: {date}";
|
||||
"lng_sent_date" = "Sent: {date}";
|
||||
"lng_imported" = "imported";
|
||||
"lng_admin_badge" = "admin";
|
||||
"lng_owner_badge" = "owner";
|
||||
"lng_channel_badge" = "channel";
|
||||
@@ -1786,6 +1833,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_call_box_status_yesterday" = "Yesterday at {time}";
|
||||
"lng_call_box_status_date" = "{date} at {time}";
|
||||
"lng_call_box_status_group" = "({amount}) {status}";
|
||||
"lng_call_box_clear_all" = "Clear All";
|
||||
"lng_call_box_clear_sure" = "Are you sure you want to completely clear your calls log?";
|
||||
"lng_call_box_clear_button" = "Clear";
|
||||
|
||||
"lng_call_outgoing" = "Outgoing call";
|
||||
"lng_call_video_outgoing" = "Outgoing video call";
|
||||
@@ -1834,6 +1884,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_settings_title" = "Settings";
|
||||
"lng_group_call_invite" = "Invite Member";
|
||||
"lng_group_call_invited_status" = "invited";
|
||||
"lng_group_call_muted_by_me_status" = "muted by me";
|
||||
"lng_group_call_invite_title" = "Invite members";
|
||||
"lng_group_call_invite_button" = "Invite";
|
||||
"lng_group_call_add_to_group_one" = "{user} isn't a member of «{group}» yet. Add them to the group?";
|
||||
@@ -1863,6 +1914,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_too_many" = "Sorry, there are too many members in this voice chat. Please try again later.";
|
||||
"lng_group_call_context_mute" = "Mute";
|
||||
"lng_group_call_context_unmute" = "Allow to speak";
|
||||
"lng_group_call_context_mute_for_me" = "Mute for me";
|
||||
"lng_group_call_context_unmute_for_me" = "Unmute for me";
|
||||
"lng_group_call_duration_days#one" = "{count} day";
|
||||
"lng_group_call_duration_days#other" = "{count} days";
|
||||
"lng_group_call_duration_hours#one" = "{count} hour";
|
||||
|
||||
@@ -109,7 +109,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType;
|
||||
storage.fileWebp#1081464c = storage.FileType;
|
||||
|
||||
userEmpty#200250ba id:int = User;
|
||||
user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User;
|
||||
user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User;
|
||||
|
||||
userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto;
|
||||
userProfilePhoto#69d3ab26 flags:# has_video:flags.0?true photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto;
|
||||
@@ -124,11 +124,11 @@ userStatusLastMonth#77ebc742 = UserStatus;
|
||||
chatEmpty#9ba2d800 id:int = Chat;
|
||||
chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat;
|
||||
chatForbidden#7328bdb id:int title:string = Chat;
|
||||
channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat;
|
||||
channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat;
|
||||
channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat;
|
||||
|
||||
chatFull#dc8c181 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall = ChatFull;
|
||||
channelFull#ef3a6acd flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall = ChatFull;
|
||||
chatFull#f3474af6 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall = ChatFull;
|
||||
channelFull#7a7de4f7 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall = ChatFull;
|
||||
|
||||
chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant;
|
||||
chatParticipantCreator#da13538a user_id:int = ChatParticipant;
|
||||
@@ -140,7 +140,7 @@ chatParticipants#3f460fed chat_id:int participants:Vector<ChatParticipant> versi
|
||||
chatPhotoEmpty#37c1011c = ChatPhoto;
|
||||
chatPhoto#d20b9f3c flags:# has_video:flags.0?true photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;
|
||||
|
||||
messageEmpty#83e5de54 id:int = Message;
|
||||
messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message;
|
||||
message#58ae39c9 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector<RestrictionReason> = Message;
|
||||
messageService#286fa604 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction = Message;
|
||||
|
||||
@@ -217,7 +217,7 @@ inputPeerNotifySettings#9c3d198e flags:# show_previews:flags.0?Bool silent:flags
|
||||
|
||||
peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = PeerNotifySettings;
|
||||
|
||||
peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true geo_distance:flags.6?int = PeerSettings;
|
||||
peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true geo_distance:flags.6?int = PeerSettings;
|
||||
|
||||
wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper;
|
||||
wallPaperNoFile#8af40b25 flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;
|
||||
@@ -229,6 +229,7 @@ inputReportReasonChildAbuse#adf44ee3 = ReportReason;
|
||||
inputReportReasonOther#e1746d0a text:string = ReportReason;
|
||||
inputReportReasonCopyright#9b89f93a = ReportReason;
|
||||
inputReportReasonGeoIrrelevant#dbd4feed = ReportReason;
|
||||
inputReportReasonFake#f5ddd6e7 = ReportReason;
|
||||
|
||||
userFull#edf17c12 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 user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int = UserFull;
|
||||
|
||||
@@ -407,7 +408,7 @@ encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat;
|
||||
encryptedChatWaiting#3bf703dc id:int access_hash:long date:int admin_id:int participant_id:int = EncryptedChat;
|
||||
encryptedChatRequested#62718a82 flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat;
|
||||
encryptedChat#fa56ce36 id:int access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long = EncryptedChat;
|
||||
encryptedChatDiscarded#13d6dd27 id:int = EncryptedChat;
|
||||
encryptedChatDiscarded#1e1c7c45 flags:# history_deleted:flags.0?true id:int = EncryptedChat;
|
||||
|
||||
inputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat;
|
||||
|
||||
@@ -455,6 +456,7 @@ sendMessageGamePlayAction#dd6a8f48 = SendMessageAction;
|
||||
sendMessageRecordRoundAction#88f27fbc = SendMessageAction;
|
||||
sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction;
|
||||
speakingInGroupCallAction#d92c2285 = SendMessageAction;
|
||||
sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction;
|
||||
|
||||
contacts.found#b3134d9d my_results:Vector<Peer> results:Vector<Peer> chats:Vector<Chat> users:Vector<User> = contacts.Found;
|
||||
|
||||
@@ -535,8 +537,7 @@ auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery;
|
||||
|
||||
receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage;
|
||||
|
||||
chatInviteEmpty#69df3769 = ExportedChatInvite;
|
||||
chatInviteExported#fc2e05bc link:string = ExportedChatInvite;
|
||||
chatInviteExported#6e24fc9d flags:# revoked:flags.0?true permanent:flags.5?true link:string admin_id:int date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int = ExportedChatInvite;
|
||||
|
||||
chatInviteAlready#5a686d7c chat:Chat = ChatInvite;
|
||||
chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector<User> = ChatInvite;
|
||||
@@ -661,7 +662,7 @@ messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_off
|
||||
|
||||
exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink;
|
||||
|
||||
messageFwdHeader#5f777dce flags:# from_id:flags.0?Peer from_name:flags.5?string date:int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int psa_type:flags.6?string = MessageFwdHeader;
|
||||
messageFwdHeader#5f777dce flags:# imported:flags.7?true from_id:flags.0?Peer from_name:flags.5?string date:int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int psa_type:flags.6?string = MessageFwdHeader;
|
||||
|
||||
auth.codeTypeSms#72a3158c = auth.CodeType;
|
||||
auth.codeTypeCall#741cd3e3 = auth.CodeType;
|
||||
@@ -820,7 +821,7 @@ payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_inf
|
||||
inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials;
|
||||
inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials;
|
||||
inputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials;
|
||||
inputPaymentCredentialsAndroidPay#ca05d50e payment_token:DataJSON google_transaction_id:string = InputPaymentCredentials;
|
||||
inputPaymentCredentialsGooglePay#8ac32801 payment_token:DataJSON = InputPaymentCredentials;
|
||||
|
||||
account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword;
|
||||
|
||||
@@ -1195,7 +1196,7 @@ groupCall#55903081 flags:# join_muted:flags.1?true can_change_join_muted:flags.2
|
||||
|
||||
inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall;
|
||||
|
||||
groupCallParticipant#56b087c9 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true user_id:int date:int active_date:flags.3?int source:int = GroupCallParticipant;
|
||||
groupCallParticipant#64c62a15 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true muted_by_you:flags.9?true user_id:int date:int active_date:flags.3?int source:int volume:flags.7?int = GroupCallParticipant;
|
||||
|
||||
phone.groupCall#66ab0bfc call:GroupCall participants:Vector<GroupCallParticipant> participants_next_offset:string users:Vector<User> = phone.GroupCall;
|
||||
|
||||
@@ -1207,6 +1208,12 @@ inlineQueryPeerTypeChat#d766c50a = InlineQueryPeerType;
|
||||
inlineQueryPeerTypeMegagroup#5ec4be43 = InlineQueryPeerType;
|
||||
inlineQueryPeerTypeBroadcast#6334ee9a = InlineQueryPeerType;
|
||||
|
||||
messages.historyImport#1662af0b id:long = messages.HistoryImport;
|
||||
|
||||
messages.historyImportParsed#5e0fb7b9 flags:# pm:flags.0?true group:flags.1?true title:flags.2?string = messages.HistoryImportParsed;
|
||||
|
||||
messages.affectedFoundMessages#ef8d3e6c pts:int pts_count:int offset:int messages:Vector<int> = messages.AffectedFoundMessages;
|
||||
|
||||
---functions---
|
||||
|
||||
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||
@@ -1349,12 +1356,12 @@ messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
||||
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
|
||||
messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates;
|
||||
messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates;
|
||||
messages.deleteChatUser#e0611f16 chat_id:int user_id:InputUser = Updates;
|
||||
messages.deleteChatUser#c534459a flags:# revoke_history:flags.0?true chat_id:int user_id:InputUser = Updates;
|
||||
messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
||||
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
|
||||
messages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat;
|
||||
messages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat;
|
||||
messages.discardEncryption#edd923c5 chat_id:int = Bool;
|
||||
messages.discardEncryption#f393aea0 flags:# delete_history:flags.0?true chat_id:int = Bool;
|
||||
messages.setEncryptedTyping#791451ed peer:InputEncryptedChat typing:Bool = Bool;
|
||||
messages.readEncryptedHistory#7f4b690a peer:InputEncryptedChat max_date:int = Bool;
|
||||
messages.sendEncrypted#44fa7a15 flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage;
|
||||
@@ -1456,6 +1463,12 @@ messages.getReplies#24b581ba peer:InputPeer msg_id:int offset_id:int offset_date
|
||||
messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;
|
||||
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
|
||||
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
||||
messages.deleteChat#83247d11 chat_id:int = Bool;
|
||||
messages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages;
|
||||
messages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed;
|
||||
messages.initHistoryImport#34090c3b peer:InputPeer file:InputFile media_count:int = messages.HistoryImport;
|
||||
messages.uploadImportedMedia#2a862092 peer:InputPeer import_id:long file_name:string media:InputMedia = MessageMedia;
|
||||
messages.startHistoryImport#b43df344 peer:InputPeer import_id:long = Bool;
|
||||
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
@@ -1507,7 +1520,7 @@ channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipant
|
||||
channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant;
|
||||
channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;
|
||||
channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;
|
||||
channels.createChannel#3d5fb10f flags:# broadcast:flags.0?true megagroup:flags.1?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string = Updates;
|
||||
channels.createChannel#3d5fb10f flags:# broadcast:flags.0?true megagroup:flags.1?true for_import:flags.3?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string = Updates;
|
||||
channels.editAdmin#d33c8902 channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:string = Updates;
|
||||
channels.editTitle#566decd0 channel:InputChannel title:string = Updates;
|
||||
channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates;
|
||||
@@ -1564,7 +1577,7 @@ phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool;
|
||||
phone.createGroupCall#bd3dabe0 peer:InputPeer random_id:int = Updates;
|
||||
phone.joinGroupCall#5f9c8e62 flags:# muted:flags.0?true call:InputGroupCall params:DataJSON = Updates;
|
||||
phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates;
|
||||
phone.editGroupCallMember#63146ae4 flags:# muted:flags.0?true call:InputGroupCall user_id:InputUser = Updates;
|
||||
phone.editGroupCallMember#a5e76cd8 flags:# muted:flags.0?true call:InputGroupCall user_id:InputUser volume:flags.1?int = Updates;
|
||||
phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector<InputUser> = Updates;
|
||||
phone.discardGroupCall#7a777135 call:InputGroupCall = Updates;
|
||||
phone.toggleGroupCallSettings#74bbb43d flags:# call:InputGroupCall join_muted:flags.0?Bool = Updates;
|
||||
@@ -1587,4 +1600,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel
|
||||
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
|
||||
|
||||
// LAYER 122
|
||||
// LAYER 123
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="2.5.2.0" />
|
||||
Version="2.5.8.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,5,2,0
|
||||
PRODUCTVERSION 2,5,2,0
|
||||
FILEVERSION 2,5,8,0
|
||||
PRODUCTVERSION 2,5,8,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "2.5.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "FileVersion", "2.5.8.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.5.2.0"
|
||||
VALUE "ProductVersion", "2.5.8.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,5,2,0
|
||||
PRODUCTVERSION 2,5,2,0
|
||||
FILEVERSION 2,5,8,0
|
||||
PRODUCTVERSION 2,5,8,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "2.5.2.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "FileVersion", "2.5.8.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.5.2.0"
|
||||
VALUE "ProductVersion", "2.5.8.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -149,6 +149,7 @@ int main(int argc, char *argv[])
|
||||
QString remove;
|
||||
int version = 0;
|
||||
bool targetosx = false;
|
||||
bool targetwin64 = false;
|
||||
QFileInfoList files;
|
||||
for (int i = 0; i < argc; ++i) {
|
||||
if (string("-path") == argv[i] && i + 1 < argc) {
|
||||
@@ -158,6 +159,7 @@ int main(int argc, char *argv[])
|
||||
if (remove.isEmpty()) remove = info.canonicalPath() + "/";
|
||||
} else if (string("-target") == argv[i] && i + 1 < argc) {
|
||||
targetosx = (string("osx") == argv[i + 1]);
|
||||
targetwin64 = (string("win64") == argv[i + 1]);
|
||||
} else if (string("-version") == argv[i] && i + 1 < argc) {
|
||||
version = QString(argv[i + 1]).toInt();
|
||||
} else if (string("-beta") == argv[i]) {
|
||||
@@ -464,7 +466,7 @@ int main(int argc, char *argv[])
|
||||
cout << "Signature verified!\n";
|
||||
RSA_free(pbKey);
|
||||
#ifdef Q_OS_WIN
|
||||
QString outName(QString("tupdate%1").arg(AlphaVersion ? AlphaVersion : version));
|
||||
QString outName((targetwin64 ? QString("tx64upd%1") : QString("tupdate%1")).arg(AlphaVersion ? AlphaVersion : version));
|
||||
#elif defined Q_OS_MAC
|
||||
QString outName((targetosx ? QString("tosxupd%1") : QString("tmacupd%1")).arg(AlphaVersion ? AlphaVersion : version));
|
||||
#elif defined Q_OS_UNIX
|
||||
|
||||
631
Telegram/SourceFiles/api/api_invite_links.cpp
Normal file
@@ -0,0 +1,631 @@
|
||||
/*
|
||||
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_invite_links.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
|
||||
namespace Api {
|
||||
namespace {
|
||||
|
||||
constexpr auto kFirstPage = 10;
|
||||
constexpr auto kPerPage = 50;
|
||||
constexpr auto kJoinedFirstPage = 10;
|
||||
|
||||
void BringPermanentToFront(PeerInviteLinks &links) {
|
||||
auto &list = links.links;
|
||||
const auto i = ranges::find_if(list, [](const InviteLink &link) {
|
||||
return link.permanent && !link.revoked;
|
||||
});
|
||||
if (i != end(list) && i != begin(list)) {
|
||||
ranges::rotate(begin(list), i, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void RemovePermanent(PeerInviteLinks &links) {
|
||||
auto &list = links.links;
|
||||
list.erase(ranges::remove_if(list, [](const InviteLink &link) {
|
||||
return link.permanent && !link.revoked;
|
||||
}), end(list));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// #TODO links
|
||||
//JoinedByLinkSlice ParseJoinedByLinkSlice(
|
||||
// not_null<PeerData*> peer,
|
||||
// const MTPmessages_ChatInviteImporters &slice) {
|
||||
// auto result = JoinedByLinkSlice();
|
||||
// slice.match([&](const MTPDmessages_chatInviteImporters &data) {
|
||||
// auto &owner = peer->session().data();
|
||||
// owner.processUsers(data.vusers());
|
||||
// result.count = data.vcount().v;
|
||||
// result.users.reserve(data.vimporters().v.size());
|
||||
// for (const auto importer : data.vimporters().v) {
|
||||
// importer.match([&](const MTPDchatInviteImporter &data) {
|
||||
// result.users.push_back({
|
||||
// .user = owner.user(data.vuser_id().v),
|
||||
// .date = data.vdate().v,
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// return result;
|
||||
//}
|
||||
|
||||
InviteLinks::InviteLinks(not_null<ApiWrap*> api) : _api(api) {
|
||||
}
|
||||
|
||||
void InviteLinks::create(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(Link)> done,
|
||||
TimeId expireDate,
|
||||
int usageLimit) {
|
||||
performCreate(peer, std::move(done), false, expireDate, usageLimit);
|
||||
}
|
||||
|
||||
void InviteLinks::performCreate(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(Link)> done,
|
||||
bool revokeLegacyPermanent,
|
||||
TimeId expireDate,
|
||||
int usageLimit) {
|
||||
if (const auto i = _createCallbacks.find(peer)
|
||||
; i != end(_createCallbacks)) {
|
||||
if (done) {
|
||||
i->second.push_back(std::move(done));
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto &callbacks = _createCallbacks[peer];
|
||||
if (done) {
|
||||
callbacks.push_back(std::move(done));
|
||||
}
|
||||
|
||||
// #TODO links
|
||||
//using Flag = MTPmessages_ExportChatInvite::Flag;
|
||||
//_api->request(MTPmessages_ExportChatInvite(
|
||||
// MTP_flags((revokeLegacyPermanent
|
||||
// ? Flag::f_legacy_revoke_permanent
|
||||
// : Flag(0))
|
||||
// | (expireDate ? Flag::f_expire_date : Flag(0))
|
||||
// | (usageLimit ? Flag::f_usage_limit : Flag(0))),
|
||||
// peer->input,
|
||||
// MTP_int(expireDate),
|
||||
// MTP_int(usageLimit)
|
||||
_api->request(MTPmessages_ExportChatInvite(
|
||||
peer->input
|
||||
)).done([=](const MTPExportedChatInvite &result) {
|
||||
const auto callbacks = _createCallbacks.take(peer);
|
||||
const auto link = prepend(peer, result);
|
||||
if (callbacks) {
|
||||
for (const auto &callback : *callbacks) {
|
||||
callback(link);
|
||||
}
|
||||
}
|
||||
}).fail([=](const RPCError &error) {
|
||||
_createCallbacks.erase(peer);
|
||||
}).send();
|
||||
}
|
||||
|
||||
auto InviteLinks::lookupPermanent(not_null<PeerData*> peer) -> Link* {
|
||||
auto i = _firstSlices.find(peer);
|
||||
return (i != end(_firstSlices)) ? lookupPermanent(i->second) : nullptr;
|
||||
}
|
||||
|
||||
auto InviteLinks::lookupPermanent(Links &links) -> Link* {
|
||||
const auto first = links.links.begin();
|
||||
return (first != end(links.links) && first->permanent && !first->revoked)
|
||||
? &*first
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
auto InviteLinks::lookupPermanent(const Links &links) const -> const Link* {
|
||||
const auto first = links.links.begin();
|
||||
return (first != end(links.links) && first->permanent && !first->revoked)
|
||||
? &*first
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
auto InviteLinks::prepend(
|
||||
not_null<PeerData*> peer,
|
||||
const MTPExportedChatInvite &invite) -> Link {
|
||||
const auto link = parse(peer, invite);
|
||||
auto i = _firstSlices.find(peer);
|
||||
if (i == end(_firstSlices)) {
|
||||
i = _firstSlices.emplace(peer).first;
|
||||
}
|
||||
auto &links = i->second;
|
||||
const auto permanent = lookupPermanent(links);
|
||||
const auto hadPermanent = (permanent != nullptr);
|
||||
auto updateOldPermanent = Update{ .peer = peer };
|
||||
if (link.permanent && hadPermanent) {
|
||||
updateOldPermanent.was = permanent->link;
|
||||
updateOldPermanent.now = *permanent;
|
||||
updateOldPermanent.now->revoked = true;
|
||||
links.links.erase(begin(links.links));
|
||||
if (links.count > 0) {
|
||||
--links.count;
|
||||
}
|
||||
}
|
||||
// Must not dereference 'permanent' pointer after that.
|
||||
|
||||
++links.count;
|
||||
if (hadPermanent && !link.permanent) {
|
||||
links.links.insert(begin(links.links) + 1, link);
|
||||
} else {
|
||||
links.links.insert(begin(links.links), link);
|
||||
}
|
||||
|
||||
if (link.permanent) {
|
||||
editPermanentLink(peer, link.link);
|
||||
}
|
||||
notify(peer);
|
||||
|
||||
if (updateOldPermanent.now) {
|
||||
_updates.fire(std::move(updateOldPermanent));
|
||||
}
|
||||
_updates.fire(Update{ .peer = peer, .now = link });
|
||||
return link;
|
||||
}
|
||||
|
||||
void InviteLinks::edit(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
TimeId expireDate,
|
||||
int usageLimit,
|
||||
Fn<void(Link)> done) {
|
||||
performEdit(peer, link, std::move(done), false, expireDate, usageLimit);
|
||||
}
|
||||
|
||||
void InviteLinks::performEdit(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
Fn<void(Link)> done,
|
||||
bool revoke,
|
||||
TimeId expireDate,
|
||||
int usageLimit) {
|
||||
const auto key = LinkKey{ peer, link };
|
||||
if (_deleteCallbacks.contains(key)) {
|
||||
return;
|
||||
} else if (const auto i = _editCallbacks.find(key)
|
||||
; i != end(_editCallbacks)) {
|
||||
if (done) {
|
||||
i->second.push_back(std::move(done));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto permanent = revoke ? lookupPermanent(peer) : nullptr) {
|
||||
if (permanent->link == link) {
|
||||
// In case of revoking a permanent link
|
||||
// we should just create a new one instead.
|
||||
performCreate(peer, std::move(done), true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto &callbacks = _editCallbacks[key];
|
||||
if (done) {
|
||||
callbacks.push_back(std::move(done));
|
||||
}
|
||||
// #TODO links
|
||||
//using Flag = MTPmessages_EditExportedChatInvite::Flag;
|
||||
//_api->request(MTPmessages_EditExportedChatInvite(
|
||||
// MTP_flags((revoke ? Flag::f_revoked : Flag(0))
|
||||
// | ((!revoke && expireDate) ? Flag::f_expire_date : Flag(0))
|
||||
// | ((!revoke && usageLimit) ? Flag::f_usage_limit : Flag(0))),
|
||||
// peer->input,
|
||||
// MTP_string(link),
|
||||
// MTP_int(expireDate),
|
||||
// MTP_int(usageLimit)
|
||||
//)).done([=](const MTPmessages_ExportedChatInvite &result) {
|
||||
// const auto callbacks = _editCallbacks.take(key);
|
||||
// const auto peer = key.peer;
|
||||
// result.match([&](const MTPDmessages_exportedChatInvite &data) {
|
||||
// _api->session().data().processUsers(data.vusers());
|
||||
// const auto link = parse(peer, data.vinvite());
|
||||
// auto i = _firstSlices.find(peer);
|
||||
// if (i != end(_firstSlices)) {
|
||||
// const auto j = ranges::find(
|
||||
// i->second.links,
|
||||
// key.link,
|
||||
// &Link::link);
|
||||
// if (j != end(i->second.links)) {
|
||||
// if (link.revoked && !j->revoked) {
|
||||
// i->second.links.erase(j);
|
||||
// if (i->second.count > 0) {
|
||||
// --i->second.count;
|
||||
// }
|
||||
// } else {
|
||||
// *j = link;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// for (const auto &callback : *callbacks) {
|
||||
// callback(link);
|
||||
// }
|
||||
// _updates.fire(Update{
|
||||
// .peer = peer,
|
||||
// .was = key.link,
|
||||
// .now = link,
|
||||
// });
|
||||
// });
|
||||
//}).fail([=](const RPCError &error) {
|
||||
// _editCallbacks.erase(key);
|
||||
//}).send();
|
||||
}
|
||||
|
||||
void InviteLinks::revoke(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
Fn<void(Link)> done) {
|
||||
performEdit(peer, link, std::move(done), true);
|
||||
}
|
||||
|
||||
void InviteLinks::revokePermanent(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(Link)> done) {
|
||||
performCreate(peer, std::move(done), true);
|
||||
}
|
||||
|
||||
void InviteLinks::destroy(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
Fn<void()> done) {
|
||||
const auto key = LinkKey{ peer, link };
|
||||
|
||||
if (const auto i = _deleteCallbacks.find(key)
|
||||
; i != end(_deleteCallbacks)) {
|
||||
if (done) {
|
||||
i->second.push_back(std::move(done));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto &callbacks = _deleteCallbacks[key];
|
||||
if (done) {
|
||||
callbacks.push_back(std::move(done));
|
||||
}
|
||||
// #TODO links
|
||||
//_api->request(MTPmessages_DeleteExportedChatInvite(
|
||||
// peer->input,
|
||||
// MTP_string(link)
|
||||
//)).done([=](const MTPBool &result) {
|
||||
// const auto callbacks = _deleteCallbacks.take(key);
|
||||
// if (callbacks) {
|
||||
// for (const auto &callback : *callbacks) {
|
||||
// callback();
|
||||
// }
|
||||
// }
|
||||
// _updates.fire(Update{
|
||||
// .peer = peer,
|
||||
// .was = key.link,
|
||||
// });
|
||||
//}).fail([=](const RPCError &error) {
|
||||
// _deleteCallbacks.erase(key);
|
||||
//}).send();
|
||||
}
|
||||
|
||||
void InviteLinks::destroyAllRevoked(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> done) {
|
||||
if (const auto i = _deleteRevokedCallbacks.find(peer)
|
||||
; i != end(_deleteRevokedCallbacks)) {
|
||||
if (done) {
|
||||
i->second.push_back(std::move(done));
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto &callbacks = _deleteRevokedCallbacks[peer];
|
||||
if (done) {
|
||||
callbacks.push_back(std::move(done));
|
||||
}
|
||||
// #TODO links
|
||||
//_api->request(MTPmessages_DeleteRevokedExportedChatInvites(
|
||||
// peer->input
|
||||
//)).done([=](const MTPBool &result) {
|
||||
// if (const auto callbacks = _deleteRevokedCallbacks.take(peer)) {
|
||||
// for (const auto &callback : *callbacks) {
|
||||
// callback();
|
||||
// }
|
||||
// }
|
||||
// _allRevokedDestroyed.fire_copy(peer);
|
||||
//}).fail([=](const RPCError &error) {
|
||||
//}).send();
|
||||
}
|
||||
|
||||
void InviteLinks::requestLinks(not_null<PeerData*> peer) {
|
||||
if (_firstSliceRequests.contains(peer)) {
|
||||
return;
|
||||
}
|
||||
// #TODO links
|
||||
//const auto requestId = _api->request(MTPmessages_GetExportedChatInvites(
|
||||
// MTP_flags(0),
|
||||
// peer->input,
|
||||
// MTPInputUser(), // admin_id
|
||||
// MTPint(), // offset_date
|
||||
// MTPstring(), // offset_link
|
||||
// MTP_int(kFirstPage)
|
||||
//)).done([=](const MTPmessages_ExportedChatInvites &result) {
|
||||
// _firstSliceRequests.remove(peer);
|
||||
// auto slice = parseSlice(peer, result);
|
||||
// auto i = _firstSlices.find(peer);
|
||||
// const auto permanent = (i != end(_firstSlices))
|
||||
// ? lookupPermanent(i->second)
|
||||
// : nullptr;
|
||||
// if (!permanent) {
|
||||
// BringPermanentToFront(slice);
|
||||
// const auto j = _firstSlices.emplace_or_assign(
|
||||
// peer,
|
||||
// std::move(slice)).first;
|
||||
// if (const auto permanent = lookupPermanent(j->second)) {
|
||||
// editPermanentLink(peer, permanent->link);
|
||||
// }
|
||||
// } else {
|
||||
// RemovePermanent(slice);
|
||||
// auto &existing = i->second.links;
|
||||
// existing.erase(begin(existing) + 1, end(existing));
|
||||
// existing.insert(
|
||||
// end(existing),
|
||||
// begin(slice.links),
|
||||
// end(slice.links));
|
||||
// i->second.count = std::max(slice.count, int(existing.size()));
|
||||
// }
|
||||
// notify(peer);
|
||||
//}).fail([=](const RPCError &error) {
|
||||
// _firstSliceRequests.remove(peer);
|
||||
//}).send();
|
||||
//_firstSliceRequests.emplace(peer, requestId);
|
||||
}
|
||||
|
||||
std::optional<JoinedByLinkSlice> InviteLinks::lookupJoinedFirstSlice(
|
||||
LinkKey key) const {
|
||||
const auto i = _firstJoined.find(key);
|
||||
return (i != end(_firstJoined))
|
||||
? std::make_optional(i->second)
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<JoinedByLinkSlice> InviteLinks::joinedFirstSliceLoaded(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link) const {
|
||||
return lookupJoinedFirstSlice({ peer, link });
|
||||
}
|
||||
|
||||
rpl::producer<JoinedByLinkSlice> InviteLinks::joinedFirstSliceValue(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
int fullCount) {
|
||||
const auto key = LinkKey{ peer, link };
|
||||
auto current = lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice());
|
||||
if (current.count == fullCount
|
||||
&& (!fullCount || !current.users.empty())) {
|
||||
return rpl::single(current);
|
||||
}
|
||||
current.count = fullCount;
|
||||
const auto remove = int(current.users.size()) - current.count;
|
||||
if (remove > 0) {
|
||||
current.users.erase(end(current.users) - remove, end(current.users));
|
||||
}
|
||||
requestJoinedFirstSlice(key);
|
||||
using namespace rpl::mappers;
|
||||
return rpl::single(
|
||||
current
|
||||
) | rpl::then(_joinedFirstSliceLoaded.events(
|
||||
) | rpl::filter(
|
||||
_1 == key
|
||||
) | rpl::map([=] {
|
||||
return lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice());
|
||||
}));
|
||||
}
|
||||
|
||||
auto InviteLinks::updates(
|
||||
not_null<PeerData*> peer) const -> rpl::producer<Update> {
|
||||
return _updates.events() | rpl::filter([=](const Update &update) {
|
||||
return update.peer == peer;
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<> InviteLinks::allRevokedDestroyed(
|
||||
not_null<PeerData*> peer) const {
|
||||
using namespace rpl::mappers;
|
||||
return _allRevokedDestroyed.events(
|
||||
) | rpl::filter(
|
||||
_1 == peer
|
||||
) | rpl::to_empty;
|
||||
}
|
||||
|
||||
void InviteLinks::requestJoinedFirstSlice(LinkKey key) {
|
||||
//if (_firstJoinedRequests.contains(key)) { // #TODO links
|
||||
// return;
|
||||
//}
|
||||
//const auto requestId = _api->request(MTPmessages_GetChatInviteImporters(
|
||||
// key.peer->input,
|
||||
// MTP_string(key.link),
|
||||
// MTP_int(0), // offset_date
|
||||
// MTP_inputUserEmpty(), // offset_user
|
||||
// MTP_int(kJoinedFirstPage)
|
||||
//)).done([=](const MTPmessages_ChatInviteImporters &result) {
|
||||
// _firstJoinedRequests.remove(key);
|
||||
// _firstJoined[key] = ParseJoinedByLinkSlice(key.peer, result);
|
||||
// _joinedFirstSliceLoaded.fire_copy(key);
|
||||
//}).fail([=](const RPCError &error) {
|
||||
// _firstJoinedRequests.remove(key);
|
||||
//}).send();
|
||||
//_firstJoinedRequests.emplace(key, requestId);
|
||||
}
|
||||
|
||||
void InviteLinks::setPermanent(
|
||||
not_null<PeerData*> peer,
|
||||
const MTPExportedChatInvite &invite) {
|
||||
auto link = parse(peer, invite);
|
||||
if (!link.permanent) {
|
||||
LOG(("API Error: "
|
||||
"InviteLinks::setPermanent called with non-permanent link."));
|
||||
return;
|
||||
}
|
||||
auto i = _firstSlices.find(peer);
|
||||
if (i == end(_firstSlices)) {
|
||||
i = _firstSlices.emplace(peer).first;
|
||||
}
|
||||
auto &links = i->second;
|
||||
auto updateOldPermanent = Update{ .peer = peer };
|
||||
if (const auto permanent = lookupPermanent(links)) {
|
||||
if (permanent->link == link.link) {
|
||||
if (permanent->usage != link.usage) {
|
||||
permanent->usage = link.usage;
|
||||
_updates.fire(Update{
|
||||
.peer = peer,
|
||||
.was = link.link,
|
||||
.now = *permanent
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
updateOldPermanent.was = permanent->link;
|
||||
updateOldPermanent.now = *permanent;
|
||||
updateOldPermanent.now->revoked = true;
|
||||
links.links.erase(begin(links.links));
|
||||
if (links.count > 0) {
|
||||
--links.count;
|
||||
}
|
||||
}
|
||||
links.links.insert(begin(links.links), link);
|
||||
|
||||
editPermanentLink(peer, link.link);
|
||||
notify(peer);
|
||||
|
||||
if (updateOldPermanent.now) {
|
||||
_updates.fire(std::move(updateOldPermanent));
|
||||
}
|
||||
_updates.fire(Update{ .peer = peer, .now = link });
|
||||
}
|
||||
|
||||
void InviteLinks::clearPermanent(not_null<PeerData*> peer) {
|
||||
auto i = _firstSlices.find(peer);
|
||||
if (i == end(_firstSlices)) {
|
||||
return;
|
||||
}
|
||||
auto &links = i->second;
|
||||
const auto permanent = lookupPermanent(links);
|
||||
if (!permanent) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto updateOldPermanent = Update{ .peer = peer };
|
||||
updateOldPermanent.was = permanent->link;
|
||||
updateOldPermanent.now = *permanent;
|
||||
updateOldPermanent.now->revoked = true;
|
||||
links.links.erase(begin(links.links));
|
||||
if (links.count > 0) {
|
||||
--links.count;
|
||||
}
|
||||
|
||||
editPermanentLink(peer, QString());
|
||||
notify(peer);
|
||||
|
||||
if (updateOldPermanent.now) {
|
||||
_updates.fire(std::move(updateOldPermanent));
|
||||
}
|
||||
}
|
||||
|
||||
void InviteLinks::notify(not_null<PeerData*> peer) {
|
||||
peer->session().changes().peerUpdated(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::InviteLinks);
|
||||
}
|
||||
|
||||
auto InviteLinks::links(not_null<PeerData*> peer) const -> const Links & {
|
||||
static const auto kEmpty = Links();
|
||||
const auto i = _firstSlices.find(peer);
|
||||
return (i != end(_firstSlices)) ? i->second : kEmpty;
|
||||
}
|
||||
// #TODO links
|
||||
//auto InviteLinks::parseSlice(
|
||||
// not_null<PeerData*> peer,
|
||||
// const MTPmessages_ExportedChatInvites &slice) const -> Links {
|
||||
// auto i = _firstSlices.find(peer);
|
||||
// const auto permanent = (i != end(_firstSlices))
|
||||
// ? lookupPermanent(i->second)
|
||||
// : nullptr;
|
||||
// auto result = Links();
|
||||
// slice.match([&](const MTPDmessages_exportedChatInvites &data) {
|
||||
// peer->session().data().processUsers(data.vusers());
|
||||
// result.count = data.vcount().v;
|
||||
// for (const auto &invite : data.vinvites().v) {
|
||||
// const auto link = parse(peer, invite);
|
||||
// if (!permanent || link.link != permanent->link) {
|
||||
// result.links.push_back(link);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// return result;
|
||||
//}
|
||||
|
||||
auto InviteLinks::parse(
|
||||
not_null<PeerData*> peer,
|
||||
const MTPExportedChatInvite &invite) const -> Link {
|
||||
return invite.match([&](const MTPDchatInviteExported &data) {
|
||||
return Link{
|
||||
.link = qs(data.vlink()),
|
||||
.admin = peer->session().data().user(data.vadmin_id().v),
|
||||
.date = data.vdate().v,
|
||||
.startDate = data.vstart_date().value_or_empty(),
|
||||
.expireDate = data.vexpire_date().value_or_empty(),
|
||||
.usageLimit = data.vusage_limit().value_or_empty(),
|
||||
.usage = data.vusage().value_or_empty(),
|
||||
.permanent = true,//data.is_permanent(), // #TODO links
|
||||
.revoked = data.is_revoked(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void InviteLinks::requestMoreLinks(
|
||||
not_null<PeerData*> peer,
|
||||
TimeId lastDate,
|
||||
const QString &lastLink,
|
||||
bool revoked,
|
||||
Fn<void(Links)> done) {
|
||||
// #TODO links
|
||||
//using Flag = MTPmessages_GetExportedChatInvites::Flag;
|
||||
//_api->request(MTPmessages_GetExportedChatInvites(
|
||||
// MTP_flags(Flag::f_offset_link
|
||||
// | (revoked ? Flag::f_revoked : Flag(0))),
|
||||
// peer->input,
|
||||
// MTPInputUser(), // admin_id,
|
||||
// MTP_int(lastDate),
|
||||
// MTP_string(lastLink),
|
||||
// MTP_int(kPerPage)
|
||||
//)).done([=](const MTPmessages_ExportedChatInvites &result) {
|
||||
// auto slice = parseSlice(peer, result);
|
||||
// RemovePermanent(slice);
|
||||
// done(std::move(slice));
|
||||
//}).fail([=](const RPCError &error) {
|
||||
// done(Links());
|
||||
//}).send();
|
||||
}
|
||||
|
||||
void InviteLinks::editPermanentLink(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link) {
|
||||
if (const auto chat = peer->asChat()) {
|
||||
chat->setInviteLink(link);
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
channel->setInviteLink(link);
|
||||
} else {
|
||||
Unexpected("Peer in InviteLinks::editMainLink.");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Api
|
||||
187
Telegram/SourceFiles/api/api_invite_links.h
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
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 ApiWrap;
|
||||
|
||||
namespace Api {
|
||||
|
||||
struct InviteLink {
|
||||
QString link;
|
||||
not_null<UserData*> admin;
|
||||
TimeId date = 0;
|
||||
TimeId startDate = 0;
|
||||
TimeId expireDate = 0;
|
||||
int usageLimit = 0;
|
||||
int usage = 0;
|
||||
bool permanent = false;
|
||||
bool revoked = false;
|
||||
};
|
||||
|
||||
struct PeerInviteLinks {
|
||||
std::vector<InviteLink> links;
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct JoinedByLinkUser {
|
||||
not_null<UserData*> user;
|
||||
TimeId date = 0;
|
||||
};
|
||||
|
||||
struct JoinedByLinkSlice {
|
||||
std::vector<JoinedByLinkUser> users;
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct InviteLinkUpdate {
|
||||
not_null<PeerData*> peer;
|
||||
QString was;
|
||||
std::optional<InviteLink> now;
|
||||
};
|
||||
// #TODO links
|
||||
//[[nodiscard]] JoinedByLinkSlice ParseJoinedByLinkSlice(
|
||||
// not_null<PeerData*> peer,
|
||||
// const MTPmessages_ChatInviteImporters &slice);
|
||||
|
||||
class InviteLinks final {
|
||||
public:
|
||||
explicit InviteLinks(not_null<ApiWrap*> api);
|
||||
|
||||
using Link = InviteLink;
|
||||
using Links = PeerInviteLinks;
|
||||
using Update = InviteLinkUpdate;
|
||||
|
||||
void create(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(Link)> done = nullptr,
|
||||
TimeId expireDate = 0,
|
||||
int usageLimit = 0);
|
||||
void edit(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
TimeId expireDate,
|
||||
int usageLimit,
|
||||
Fn<void(Link)> done = nullptr);
|
||||
void revoke(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
Fn<void(Link)> done = nullptr);
|
||||
void revokePermanent(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(Link)> done = nullptr);
|
||||
void destroy(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
Fn<void()> done = nullptr);
|
||||
void destroyAllRevoked(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void()> done = nullptr);
|
||||
|
||||
void setPermanent(
|
||||
not_null<PeerData*> peer,
|
||||
const MTPExportedChatInvite &invite);
|
||||
void clearPermanent(not_null<PeerData*> peer);
|
||||
|
||||
void requestLinks(not_null<PeerData*> peer);
|
||||
[[nodiscard]] const Links &links(not_null<PeerData*> peer) const;
|
||||
|
||||
[[nodiscard]] rpl::producer<JoinedByLinkSlice> joinedFirstSliceValue(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
int fullCount);
|
||||
[[nodiscard]] std::optional<JoinedByLinkSlice> joinedFirstSliceLoaded(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link) const;
|
||||
[[nodiscard]] rpl::producer<Update> updates(
|
||||
not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] rpl::producer<> allRevokedDestroyed(
|
||||
not_null<PeerData*> peer) const;
|
||||
|
||||
void requestMoreLinks(
|
||||
not_null<PeerData*> peer,
|
||||
TimeId lastDate,
|
||||
const QString &lastLink,
|
||||
bool revoked,
|
||||
Fn<void(Links)> done);
|
||||
|
||||
private:
|
||||
struct LinkKey {
|
||||
not_null<PeerData*> peer;
|
||||
QString link;
|
||||
|
||||
friend inline bool operator<(const LinkKey &a, const LinkKey &b) {
|
||||
return (a.peer == b.peer)
|
||||
? (a.link < b.link)
|
||||
: (a.peer < b.peer);
|
||||
}
|
||||
friend inline bool operator==(const LinkKey &a, const LinkKey &b) {
|
||||
return (a.peer == b.peer) && (a.link == b.link);
|
||||
}
|
||||
};
|
||||
|
||||
// #TODO links
|
||||
//[[nodiscard]] Links parseSlice(
|
||||
// not_null<PeerData*> peer,
|
||||
// const MTPmessages_ExportedChatInvites &slice) const;
|
||||
[[nodiscard]] Link parse(
|
||||
not_null<PeerData*> peer,
|
||||
const MTPExportedChatInvite &invite) const;
|
||||
[[nodiscard]] Link *lookupPermanent(not_null<PeerData*> peer);
|
||||
[[nodiscard]] Link *lookupPermanent(Links &links);
|
||||
[[nodiscard]] const Link *lookupPermanent(const Links &links) const;
|
||||
Link prepend(
|
||||
not_null<PeerData*> peer,
|
||||
const MTPExportedChatInvite &invite);
|
||||
void notify(not_null<PeerData*> peer);
|
||||
|
||||
void editPermanentLink(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link);
|
||||
|
||||
void performEdit(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &link,
|
||||
Fn<void(Link)> done,
|
||||
bool revoke,
|
||||
TimeId expireDate = 0,
|
||||
int usageLimit = 0);
|
||||
void performCreate(
|
||||
not_null<PeerData*> peer,
|
||||
Fn<void(Link)> done,
|
||||
bool revokeLegacyPermanent,
|
||||
TimeId expireDate = 0,
|
||||
int usageLimit = 0);
|
||||
|
||||
void requestJoinedFirstSlice(LinkKey key);
|
||||
[[nodiscard]] std::optional<JoinedByLinkSlice> lookupJoinedFirstSlice(
|
||||
LinkKey key) const;
|
||||
|
||||
const not_null<ApiWrap*> _api;
|
||||
|
||||
base::flat_map<not_null<PeerData*>, Links> _firstSlices;
|
||||
base::flat_map<not_null<PeerData*>, mtpRequestId> _firstSliceRequests;
|
||||
|
||||
base::flat_map<LinkKey, JoinedByLinkSlice> _firstJoined;
|
||||
base::flat_map<LinkKey, mtpRequestId> _firstJoinedRequests;
|
||||
rpl::event_stream<LinkKey> _joinedFirstSliceLoaded;
|
||||
|
||||
base::flat_map<
|
||||
not_null<PeerData*>,
|
||||
std::vector<Fn<void(Link)>>> _createCallbacks;
|
||||
base::flat_map<LinkKey, std::vector<Fn<void(Link)>>> _editCallbacks;
|
||||
base::flat_map<LinkKey, std::vector<Fn<void()>>> _deleteCallbacks;
|
||||
base::flat_map<
|
||||
not_null<PeerData*>,
|
||||
std::vector<Fn<void()>>> _deleteRevokedCallbacks;
|
||||
|
||||
rpl::event_stream<Update> _updates;
|
||||
rpl::event_stream<not_null<PeerData*>> _allRevokedDestroyed;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Api
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "api/api_sending.h"
|
||||
|
||||
#include "api/api_text_entities.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_photo.h"
|
||||
@@ -76,7 +77,7 @@ void SendExistingMedia(
|
||||
const auto newId = FullMsgId(
|
||||
peerToChannel(peer->id),
|
||||
session->data().nextLocalMessageId());
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
|
||||
auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
|
||||
auto clientFlags = NewMessageClientFlags();
|
||||
@@ -249,7 +250,7 @@ bool SendDice(Api::MessageToSend &message) {
|
||||
const auto newId = FullMsgId(
|
||||
peerToChannel(peer->id),
|
||||
session->data().nextLocalMessageId());
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
|
||||
auto &histories = history->owner().histories();
|
||||
auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
|
||||
|
||||
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "api/api_authorizations.h"
|
||||
#include "api/api_attached_stickers.h"
|
||||
#include "api/api_hash.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "api/api_media.h"
|
||||
#include "api/api_sending.h"
|
||||
#include "api/api_text_entities.h"
|
||||
@@ -194,7 +195,8 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
|
||||
, _attachedStickers(std::make_unique<Api::AttachedStickers>(this))
|
||||
, _selfDestruct(std::make_unique<Api::SelfDestruct>(this))
|
||||
, _sensitiveContent(std::make_unique<Api::SensitiveContent>(this))
|
||||
, _globalPrivacy(std::make_unique<Api::GlobalPrivacy>(this)) {
|
||||
, _globalPrivacy(std::make_unique<Api::GlobalPrivacy>(this))
|
||||
, _inviteLinks(std::make_unique<Api::InviteLinks>(this)) {
|
||||
crl::on_main(session, [=] {
|
||||
// You can't use _session->lifetime() in the constructor,
|
||||
// only queued, because it is not constructed yet.
|
||||
@@ -354,7 +356,7 @@ void ApiWrap::requestTermsUpdate() {
|
||||
|
||||
const auto requestNext = [&](auto &&data) {
|
||||
const auto timeout = (data.vexpires().v - base::unixtime::now());
|
||||
_termsUpdateSendAt = crl::now() + snap(
|
||||
_termsUpdateSendAt = crl::now() + std::clamp(
|
||||
timeout * crl::time(1000),
|
||||
kTermsUpdateTimeoutMin,
|
||||
kTermsUpdateTimeoutMax);
|
||||
@@ -1712,6 +1714,7 @@ void ApiWrap::kickParticipant(
|
||||
not_null<ChatData*> chat,
|
||||
not_null<UserData*> user) {
|
||||
request(MTPmessages_DeleteChatUser(
|
||||
MTP_flags(0),
|
||||
chat->inputChat,
|
||||
user->inputUser
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
@@ -2120,33 +2123,6 @@ void ApiWrap::unblockPeer(not_null<PeerData*> peer, Fn<void()> onDone) {
|
||||
_blockRequests.emplace(peer, requestId);
|
||||
}
|
||||
|
||||
void ApiWrap::exportInviteLink(not_null<PeerData*> peer) {
|
||||
if (_exportInviteRequests.find(peer) != end(_exportInviteRequests)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto requestId = [&] {
|
||||
return request(MTPmessages_ExportChatInvite(
|
||||
peer->input
|
||||
)).done([=](const MTPExportedChatInvite &result) {
|
||||
_exportInviteRequests.erase(peer);
|
||||
const auto link = (result.type() == mtpc_chatInviteExported)
|
||||
? qs(result.c_chatInviteExported().vlink())
|
||||
: QString();
|
||||
if (const auto chat = peer->asChat()) {
|
||||
chat->setInviteLink(link);
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
channel->setInviteLink(link);
|
||||
} else {
|
||||
Unexpected("Peer in ApiWrap::exportInviteLink.");
|
||||
}
|
||||
}).fail([=](const RPCError &error) {
|
||||
_exportInviteRequests.erase(peer);
|
||||
}).send();
|
||||
}();
|
||||
_exportInviteRequests.emplace(peer, requestId);
|
||||
}
|
||||
|
||||
void ApiWrap::requestNotifySettings(const MTPInputNotifyPeer &peer) {
|
||||
const auto key = [&] {
|
||||
switch (peer.type()) {
|
||||
@@ -2329,6 +2305,7 @@ void ApiWrap::clearHistory(not_null<PeerData*> peer, bool revoke) {
|
||||
void ApiWrap::deleteConversation(not_null<PeerData*> peer, bool revoke) {
|
||||
if (const auto chat = peer->asChat()) {
|
||||
request(MTPmessages_DeleteChatUser(
|
||||
MTP_flags(0),
|
||||
chat->inputChat,
|
||||
_session->user()->inputUser
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
@@ -2373,14 +2350,14 @@ void ApiWrap::deleteHistory(
|
||||
deleteTillId = history->lastMessage()->id;
|
||||
}
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
if (!justClear) {
|
||||
if (!justClear && !revoke) {
|
||||
channel->ptsWaitingForShortPoll(-1);
|
||||
leaveChannel(channel);
|
||||
} else {
|
||||
if (const auto migrated = peer->migrateFrom()) {
|
||||
deleteHistory(migrated, justClear, revoke);
|
||||
}
|
||||
if (IsServerMsgId(deleteTillId)) {
|
||||
if (IsServerMsgId(deleteTillId) || (!justClear && revoke)) {
|
||||
history->owner().histories().deleteAllMessages(
|
||||
history,
|
||||
deleteTillId,
|
||||
@@ -2409,10 +2386,10 @@ void ApiWrap::applyUpdates(
|
||||
}
|
||||
|
||||
int ApiWrap::applyAffectedHistory(
|
||||
not_null<PeerData*> peer,
|
||||
PeerData *peer,
|
||||
const MTPmessages_AffectedHistory &result) {
|
||||
const auto &data = result.c_messages_affectedHistory();
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
if (const auto channel = peer ? peer->asChannel() : nullptr) {
|
||||
channel->ptsUpdateAndApply(data.vpts().v, data.vpts_count().v);
|
||||
} else {
|
||||
updates().updateAndApply(data.vpts().v, data.vpts_count().v);
|
||||
@@ -2472,7 +2449,7 @@ void ApiWrap::saveDraftsToCloud() {
|
||||
|
||||
auto flags = MTPmessages_SaveDraft::Flags(0);
|
||||
auto &textWithTags = cloudDraft->textWithTags;
|
||||
if (cloudDraft->previewCancelled) {
|
||||
if (cloudDraft->previewState != Data::PreviewState::Allowed) {
|
||||
flags |= MTPmessages_SaveDraft::Flag::f_no_webpage;
|
||||
}
|
||||
if (cloudDraft->msgId) {
|
||||
@@ -4025,7 +4002,7 @@ void ApiWrap::forwardMessages(
|
||||
ids.reserve(count);
|
||||
randomIds.reserve(count);
|
||||
for (const auto item : items) {
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
if (genClientSideMessage) {
|
||||
if (const auto message = item->toHistoryMessage()) {
|
||||
const auto newId = FullMsgId(
|
||||
@@ -4348,7 +4325,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
|
||||
auto newId = FullMsgId(
|
||||
peerToChannel(peer->id),
|
||||
_session->data().nextLocalMessageId());
|
||||
auto randomId = rand_value<uint64>();
|
||||
auto randomId = openssl::RandomValue<uint64>();
|
||||
|
||||
TextUtilities::Trim(sending);
|
||||
|
||||
@@ -4481,7 +4458,7 @@ void ApiWrap::sendBotStart(not_null<UserData*> bot, PeerData *chat) {
|
||||
sendMessage(std::move(message));
|
||||
return;
|
||||
}
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
request(MTPmessages_StartBot(
|
||||
bot->inputUser,
|
||||
chat ? chat->input : MTP_inputPeerEmpty(),
|
||||
@@ -4507,7 +4484,7 @@ void ApiWrap::sendInlineResult(
|
||||
const auto newId = FullMsgId(
|
||||
peerToChannel(peer->id),
|
||||
_session->data().nextLocalMessageId());
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
|
||||
auto flags = NewMessageFlags(peer) | MTPDmessage::Flag::f_media;
|
||||
auto clientFlags = NewMessageClientFlags();
|
||||
@@ -4659,7 +4636,7 @@ void ApiWrap::sendMedia(
|
||||
not_null<HistoryItem*> item,
|
||||
const MTPInputMedia &media,
|
||||
Api::SendOptions options) {
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
_session->data().registerMessageRandomId(randomId, item->fullId());
|
||||
|
||||
sendMediaWithRandomId(item, media, options, randomId);
|
||||
@@ -4727,7 +4704,7 @@ void ApiWrap::sendAlbumWithUploaded(
|
||||
const MessageGroupId &groupId,
|
||||
const MTPInputMedia &media) {
|
||||
const auto localId = item->fullId();
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
_session->data().registerMessageRandomId(randomId, localId);
|
||||
|
||||
const auto albumIt = _sendingAlbums.find(groupId.raw());
|
||||
@@ -5240,6 +5217,10 @@ Api::GlobalPrivacy &ApiWrap::globalPrivacy() {
|
||||
return *_globalPrivacy;
|
||||
}
|
||||
|
||||
Api::InviteLinks &ApiWrap::inviteLinks() {
|
||||
return *_inviteLinks;
|
||||
}
|
||||
|
||||
void ApiWrap::createPoll(
|
||||
const PollData &data,
|
||||
const SendAction &action,
|
||||
@@ -5275,7 +5256,7 @@ void ApiWrap::createPoll(
|
||||
MTP_int(replyTo),
|
||||
PollDataToInputMedia(&data),
|
||||
MTP_string(),
|
||||
MTP_long(rand_value<uint64>()),
|
||||
MTP_long(openssl::RandomValue<uint64>()),
|
||||
MTPReplyMarkup(),
|
||||
MTPVector<MTPMessageEntity>(),
|
||||
MTP_int(action.options.scheduled)
|
||||
|
||||
@@ -60,6 +60,7 @@ class AttachedStickers;
|
||||
class SelfDestruct;
|
||||
class SensitiveContent;
|
||||
class GlobalPrivacy;
|
||||
class InviteLinks;
|
||||
|
||||
namespace details {
|
||||
|
||||
@@ -153,7 +154,7 @@ public:
|
||||
const MTPUpdates &updates,
|
||||
uint64 sentMessageRandomId = 0);
|
||||
int applyAffectedHistory(
|
||||
not_null<PeerData*> peer,
|
||||
PeerData *peer, // May be nullptr, like for deletePhoneCallHistory.
|
||||
const MTPmessages_AffectedHistory &result);
|
||||
|
||||
void registerModifyRequest(const QString &key, mtpRequestId requestId);
|
||||
@@ -289,7 +290,6 @@ public:
|
||||
void blockPeer(not_null<PeerData*> peer);
|
||||
void unblockPeer(not_null<PeerData*> peer, Fn<void()> onDone = nullptr);
|
||||
|
||||
void exportInviteLink(not_null<PeerData*> peer);
|
||||
void requestNotifySettings(const MTPInputNotifyPeer &peer);
|
||||
void updateNotifySettingsDelayed(not_null<const PeerData*> peer);
|
||||
void saveDraftToCloudDelayed(not_null<History*> history);
|
||||
@@ -464,6 +464,7 @@ public:
|
||||
[[nodiscard]] Api::SelfDestruct &selfDestruct();
|
||||
[[nodiscard]] Api::SensitiveContent &sensitiveContent();
|
||||
[[nodiscard]] Api::GlobalPrivacy &globalPrivacy();
|
||||
[[nodiscard]] Api::InviteLinks &inviteLinks();
|
||||
|
||||
void createPoll(
|
||||
const PollData &data,
|
||||
@@ -701,7 +702,6 @@ private:
|
||||
|
||||
QMap<ChannelData*, mtpRequestId> _channelAmInRequests;
|
||||
base::flat_map<not_null<PeerData*>, mtpRequestId> _blockRequests;
|
||||
base::flat_map<not_null<PeerData*>, mtpRequestId> _exportInviteRequests;
|
||||
base::flat_map<PeerId, mtpRequestId> _notifySettingRequests;
|
||||
base::flat_map<not_null<History*>, mtpRequestId> _draftsSaveRequestIds;
|
||||
base::Timer _draftsSaveTimer;
|
||||
@@ -828,6 +828,7 @@ private:
|
||||
const std::unique_ptr<Api::SelfDestruct> _selfDestruct;
|
||||
const std::unique_ptr<Api::SensitiveContent> _sensitiveContent;
|
||||
const std::unique_ptr<Api::GlobalPrivacy> _globalPrivacy;
|
||||
const std::unique_ptr<Api::InviteLinks> _inviteLinks;
|
||||
|
||||
base::flat_map<FullMsgId, mtpRequestId> _pollVotesRequestIds;
|
||||
base::flat_map<FullMsgId, mtpRequestId> _pollCloseRequestIds;
|
||||
|
||||
@@ -88,8 +88,10 @@ void AboutBox::resizeEvent(QResizeEvent *e) {
|
||||
void AboutBox::showVersionHistory() {
|
||||
if (cRealAlphaVersion()) {
|
||||
auto url = qsl("https://tdesktop.com/");
|
||||
if (Platform::IsWindows()) {
|
||||
if (Platform::IsWindows32Bit()) {
|
||||
url += qsl("win/%1.zip");
|
||||
} else if (Platform::IsWindows64Bit()) {
|
||||
url += qsl("win64/%1.zip");
|
||||
} else if (Platform::IsOSXBuild()) {
|
||||
url += qsl("osx/%1.zip");
|
||||
} else if (Platform::IsMac()) {
|
||||
@@ -143,5 +145,8 @@ QString currentVersionText() {
|
||||
} else if (AppBetaVersion) {
|
||||
result += " beta";
|
||||
}
|
||||
if (Platform::IsWindows64Bit()) {
|
||||
result += " x64";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lang/lang_keys.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "base/flat_set.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "boxes/confirm_phone_box.h" // ExtractPhonePrefix.
|
||||
#include "boxes/photo_crop_box.h"
|
||||
@@ -39,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwidget.h"
|
||||
#include "mainwindow.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "main/main_session.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_layers.h"
|
||||
@@ -382,7 +384,7 @@ void AddContactBox::save() {
|
||||
lastName = QString();
|
||||
}
|
||||
_sentName = firstName;
|
||||
_contactId = rand_value<uint64>();
|
||||
_contactId = openssl::RandomValue<uint64>();
|
||||
_addRequest = _session->api().request(MTPcontacts_ImportContacts(
|
||||
MTP_vector<MTPInputContact>(
|
||||
1,
|
||||
@@ -612,7 +614,9 @@ void GroupInfoBox::createGroup(
|
||||
}
|
||||
|
||||
void GroupInfoBox::submit() {
|
||||
if (_creationRequestId) return;
|
||||
if (_creationRequestId || _creatingInviteLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto title = TextUtilities::PrepareForSending(_title->getLastText());
|
||||
auto description = _description
|
||||
@@ -651,6 +655,8 @@ void GroupInfoBox::submit() {
|
||||
}
|
||||
|
||||
void GroupInfoBox::createChannel(const QString &title, const QString &description) {
|
||||
Expects(!_creationRequestId);
|
||||
|
||||
const auto flags = (_type == Type::Megagroup)
|
||||
? MTPchannels_CreateChannel::Flag::f_megagroup
|
||||
: MTPchannels_CreateChannel::Flag::f_broadcast;
|
||||
@@ -691,25 +697,7 @@ void GroupInfoBox::createChannel(const QString &title, const QString &descriptio
|
||||
std::move(image));
|
||||
}
|
||||
_createdChannel = channel;
|
||||
_creationRequestId = _api.request(MTPmessages_ExportChatInvite(
|
||||
_createdChannel->input
|
||||
)).done([=](const MTPExportedChatInvite &result) {
|
||||
_creationRequestId = 0;
|
||||
if (result.type() == mtpc_chatInviteExported) {
|
||||
auto link = qs(result.c_chatInviteExported().vlink());
|
||||
_createdChannel->setInviteLink(link);
|
||||
}
|
||||
if (_channelDone) {
|
||||
const auto callback = _channelDone;
|
||||
const auto argument = _createdChannel;
|
||||
closeBox();
|
||||
callback(argument);
|
||||
} else {
|
||||
Ui::show(Box<SetupChannelBox>(
|
||||
_navigation,
|
||||
_createdChannel));
|
||||
}
|
||||
}).send();
|
||||
checkInviteLink();
|
||||
};
|
||||
if (!success) {
|
||||
LOG(("API Error: channel not found in updates (GroupInfoBox::creationDone)"));
|
||||
@@ -728,6 +716,32 @@ void GroupInfoBox::createChannel(const QString &title, const QString &descriptio
|
||||
}).send();
|
||||
}
|
||||
|
||||
void GroupInfoBox::checkInviteLink() {
|
||||
Expects(_createdChannel != nullptr);
|
||||
|
||||
if (!_createdChannel->inviteLink().isEmpty()) {
|
||||
channelReady();
|
||||
return;
|
||||
}
|
||||
_creatingInviteLink = true;
|
||||
_createdChannel->session().api().inviteLinks().create(
|
||||
_createdChannel,
|
||||
crl::guard(this, [=](auto&&) { channelReady(); }));
|
||||
}
|
||||
|
||||
void GroupInfoBox::channelReady() {
|
||||
if (_channelDone) {
|
||||
const auto callback = _channelDone;
|
||||
const auto argument = _createdChannel;
|
||||
closeBox();
|
||||
callback(argument);
|
||||
} else {
|
||||
Ui::show(Box<SetupChannelBox>(
|
||||
_navigation,
|
||||
_createdChannel));
|
||||
}
|
||||
}
|
||||
|
||||
void GroupInfoBox::descriptionResized() {
|
||||
updateMaxHeight();
|
||||
update();
|
||||
@@ -829,7 +843,7 @@ void SetupChannelBox::prepare() {
|
||||
|
||||
_channel->session().changes().peerUpdates(
|
||||
_channel,
|
||||
Data::PeerUpdate::Flag::InviteLink
|
||||
Data::PeerUpdate::Flag::InviteLinks
|
||||
) | rpl::start_with_next([=] {
|
||||
rtlupdate(_invitationLink);
|
||||
}, lifetime());
|
||||
@@ -938,7 +952,7 @@ void SetupChannelBox::mouseMoveEvent(QMouseEvent *e) {
|
||||
void SetupChannelBox::mousePressEvent(QMouseEvent *e) {
|
||||
if (_linkOver) {
|
||||
if (_channel->inviteLink().isEmpty()) {
|
||||
_channel->session().api().exportInviteLink(_channel);
|
||||
_channel->session().api().inviteLinks().create(_channel);
|
||||
} else {
|
||||
QGuiApplication::clipboard()->setText(_channel->inviteLink());
|
||||
Ui::Toast::Show(tr::lng_create_channel_link_copied(tr::now));
|
||||
|
||||
@@ -121,6 +121,8 @@ private:
|
||||
void createGroup(not_null<PeerListBox*> selectUsersBox, const QString &title, const std::vector<not_null<PeerData*>> &users);
|
||||
void submitName();
|
||||
void submit();
|
||||
void checkInviteLink();
|
||||
void channelReady();
|
||||
|
||||
void descriptionResized();
|
||||
void updateMaxHeight();
|
||||
@@ -138,6 +140,7 @@ private:
|
||||
|
||||
// group / channel creation
|
||||
mtpRequestId _creationRequestId = 0;
|
||||
bool _creatingInviteLink = false;
|
||||
ChannelData *_createdChannel = nullptr;
|
||||
|
||||
};
|
||||
|
||||
@@ -905,3 +905,32 @@ pollResultsShowMore: SettingsButton(defaultSettingsButton) {
|
||||
|
||||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
|
||||
scheduleHeight: 95px;
|
||||
scheduleDateTop: 38px;
|
||||
scheduleDateField: InputField(defaultInputField) {
|
||||
textMargins: margins(2px, 0px, 2px, 0px);
|
||||
placeholderScale: 0.;
|
||||
heightMin: 30px;
|
||||
textAlign: align(top);
|
||||
font: font(14px);
|
||||
}
|
||||
scheduleTimeField: InputField(scheduleDateField) {
|
||||
border: 0px;
|
||||
borderActive: 0px;
|
||||
heightMin: 28px;
|
||||
placeholderFont: font(14px);
|
||||
placeholderFgActive: placeholderFgActive;
|
||||
}
|
||||
scheduleDateWidth: 136px;
|
||||
scheduleTimeWidth: 72px;
|
||||
scheduleAtSkip: 24px;
|
||||
scheduleAtTop: 42px;
|
||||
scheduleAtLabel: FlatLabel(defaultFlatLabel) {
|
||||
}
|
||||
scheduleTimeSeparator: FlatLabel(defaultFlatLabel) {
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(14px);
|
||||
}
|
||||
}
|
||||
scheduleTimeSeparatorPadding: margins(2px, 0px, 2px, 0px);
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwidget.h"
|
||||
#include "mainwindow.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
@@ -393,7 +394,7 @@ void MaxInviteBox::prepare() {
|
||||
|
||||
_channel->session().changes().peerUpdates(
|
||||
_channel,
|
||||
Data::PeerUpdate::Flag::InviteLink
|
||||
Data::PeerUpdate::Flag::InviteLinks
|
||||
) | rpl::start_with_next([=] {
|
||||
rtlupdate(_invitationLink);
|
||||
}, lifetime());
|
||||
@@ -407,7 +408,7 @@ void MaxInviteBox::mousePressEvent(QMouseEvent *e) {
|
||||
mouseMoveEvent(e);
|
||||
if (_linkOver) {
|
||||
if (_channel->inviteLink().isEmpty()) {
|
||||
_channel->session().api().exportInviteLink(_channel);
|
||||
_channel->session().api().inviteLinks().create(_channel);
|
||||
} else {
|
||||
QGuiApplication::clipboard()->setText(_channel->inviteLink());
|
||||
Ui::Toast::Show(tr::lng_create_channel_link_copied(tr::now));
|
||||
@@ -577,7 +578,8 @@ void DeleteMessagesBox::prepare() {
|
||||
const auto appendDetails = [&](TextWithEntities &&text) {
|
||||
details.append(qstr("\n\n")).append(std::move(text));
|
||||
};
|
||||
auto deleteText = tr::lng_box_delete();
|
||||
auto deleteText = lifetime().make_state<rpl::variable<QString>>();
|
||||
*deleteText = tr::lng_box_delete();
|
||||
auto deleteStyle = &st::defaultBoxButton;
|
||||
if (const auto peer = _wipeHistoryPeer) {
|
||||
if (_wipeHistoryJustClear) {
|
||||
@@ -597,16 +599,22 @@ void DeleteMessagesBox::prepare() {
|
||||
: peer->isMegagroup()
|
||||
? tr::lng_sure_leave_group(tr::now)
|
||||
: tr::lng_sure_leave_channel(tr::now);
|
||||
deleteText = _wipeHistoryPeer->isUser()
|
||||
? tr::lng_box_delete()
|
||||
: tr::lng_box_leave();
|
||||
deleteStyle = &(peer->isChannel()
|
||||
? st::defaultBoxButton
|
||||
: st::attentionBoxButton);
|
||||
if (!peer->isUser()) {
|
||||
*deleteText = tr::lng_box_leave();
|
||||
}
|
||||
deleteStyle = &st::attentionBoxButton;
|
||||
}
|
||||
if (auto revoke = revokeText(peer)) {
|
||||
_revoke.create(this, revoke->checkbox, false, st::defaultBoxCheckbox);
|
||||
appendDetails(std::move(revoke->description));
|
||||
if (!peer->isUser() && !_wipeHistoryJustClear) {
|
||||
_revoke->checkedValue(
|
||||
) | rpl::start_with_next([=](bool revokeForAll) {
|
||||
*deleteText = revokeForAll
|
||||
? tr::lng_box_delete()
|
||||
: tr::lng_box_leave();
|
||||
}, _revoke->lifetime());
|
||||
}
|
||||
}
|
||||
} else if (_moderateFrom) {
|
||||
Assert(_moderateInChannel != nullptr);
|
||||
@@ -643,7 +651,7 @@ void DeleteMessagesBox::prepare() {
|
||||
_text.create(this, rpl::single(std::move(details)), st::boxLabel);
|
||||
|
||||
addButton(
|
||||
std::move(deleteText),
|
||||
deleteText->value(),
|
||||
[=] { deleteAndClear(); },
|
||||
*deleteStyle);
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
@@ -701,6 +709,8 @@ auto DeleteMessagesBox::revokeText(not_null<PeerData*> peer) const
|
||||
tr::now,
|
||||
lt_user,
|
||||
user->firstName);
|
||||
} else if (_wipeHistoryJustClear) {
|
||||
return std::nullopt;
|
||||
} else {
|
||||
result.checkbox = tr::lng_delete_for_everyone_check(tr::now);
|
||||
}
|
||||
@@ -860,10 +870,16 @@ void DeleteMessagesBox::deleteAndClear() {
|
||||
_deleteConfirmedCallback();
|
||||
}
|
||||
|
||||
_session->data().histories().deleteMessages(_ids, revoke);
|
||||
|
||||
// deleteMessages can initiate closing of the current section,
|
||||
// which will cause this box to be destroyed.
|
||||
const auto session = _session;
|
||||
Ui::hideLayer();
|
||||
const auto weak = Ui::MakeWeak(this);
|
||||
|
||||
session->data().histories().deleteMessages(_ids, revoke);
|
||||
|
||||
if (const auto strong = weak.data()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
session->data().sendHistoryChangeNotifications();
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ private:
|
||||
|
||||
QPointer<Ui::SlideWrap<>> _aboutSponsored;
|
||||
QPointer<HostInput> _host;
|
||||
QPointer<Ui::PortInput> _port;
|
||||
QPointer<Ui::NumberInput> _port;
|
||||
QPointer<Ui::InputField> _user;
|
||||
QPointer<Ui::PasswordInput> _password;
|
||||
QPointer<Base64UrlInput> _secret;
|
||||
@@ -928,11 +928,12 @@ void ProxyBox::setupSocketAddress(const ProxyData &data) {
|
||||
st::connectionHostInputField,
|
||||
tr::lng_connection_host_ph(),
|
||||
data.host);
|
||||
_port = Ui::CreateChild<Ui::PortInput>(
|
||||
_port = Ui::CreateChild<Ui::NumberInput>(
|
||||
address,
|
||||
st::connectionPortInputField,
|
||||
tr::lng_connection_port_ph(),
|
||||
data.port ? QString::number(data.port) : QString());
|
||||
data.port ? QString::number(data.port) : QString(),
|
||||
65535);
|
||||
address->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
_port->moveToRight(0, 0);
|
||||
|
||||
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/unique_qptr.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
@@ -885,7 +886,7 @@ not_null<Ui::InputField*> CreatePollBox::setupSolution(
|
||||
object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
using namespace Settings;
|
||||
|
||||
const auto id = rand_value<uint64>();
|
||||
const auto id = openssl::RandomValue<uint64>();
|
||||
const auto error = lifetime().make_state<Errors>(Error::Question);
|
||||
|
||||
auto result = object_ptr<Ui::VerticalLayout>(this);
|
||||
|
||||
@@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history.h"
|
||||
#include "history/history_drag_area.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/media/history_view_document.h" // DrawThumbnailAsSongCover
|
||||
#include "platform/platform_specific.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "media/streaming/media_streaming_instance.h"
|
||||
@@ -171,7 +172,9 @@ EditCaptionBox::EditCaptionBox(
|
||||
_thumbw = 0;
|
||||
_thumbnailImageLoaded = true;
|
||||
} else {
|
||||
const auto thumbSize = st::msgFileThumbLayout.thumbSize;
|
||||
const auto thumbSize = (!media->document()->isSongWithCover()
|
||||
? st::msgFileThumbLayout
|
||||
: st::msgFileLayout).thumbSize;
|
||||
const auto tw = dimensions.width(), th = dimensions.height();
|
||||
if (tw > th) {
|
||||
_thumbw = (tw * thumbSize) / th;
|
||||
@@ -183,19 +186,31 @@ EditCaptionBox::EditCaptionBox(
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
const auto options = Images::Option::Smooth
|
||||
| Images::Option::RoundedSmall
|
||||
| Images::Option::RoundedTopLeft
|
||||
| Images::Option::RoundedTopRight
|
||||
| Images::Option::RoundedBottomLeft
|
||||
| Images::Option::RoundedBottomRight;
|
||||
_thumb = App::pixmapFromImageInPlace(Images::prepare(
|
||||
image->original(),
|
||||
_thumbw * cIntRetinaFactor(),
|
||||
0,
|
||||
options,
|
||||
thumbSize,
|
||||
thumbSize));
|
||||
if (media->document()->isSongWithCover()) {
|
||||
const auto size = QSize(thumbSize, thumbSize);
|
||||
_thumb = QPixmap(size);
|
||||
_thumb.fill(Qt::transparent);
|
||||
Painter p(&_thumb);
|
||||
|
||||
HistoryView::DrawThumbnailAsSongCover(
|
||||
p,
|
||||
_documentMedia,
|
||||
QRect(QPoint(), size));
|
||||
} else {
|
||||
const auto options = Images::Option::Smooth
|
||||
| Images::Option::RoundedSmall
|
||||
| Images::Option::RoundedTopLeft
|
||||
| Images::Option::RoundedTopRight
|
||||
| Images::Option::RoundedBottomLeft
|
||||
| Images::Option::RoundedBottomRight;
|
||||
_thumb = App::pixmapFromImageInPlace(Images::prepare(
|
||||
image->original(),
|
||||
_thumbw * cIntRetinaFactor(),
|
||||
0,
|
||||
options,
|
||||
thumbSize,
|
||||
thumbSize));
|
||||
}
|
||||
_thumbnailImageLoaded = true;
|
||||
};
|
||||
_refreshThumbnail();
|
||||
@@ -539,6 +554,14 @@ void EditCaptionBox::updateEditPreview() {
|
||||
song->title,
|
||||
song->performer);
|
||||
_isAudio = true;
|
||||
|
||||
if (auto cover = song->cover; !cover.isNull()) {
|
||||
_thumb = Ui::PrepareSongCoverForThumbnail(
|
||||
cover,
|
||||
st::msgFileLayout.thumbSize);
|
||||
_thumbw = _thumb.width() / cIntRetinaFactor();
|
||||
_thumbh = _thumb.height() / cIntRetinaFactor();
|
||||
}
|
||||
}
|
||||
|
||||
const auto getExt = [&] {
|
||||
@@ -810,15 +833,21 @@ void EditCaptionBox::setupDragArea() {
|
||||
areas.photo->setDroppedCallback(droppedCallback(true));
|
||||
}
|
||||
|
||||
bool EditCaptionBox::isThumbedLayout() const {
|
||||
return (_thumbw && !_isAudio);
|
||||
}
|
||||
|
||||
void EditCaptionBox::updateBoxSize() {
|
||||
auto newHeight = st::boxPhotoPadding.top() + st::boxPhotoCaptionSkip + _field->height() + errorTopSkip() + st::normalFont->height;
|
||||
if (_photo) {
|
||||
newHeight += _wayWrap->height() / 2;
|
||||
}
|
||||
const auto &st = _thumbw ? st::msgFileThumbLayout : st::msgFileLayout;
|
||||
const auto &st = isThumbedLayout()
|
||||
? st::msgFileThumbLayout
|
||||
: st::msgFileLayout;
|
||||
if (_photo || _animated) {
|
||||
newHeight += std::max(_thumbh, _gifh);
|
||||
} else if (_thumbw || _doc) {
|
||||
} else if (isThumbedLayout() || _doc) {
|
||||
newHeight += 0 + st.thumbSize + 0;
|
||||
} else {
|
||||
newHeight += st::boxTitleFont->height;
|
||||
@@ -902,7 +931,9 @@ void EditCaptionBox::paintEvent(QPaintEvent *e) {
|
||||
icon->paintInCenter(p, inner);
|
||||
}
|
||||
} else if (_doc) {
|
||||
const auto &st = _thumbw ? st::msgFileThumbLayout : st::msgFileLayout;
|
||||
const auto &st = isThumbedLayout()
|
||||
? st::msgFileThumbLayout
|
||||
: st::msgFileLayout;
|
||||
const auto w = width() - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
|
||||
const auto h = 0 + st.thumbSize + 0;
|
||||
const auto nameleft = 0 + st.thumbSize + st.padding.right();
|
||||
@@ -918,18 +949,24 @@ void EditCaptionBox::paintEvent(QPaintEvent *e) {
|
||||
// Ui::FillRoundCorner(p, x, y, w, h, st::msgInBg, Ui::MessageInCorners, &st::msgInShadow);
|
||||
|
||||
const auto rthumb = style::rtlrect(x + 0, y + 0, st.thumbSize, st.thumbSize, width());
|
||||
if (_thumbw) {
|
||||
if (isThumbedLayout()) {
|
||||
p.drawPixmap(rthumb.topLeft(), _thumb);
|
||||
} else {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::msgFileInBg);
|
||||
|
||||
{
|
||||
if (_isAudio && _thumbw) {
|
||||
p.drawPixmap(rthumb.topLeft(), _thumb);
|
||||
} else {
|
||||
p.setBrush(st::msgFileInBg);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(rthumb);
|
||||
}
|
||||
|
||||
const auto icon = &(_isAudio ? st::historyFileInPlay : _isImage ? st::historyFileInImage : st::historyFileInDocument);
|
||||
const auto icon = &(_isAudio
|
||||
? (_thumbw ? st::historyFileSongPlay : st::historyFileInPlay)
|
||||
: _isImage
|
||||
? st::historyFileInImage
|
||||
: st::historyFileInDocument);
|
||||
icon->paintInCenter(p, rthumb);
|
||||
}
|
||||
p.setFont(st::semiboldFont);
|
||||
|
||||
@@ -96,6 +96,8 @@ private:
|
||||
void createEditMediaButton();
|
||||
bool setPreparedList(Ui::PreparedList &&list);
|
||||
|
||||
bool isThumbedLayout() const;
|
||||
|
||||
inline QString getNewMediaPath() {
|
||||
return _preparedList.files.empty()
|
||||
? QString()
|
||||
|
||||
@@ -228,8 +228,8 @@ void EditColorBox::Picker::preparePaletteHSL() {
|
||||
}
|
||||
|
||||
void EditColorBox::Picker::updateCurrentPoint(QPoint localPosition) {
|
||||
auto x = snap(localPosition.x(), 0, width()) / float64(width());
|
||||
auto y = snap(localPosition.y(), 0, height()) / float64(height());
|
||||
auto x = std::clamp(localPosition.x(), 0, width()) / float64(width());
|
||||
auto y = std::clamp(localPosition.y(), 0, height()) / float64(height());
|
||||
if (_x != x || _y != y) {
|
||||
_x = x;
|
||||
_y = y;
|
||||
@@ -245,14 +245,14 @@ void EditColorBox::Picker::setHSB(HSB hsb) {
|
||||
_topright = _topright.toRgb();
|
||||
_bottomleft = _bottomright = QColor(0, 0, 0);
|
||||
|
||||
_x = snap(hsb.saturation / 255., 0., 1.);
|
||||
_y = 1. - snap(hsb.brightness / 255., 0., 1.);
|
||||
_x = std::clamp(hsb.saturation / 255., 0., 1.);
|
||||
_y = 1. - std::clamp(hsb.brightness / 255., 0., 1.);
|
||||
} else {
|
||||
_topleft = _topright = QColor::fromHsl(0, 255, hsb.brightness);
|
||||
_bottomleft = _bottomright = QColor::fromHsl(0, 0, hsb.brightness);
|
||||
|
||||
_x = snap(hsb.hue / 360., 0., 1.);
|
||||
_y = 1. - snap(hsb.saturation / 255., 0., 1.);
|
||||
_x = std::clamp(hsb.hue / 360., 0., 1.);
|
||||
_y = 1. - std::clamp(hsb.saturation / 255., 0., 1.);
|
||||
}
|
||||
|
||||
_paletteInvalidated = true;
|
||||
@@ -291,7 +291,7 @@ public:
|
||||
return _value;
|
||||
}
|
||||
void setValue(float64 value) {
|
||||
_value = snap(value, 0., 1.);
|
||||
_value = std::clamp(value, 0., 1.);
|
||||
update();
|
||||
}
|
||||
void setHSB(HSB hsb);
|
||||
@@ -508,12 +508,12 @@ float64 EditColorBox::Slider::valueFromColor(QColor color) const {
|
||||
}
|
||||
|
||||
float64 EditColorBox::Slider::valueFromHue(int hue) const {
|
||||
return (1. - snap(hue, 0, 360) / 360.);
|
||||
return (1. - std::clamp(hue, 0, 360) / 360.);
|
||||
}
|
||||
|
||||
void EditColorBox::Slider::setAlpha(int alpha) {
|
||||
if (_type == Type::Opacity) {
|
||||
_value = snap(alpha, 0, 255) / 255.;
|
||||
_value = std::clamp(alpha, 0, 255) / 255.;
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -534,7 +534,7 @@ void EditColorBox::Slider::updatePixmapFromMask() {
|
||||
void EditColorBox::Slider::updateCurrentPoint(QPoint localPosition) {
|
||||
auto coord = (isHorizontal() ? localPosition.x() : localPosition.y()) - st::colorSliderSkip;
|
||||
auto maximum = (isHorizontal() ? width() : height()) - 2 * st::colorSliderSkip;
|
||||
auto value = snap(coord, 0, maximum) / float64(maximum);
|
||||
auto value = std::clamp(coord, 0, maximum) / float64(maximum);
|
||||
if (_value != value) {
|
||||
_value = value;
|
||||
update();
|
||||
@@ -663,7 +663,7 @@ void EditColorBox::Field::wheelEvent(QWheelEvent *e) {
|
||||
|
||||
void EditColorBox::Field::changeValue(int delta) {
|
||||
auto currentValue = value();
|
||||
auto newValue = snap(currentValue + delta, 0, _limit);
|
||||
auto newValue = std::clamp(currentValue + delta, 0, _limit);
|
||||
if (newValue != currentValue) {
|
||||
setText(QString::number(newValue));
|
||||
setFocus();
|
||||
|
||||
@@ -66,10 +66,10 @@ private:
|
||||
[[nodiscard]] QColor applyLimits(QColor color) const;
|
||||
|
||||
int percentFromByte(int byte) {
|
||||
return snap(qRound(byte * 100 / 255.), 0, 100);
|
||||
return std::clamp(qRound(byte * 100 / 255.), 0, 100);
|
||||
}
|
||||
int percentToByte(int percent) {
|
||||
return snap(qRound(percent * 255 / 100.), 0, 255);
|
||||
return std::clamp(qRound(percent * 255 / 100.), 0, 255);
|
||||
}
|
||||
|
||||
class Picker;
|
||||
|
||||
@@ -62,22 +62,6 @@ public:
|
||||
|
||||
};
|
||||
|
||||
class TypeDelegate final : public PeerListContentDelegate {
|
||||
public:
|
||||
void peerListSetTitle(rpl::producer<QString> title) override;
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
|
||||
int peerListSelectedRowsCount() override;
|
||||
void peerListScrollToTop() override;
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override;
|
||||
void peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override;
|
||||
void peerListFinishSelectedRowsBunch() override;
|
||||
void peerListSetDescription(
|
||||
object_ptr<Ui::FlatLabel> description) override;
|
||||
|
||||
};
|
||||
|
||||
class TypeController final : public PeerListController {
|
||||
public:
|
||||
TypeController(
|
||||
@@ -194,39 +178,6 @@ PaintRoundImageCallback ExceptionRow::generatePaintUserpicCallback() {
|
||||
};
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListSetTitle(rpl::producer<QString> title) {
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListSetAdditionalTitle(rpl::producer<QString> title) {
|
||||
}
|
||||
|
||||
bool TypeDelegate::peerListIsRowChecked(not_null<PeerListRow*> row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int TypeDelegate::peerListSelectedRowsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListScrollToTop() {
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
|
||||
Unexpected("Item selection in Info::Profile::Members.");
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {
|
||||
Unexpected("Item selection in Info::Profile::Members.");
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListFinishSelectedRowsBunch() {
|
||||
}
|
||||
|
||||
void TypeDelegate::peerListSetDescription(
|
||||
object_ptr<Ui::FlatLabel> description) {
|
||||
description.destroy();
|
||||
}
|
||||
|
||||
TypeController::TypeController(
|
||||
not_null<Main::Session*> session,
|
||||
Flags options,
|
||||
@@ -412,7 +363,9 @@ object_ptr<Ui::RpWidget> EditFilterChatsListController::prepareTypesList() {
|
||||
container->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
container,
|
||||
st::membersMarginTop));
|
||||
const auto delegate = container->lifetime().make_state<TypeDelegate>();
|
||||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime().make_state<TypeController>(
|
||||
&session(),
|
||||
_options,
|
||||
|
||||
@@ -1440,7 +1440,10 @@ PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
|
||||
}
|
||||
|
||||
// Snap the index.
|
||||
newSelectedIndex = snap(newSelectedIndex, firstEnabled - 1, lastEnabled);
|
||||
newSelectedIndex = std::clamp(
|
||||
newSelectedIndex,
|
||||
firstEnabled - 1,
|
||||
lastEnabled);
|
||||
|
||||
// Skip the disabled rows.
|
||||
if (newSelectedIndex < firstEnabled) {
|
||||
|
||||
@@ -302,7 +302,7 @@ public:
|
||||
|
||||
virtual void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) = 0;
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) = 0;
|
||||
virtual int peerListSelectedRowsCount() = 0;
|
||||
virtual std::unique_ptr<PeerListState> peerListSaveState() const = 0;
|
||||
virtual void peerListRestoreState(
|
||||
@@ -375,6 +375,9 @@ public:
|
||||
_delegate = delegate;
|
||||
prepare();
|
||||
}
|
||||
[[nodiscard]] not_null<PeerListDelegate*> delegate() const {
|
||||
return _delegate;
|
||||
}
|
||||
|
||||
void setStyleOverrides(
|
||||
const style::PeerList *listSt,
|
||||
@@ -453,9 +456,6 @@ public:
|
||||
virtual ~PeerListController() = default;
|
||||
|
||||
protected:
|
||||
not_null<PeerListDelegate*> delegate() const {
|
||||
return _delegate;
|
||||
}
|
||||
PeerListSearchController *searchController() const {
|
||||
return _searchController.get();
|
||||
}
|
||||
@@ -837,7 +837,7 @@ public:
|
||||
}
|
||||
void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) override {
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override {
|
||||
_content->showRowMenu(row, std::move(destroyed));
|
||||
}
|
||||
|
||||
@@ -851,6 +851,38 @@ private:
|
||||
|
||||
};
|
||||
|
||||
class PeerListContentDelegateSimple : public PeerListContentDelegate {
|
||||
public:
|
||||
void peerListSetTitle(rpl::producer<QString> title) override {
|
||||
}
|
||||
void peerListSetAdditionalTitle(rpl::producer<QString> title) override {
|
||||
}
|
||||
bool peerListIsRowChecked(not_null<PeerListRow*> row) override {
|
||||
return false;
|
||||
}
|
||||
int peerListSelectedRowsCount() override {
|
||||
return 0;
|
||||
}
|
||||
void peerListScrollToTop() override {
|
||||
}
|
||||
void peerListAddSelectedPeerInBunch(
|
||||
not_null<PeerData*> peer) override {
|
||||
Unexpected("...DelegateSimple::peerListAddSelectedPeerInBunch");
|
||||
}
|
||||
void peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override {
|
||||
Unexpected("...DelegateSimple::peerListAddSelectedRowInBunch");
|
||||
}
|
||||
void peerListFinishSelectedRowsBunch() override {
|
||||
Unexpected("...DelegateSimple::peerListFinishSelectedRowsBunch");
|
||||
}
|
||||
void peerListSetDescription(
|
||||
object_ptr<Ui::FlatLabel> description) override {
|
||||
description.destroy();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
class PeerListBox
|
||||
: public Ui::BoxContent
|
||||
, public PeerListContentDelegate {
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
|
||||
#include "base/openssl_help.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/ui_utility.h"
|
||||
@@ -23,7 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lang/lang_keys.h"
|
||||
#include "history/history.h"
|
||||
#include "dialogs/dialogs_main_list.h"
|
||||
#include "window/window_session_controller.h" // onShowAddContact()
|
||||
#include "window/window_session_controller.h" // showAddContact()
|
||||
#include "facades.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_profile.h"
|
||||
@@ -35,7 +36,7 @@ void ShareBotGame(not_null<UserData*> bot, not_null<PeerData*> chat) {
|
||||
auto &histories = history->owner().histories();
|
||||
const auto requestType = Data::Histories::RequestType::Send;
|
||||
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
|
||||
const auto randomId = rand_value<uint64>();
|
||||
const auto randomId = openssl::RandomValue<uint64>();
|
||||
const auto api = &chat->session().api();
|
||||
history->sendRequestId = api->request(MTPmessages_SendMedia(
|
||||
MTP_flags(0),
|
||||
@@ -112,7 +113,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
|
||||
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
|
||||
box->addLeftButton(
|
||||
tr::lng_profile_add_contact(),
|
||||
[=] { controller->widget()->onShowAddContact(); });
|
||||
[=] { controller->widget()->showAddContact(); });
|
||||
};
|
||||
return Box<PeerListBox>(
|
||||
std::make_unique<ContactsBoxController>(
|
||||
|
||||
@@ -20,10 +20,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/boxes/calendar_box.h"
|
||||
#include "ui/special_buttons.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "settings/settings_privacy_security.h"
|
||||
#include "boxes/calendar_box.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "boxes/passcode_box.h"
|
||||
#include "boxes/peers/edit_peer_permissions_box.h"
|
||||
@@ -691,7 +691,7 @@ void EditRestrictedBox::showRestrictUntil() {
|
||||
: base::unixtime::parse(getRealUntilValue()).date();
|
||||
auto month = highlighted;
|
||||
_restrictUntilBox = Ui::show(
|
||||
Box<CalendarBox>(
|
||||
Box<Ui::CalendarBox>(
|
||||
month,
|
||||
highlighted,
|
||||
[this](const QDate &date) {
|
||||
|
||||
@@ -18,6 +18,7 @@ class LinkButton;
|
||||
class Checkbox;
|
||||
class Radiobutton;
|
||||
class RadiobuttonGroup;
|
||||
class CalendarBox;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
@@ -26,7 +27,6 @@ namespace Core {
|
||||
struct CloudPasswordResult;
|
||||
} // namespace Core
|
||||
|
||||
class CalendarBox;
|
||||
class PasscodeBox;
|
||||
|
||||
class EditParticipantBox : public Ui::BoxContent {
|
||||
@@ -162,7 +162,7 @@ private:
|
||||
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> _untilGroup;
|
||||
std::vector<base::unique_qptr<Ui::Radiobutton>> _untilVariants;
|
||||
QPointer<CalendarBox> _restrictUntilBox;
|
||||
QPointer<Ui::CalendarBox> _restrictUntilBox;
|
||||
|
||||
static constexpr auto kUntilOneDay = -1;
|
||||
static constexpr auto kUntilOneWeek = -2;
|
||||
|
||||
@@ -174,6 +174,7 @@ void SaveChatParticipantKick(
|
||||
Fn<void()> onDone,
|
||||
Fn<void()> onFail) {
|
||||
chat->session().api().request(MTPmessages_DeleteChatUser(
|
||||
MTP_flags(0),
|
||||
chat->inputChat,
|
||||
user->inputUser
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
@@ -358,7 +359,7 @@ bool ParticipantsAdditionalData::canRemoveUser(
|
||||
if (canRestrictUser(user)) {
|
||||
return true;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
return chat->invitedByMe.contains(user);
|
||||
return !user->isSelf() && chat->invitedByMe.contains(user);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1212,6 +1213,9 @@ void ParticipantsBoxController::rebuildChatAdmins(
|
||||
return true;
|
||||
}();
|
||||
if (same) {
|
||||
if (!_allLoaded && !delegate()->peerListFullRowsCount()) {
|
||||
chatListReady();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/peers/edit_peer_type_box.h"
|
||||
#include "boxes/peers/edit_peer_history_visibility_box.h"
|
||||
#include "boxes/peers/edit_peer_permissions_box.h"
|
||||
#include "boxes/peers/edit_peer_invite_links.h"
|
||||
#include "boxes/peers/edit_linked_chat_box.h"
|
||||
#include "boxes/stickers_box.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
@@ -25,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "history/admin_log/history_admin_log_section.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_keys.h"
|
||||
@@ -45,6 +47,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "window/window_session_controller.h"
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "app.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
@@ -236,6 +240,49 @@ void ShowEditPermissions(
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
void ShowEditInviteLinks(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer) {
|
||||
const auto box = Ui::show(
|
||||
Box<EditPeerPermissionsBox>(navigation, peer),
|
||||
Ui::LayerOption::KeepOther);
|
||||
const auto saving = box->lifetime().make_state<int>(0);
|
||||
const auto save = [=](
|
||||
not_null<PeerData*> peer,
|
||||
EditPeerPermissionsBox::Result result) {
|
||||
Expects(result.slowmodeSeconds == 0 || peer->isChannel());
|
||||
|
||||
const auto close = crl::guard(box, [=] { box->closeBox(); });
|
||||
SaveDefaultRestrictions(
|
||||
peer,
|
||||
MTP_chatBannedRights(MTP_flags(result.rights), MTP_int(0)),
|
||||
close);
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
SaveSlowmodeSeconds(channel, result.slowmodeSeconds, close);
|
||||
}
|
||||
};
|
||||
box->saveEvents(
|
||||
) | rpl::start_with_next([=](EditPeerPermissionsBox::Result result) {
|
||||
if (*saving) {
|
||||
return;
|
||||
}
|
||||
*saving = true;
|
||||
|
||||
const auto saveFor = peer->migrateToOrMe();
|
||||
const auto chat = saveFor->asChat();
|
||||
if (!result.slowmodeSeconds || !chat) {
|
||||
save(saveFor, result);
|
||||
return;
|
||||
}
|
||||
const auto api = &peer->session().api();
|
||||
api->migrateChat(chat, [=](not_null<ChannelData*> channel) {
|
||||
save(channel, result);
|
||||
}, [=](const RPCError &error) {
|
||||
*saving = false;
|
||||
});
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
@@ -285,7 +332,6 @@ private:
|
||||
void showEditLinkedChatBox();
|
||||
void fillPrivacyTypeButton();
|
||||
void fillLinkedChatButton();
|
||||
void fillInviteLinkButton();
|
||||
void fillSignaturesButton();
|
||||
void fillHistoryVisibilityButton();
|
||||
void fillManageSection();
|
||||
@@ -594,6 +640,8 @@ void Controller::refreshHistoryVisibility(anim::type animated) {
|
||||
|
||||
void Controller::showEditPeerTypeBox(
|
||||
std::optional<rpl::producer<QString>> error) {
|
||||
Expects(_privacySavedValue.has_value());
|
||||
|
||||
const auto boxCallback = crl::guard(this, [=](
|
||||
Privacy checked, QString publicLink) {
|
||||
_privacyTypeUpdates.fire(std::move(checked));
|
||||
@@ -606,7 +654,7 @@ void Controller::showEditPeerTypeBox(
|
||||
_peer,
|
||||
_channelHasLocationOriginalValue,
|
||||
boxCallback,
|
||||
_privacySavedValue,
|
||||
*_privacySavedValue,
|
||||
_usernameSavedValue,
|
||||
error),
|
||||
Ui::LayerOption::KeepOther);
|
||||
@@ -752,22 +800,9 @@ void Controller::fillLinkedChatButton() {
|
||||
_linkedChatUpdates.fire_copy(*_linkedChatSavedValue);
|
||||
}
|
||||
|
||||
void Controller::fillInviteLinkButton() {
|
||||
Expects(_controls.buttonsLayout != nullptr);
|
||||
|
||||
const auto buttonCallback = [=] {
|
||||
Ui::show(Box<EditPeerTypeBox>(_peer), Ui::LayerOption::KeepOther);
|
||||
};
|
||||
|
||||
AddButtonWithText(
|
||||
_controls.buttonsLayout,
|
||||
tr::lng_profile_invite_link_section(),
|
||||
rpl::single(QString()), //Empty text.
|
||||
buttonCallback);
|
||||
}
|
||||
|
||||
void Controller::fillSignaturesButton() {
|
||||
Expects(_controls.buttonsLayout != nullptr);
|
||||
|
||||
const auto channel = _peer->asChannel();
|
||||
if (!channel) return;
|
||||
|
||||
@@ -868,6 +903,11 @@ void Controller::fillManageSection() {
|
||||
? channel->canEditPermissions()
|
||||
: chat->canEditPermissions();
|
||||
}();
|
||||
const auto canEditInviteLinks = [&] {
|
||||
return isChannel
|
||||
? channel->canHaveInviteLink()
|
||||
: chat->canHaveInviteLink();
|
||||
}();
|
||||
const auto canViewAdmins = [&] {
|
||||
return isChannel
|
||||
? channel->canViewAdmins()
|
||||
@@ -913,8 +953,6 @@ void Controller::fillManageSection() {
|
||||
|
||||
if (canEditUsername) {
|
||||
fillPrivacyTypeButton();
|
||||
} else if (canEditInviteLink) {
|
||||
fillInviteLinkButton();
|
||||
}
|
||||
if (canViewOrEditLinkedChat) {
|
||||
fillLinkedChatButton();
|
||||
@@ -949,6 +987,29 @@ void Controller::fillManageSection() {
|
||||
[=] { ShowEditPermissions(_navigation, _peer); },
|
||||
st::infoIconPermissions);
|
||||
}
|
||||
//if (canEditInviteLinks) { // #TODO links
|
||||
// AddButtonWithCount(
|
||||
// _controls.buttonsLayout,
|
||||
// tr::lng_manage_peer_invite_links(),
|
||||
// Info::Profile::MigratedOrMeValue(
|
||||
// _peer
|
||||
// ) | rpl::map([=](not_null<PeerData*> peer) {
|
||||
// peer->session().api().inviteLinks().requestLinks(peer);
|
||||
// return peer->session().changes().peerUpdates(
|
||||
// peer,
|
||||
// Data::PeerUpdate::Flag::InviteLinks
|
||||
// ) | rpl::map([=] {
|
||||
// return peer->session().api().inviteLinks().links(
|
||||
// peer).count;
|
||||
// });
|
||||
// }) | rpl::flatten_latest(
|
||||
// ) | ToPositiveNumberString(),
|
||||
// [=] { Ui::show(
|
||||
// Box(ManageInviteLinksBox, _peer),
|
||||
// Ui::LayerOption::KeepOther);
|
||||
// },
|
||||
// st::infoIconInviteLinks);
|
||||
//}
|
||||
if (canViewAdmins) {
|
||||
AddButtonWithCount(
|
||||
_controls.buttonsLayout,
|
||||
|
||||
591
Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp
Normal file
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
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 "boxes/peers/edit_peer_invite_link.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "main/main_session.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "apiwrap.h"
|
||||
#include "ui/controls/invite_link_buttons.h"
|
||||
#include "ui/controls/invite_link_label.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "boxes/share_box.h"
|
||||
#include "history/view/history_view_group_call_tracker.h" // GenerateUs...
|
||||
#include "history/history_message.h" // GetErrorTextForSending.
|
||||
#include "history/history.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "mainwindow.h"
|
||||
#include "facades.h" // Ui::showPerProfile.
|
||||
#include "lang/lang_keys.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kFirstPage = 20;
|
||||
constexpr auto kPerPage = 100;
|
||||
|
||||
using LinkData = Api::InviteLink;
|
||||
|
||||
class Controller final : public PeerListController {
|
||||
public:
|
||||
Controller(not_null<PeerData*> peer, const LinkData &data);
|
||||
|
||||
void prepare() override;
|
||||
void loadMoreRows() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
Main::Session &session() const override;
|
||||
|
||||
private:
|
||||
void appendSlice(const Api::JoinedByLinkSlice &slice);
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> prepareHeader();
|
||||
|
||||
const not_null<PeerData*> _peer;
|
||||
LinkData _data;
|
||||
|
||||
mtpRequestId _requestId = 0;
|
||||
std::optional<Api::JoinedByLinkUser> _lastUser;
|
||||
bool _allLoaded = false;
|
||||
|
||||
MTP::Sender _api;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
class SingleRowController final : public PeerListController {
|
||||
public:
|
||||
SingleRowController(not_null<PeerData*> peer, TimeId date);
|
||||
|
||||
void prepare() override;
|
||||
void loadMoreRows() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
Main::Session &session() const override;
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
TimeId _date = 0;
|
||||
|
||||
};
|
||||
|
||||
void AddHeaderBlock(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<PeerData*> peer,
|
||||
const LinkData &data,
|
||||
TimeId now) {
|
||||
const auto link = data.link;
|
||||
const auto weak = Ui::MakeWeak(container);
|
||||
const auto copyLink = crl::guard(weak, [=] {
|
||||
CopyInviteLink(link);
|
||||
});
|
||||
const auto shareLink = crl::guard(weak, [=] {
|
||||
ShareInviteLinkBox(peer, link);
|
||||
});
|
||||
const auto revokeLink = crl::guard(weak, [=] {
|
||||
RevokeLink(peer, link);
|
||||
});
|
||||
|
||||
const auto createMenu = [=] {
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(container);
|
||||
result->addAction(
|
||||
tr::lng_group_invite_context_copy(tr::now),
|
||||
copyLink);
|
||||
result->addAction(
|
||||
tr::lng_group_invite_context_share(tr::now),
|
||||
shareLink);
|
||||
result->addAction(
|
||||
tr::lng_group_invite_context_revoke(tr::now),
|
||||
revokeLink);
|
||||
return result;
|
||||
};
|
||||
|
||||
const auto prefix = qstr("https://");
|
||||
const auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(
|
||||
container,
|
||||
rpl::single(link.startsWith(prefix)
|
||||
? link.mid(prefix.size())
|
||||
: link),
|
||||
createMenu);
|
||||
container->add(
|
||||
label->take(),
|
||||
st::inviteLinkFieldPadding);
|
||||
|
||||
label->clicks(
|
||||
) | rpl::start_with_next(copyLink, label->lifetime());
|
||||
|
||||
if ((data.expireDate <= 0 || now < data.expireDate)
|
||||
&& (data.usageLimit <= 0 || data.usage < data.usageLimit)) {
|
||||
AddCopyShareLinkButtons(
|
||||
container,
|
||||
copyLink,
|
||||
shareLink);
|
||||
}
|
||||
}
|
||||
|
||||
void AddHeader(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<PeerData*> peer,
|
||||
const LinkData &data) {
|
||||
using namespace Settings;
|
||||
|
||||
if (!data.revoked && !data.permanent) {
|
||||
const auto now = base::unixtime::now();
|
||||
AddHeaderBlock(container, peer, data, now);
|
||||
AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2);
|
||||
if (data.expireDate > 0) {
|
||||
AddDividerText(
|
||||
container,
|
||||
(data.expireDate > now
|
||||
? tr::lng_group_invite_expires_at(
|
||||
lt_when,
|
||||
rpl::single(langDateTime(
|
||||
base::unixtime::parse(data.expireDate))))
|
||||
: tr::lng_group_invite_expired_already()));
|
||||
} else {
|
||||
AddDivider(container);
|
||||
}
|
||||
}
|
||||
AddSkip(container);
|
||||
AddSubsectionTitle(
|
||||
container,
|
||||
tr::lng_group_invite_created_by());
|
||||
|
||||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime().make_state<
|
||||
SingleRowController
|
||||
>(data.admin, data.date);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
controller));
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
}
|
||||
|
||||
Controller::Controller(not_null<PeerData*> peer, const LinkData &data)
|
||||
: _peer(peer)
|
||||
, _data(data)
|
||||
, _api(&_peer->session().api().instance()) {
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> Controller::prepareHeader() {
|
||||
using namespace Settings;
|
||||
|
||||
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
|
||||
const auto container = result.data();
|
||||
AddHeader(container, _peer, _data);
|
||||
AddDivider(container);
|
||||
AddSkip(container);
|
||||
AddSubsectionTitle(
|
||||
container,
|
||||
(_data.usage
|
||||
? tr::lng_group_invite_joined(
|
||||
lt_count,
|
||||
rpl::single(float64(_data.usage)))
|
||||
: tr::lng_group_invite_no_joined()));
|
||||
return result;
|
||||
}
|
||||
|
||||
void Controller::prepare() {
|
||||
delegate()->peerListSetAboveWidget(prepareHeader());
|
||||
_allLoaded = (_data.usage == 0);
|
||||
const auto &inviteLinks = _peer->session().api().inviteLinks();
|
||||
const auto slice = inviteLinks.joinedFirstSliceLoaded(_peer, _data.link);
|
||||
if (slice) {
|
||||
appendSlice(*slice);
|
||||
}
|
||||
loadMoreRows();
|
||||
}
|
||||
|
||||
void Controller::loadMoreRows() {
|
||||
if (_requestId || _allLoaded) {
|
||||
return;
|
||||
}
|
||||
_allLoaded = true; // #TODO links
|
||||
//_requestId = _api.request(MTPmessages_GetChatInviteImporters(
|
||||
// _peer->input,
|
||||
// MTP_string(_data.link),
|
||||
// MTP_int(_lastUser ? _lastUser->date : 0),
|
||||
// _lastUser ? _lastUser->user->inputUser : MTP_inputUserEmpty(),
|
||||
// MTP_int(_lastUser ? kPerPage : kFirstPage)
|
||||
//)).done([=](const MTPmessages_ChatInviteImporters &result) {
|
||||
// _requestId = 0;
|
||||
// auto slice = Api::ParseJoinedByLinkSlice(_peer, result);
|
||||
// _allLoaded = slice.users.empty();
|
||||
// appendSlice(slice);
|
||||
//}).fail([=](const RPCError &error) {
|
||||
// _requestId = 0;
|
||||
// _allLoaded = true;
|
||||
//}).send();
|
||||
}
|
||||
|
||||
void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) {
|
||||
for (const auto &user : slice.users) {
|
||||
_lastUser = user;
|
||||
delegate()->peerListAppendRow(
|
||||
std::make_unique<PeerListRow>(user.user));
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void Controller::rowClicked(not_null<PeerListRow*> row) {
|
||||
Ui::showPeerProfile(row->peer());
|
||||
}
|
||||
|
||||
Main::Session &Controller::session() const {
|
||||
return _peer->session();
|
||||
}
|
||||
|
||||
SingleRowController::SingleRowController(
|
||||
not_null<PeerData*> peer,
|
||||
TimeId date)
|
||||
: _peer(peer)
|
||||
, _date(date) {
|
||||
}
|
||||
|
||||
void SingleRowController::prepare() {
|
||||
auto row = std::make_unique<PeerListRow>(_peer);
|
||||
row->setCustomStatus(langDateTime(base::unixtime::parse(_date)));
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void SingleRowController::loadMoreRows() {
|
||||
}
|
||||
|
||||
void SingleRowController::rowClicked(not_null<PeerListRow*> row) {
|
||||
Ui::showPeerProfile(row->peer());
|
||||
}
|
||||
|
||||
Main::Session &SingleRowController::session() const {
|
||||
return _peer->session();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AddPermanentLinkBlock(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<PeerData*> peer) {
|
||||
const auto computePermanentLink = [=] {
|
||||
const auto &links = peer->session().api().inviteLinks().links(
|
||||
peer).links;
|
||||
const auto link = links.empty() ? nullptr : &links.front();
|
||||
return (link && link->permanent && !link->revoked) ? link : nullptr;
|
||||
};
|
||||
auto value = peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::InviteLinks
|
||||
) | rpl::map([=] {
|
||||
const auto link = computePermanentLink();
|
||||
return link
|
||||
? std::make_tuple(link->link, link->usage)
|
||||
: std::make_tuple(QString(), 0);
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::start_spawning(container->lifetime());
|
||||
|
||||
const auto weak = Ui::MakeWeak(container);
|
||||
const auto copyLink = crl::guard(weak, [=] {
|
||||
if (const auto link = computePermanentLink()) {
|
||||
CopyInviteLink(link->link);
|
||||
}
|
||||
});
|
||||
const auto shareLink = crl::guard(weak, [=] {
|
||||
if (const auto link = computePermanentLink()) {
|
||||
ShareInviteLinkBox(peer, link->link);
|
||||
}
|
||||
});
|
||||
const auto revokeLink = crl::guard(weak, [=] {
|
||||
const auto box = std::make_shared<QPointer<ConfirmBox>>();
|
||||
const auto done = crl::guard(weak, [=] {
|
||||
const auto close = [=](auto&&) {
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
};
|
||||
peer->session().api().inviteLinks().revokePermanent(peer, close);
|
||||
});
|
||||
*box = Ui::show(
|
||||
Box<ConfirmBox>(tr::lng_group_invite_about_new(tr::now), done),
|
||||
Ui::LayerOption::KeepOther);
|
||||
});
|
||||
|
||||
auto link = rpl::duplicate(
|
||||
value
|
||||
) | rpl::map([=](QString link, int usage) {
|
||||
const auto prefix = qstr("https://");
|
||||
return link.startsWith(prefix) ? link.mid(prefix.size()) : link;
|
||||
});
|
||||
const auto createMenu = [=] {
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(container);
|
||||
result->addAction(
|
||||
tr::lng_group_invite_context_copy(tr::now),
|
||||
copyLink);
|
||||
result->addAction(
|
||||
tr::lng_group_invite_context_share(tr::now),
|
||||
shareLink);
|
||||
result->addAction(
|
||||
tr::lng_group_invite_context_revoke(tr::now),
|
||||
revokeLink);
|
||||
return result;
|
||||
};
|
||||
const auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(
|
||||
container,
|
||||
std::move(link),
|
||||
createMenu);
|
||||
container->add(
|
||||
label->take(),
|
||||
st::inviteLinkFieldPadding);
|
||||
|
||||
label->clicks(
|
||||
) | rpl::start_with_next(copyLink, label->lifetime());
|
||||
|
||||
AddCopyShareLinkButtons(
|
||||
container,
|
||||
copyLink,
|
||||
shareLink);
|
||||
|
||||
struct JoinedState {
|
||||
QImage cachedUserpics;
|
||||
std::vector<HistoryView::UserpicInRow> list;
|
||||
int count = 0;
|
||||
bool allUserpicsLoaded = false;
|
||||
rpl::variable<Ui::JoinedCountContent> content;
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
const auto state = container->lifetime().make_state<JoinedState>();
|
||||
const auto push = [=] {
|
||||
HistoryView::GenerateUserpicsInRow(
|
||||
state->cachedUserpics,
|
||||
state->list,
|
||||
st::inviteLinkUserpics,
|
||||
0);
|
||||
state->allUserpicsLoaded = ranges::all_of(
|
||||
state->list,
|
||||
[](const HistoryView::UserpicInRow &element) {
|
||||
return !element.peer->hasUserpic() || element.view->image();
|
||||
});
|
||||
state->content = Ui::JoinedCountContent{
|
||||
.count = state->count,
|
||||
.userpics = state->cachedUserpics
|
||||
};
|
||||
};
|
||||
std::move(
|
||||
value
|
||||
) | rpl::map([=](QString link, int usage) {
|
||||
return peer->session().api().inviteLinks().joinedFirstSliceValue(
|
||||
peer,
|
||||
link,
|
||||
usage);
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::start_with_next([=](const Api::JoinedByLinkSlice &slice) {
|
||||
auto list = std::vector<HistoryView::UserpicInRow>();
|
||||
list.reserve(slice.users.size());
|
||||
for (const auto &item : slice.users) {
|
||||
const auto i = ranges::find(
|
||||
state->list,
|
||||
item.user,
|
||||
&HistoryView::UserpicInRow::peer);
|
||||
if (i != end(state->list)) {
|
||||
list.push_back(std::move(*i));
|
||||
} else {
|
||||
list.push_back({ item.user });
|
||||
}
|
||||
}
|
||||
state->count = slice.count;
|
||||
state->list = std::move(list);
|
||||
push();
|
||||
}, state->lifetime);
|
||||
|
||||
peer->session().downloaderTaskFinished(
|
||||
) | rpl::filter([=] {
|
||||
return !state->allUserpicsLoaded;
|
||||
}) | rpl::start_with_next([=] {
|
||||
auto pushing = false;
|
||||
state->allUserpicsLoaded = true;
|
||||
for (const auto &element : state->list) {
|
||||
if (!element.peer->hasUserpic()) {
|
||||
continue;
|
||||
} else if (element.peer->userpicUniqueKey(element.view)
|
||||
!= element.uniqueKey) {
|
||||
pushing = true;
|
||||
} else if (!element.view->image()) {
|
||||
state->allUserpicsLoaded = false;
|
||||
}
|
||||
}
|
||||
if (pushing) {
|
||||
push();
|
||||
}
|
||||
}, state->lifetime);
|
||||
|
||||
Ui::AddJoinedCountButton(
|
||||
container,
|
||||
state->content.value(),
|
||||
st::inviteLinkJoinedRowPadding
|
||||
)->setClickedCallback([=] {
|
||||
});
|
||||
|
||||
container->add(object_ptr<Ui::SlideWrap<Ui::FixedHeightWidget>>(
|
||||
container,
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
container,
|
||||
st::inviteLinkJoinedRowPadding.bottom()))
|
||||
)->setDuration(0)->toggleOn(state->content.value(
|
||||
) | rpl::map([=](const Ui::JoinedCountContent &content) {
|
||||
return (content.count <= 0);
|
||||
}));
|
||||
}
|
||||
|
||||
void CopyInviteLink(const QString &link) {
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
Ui::Toast::Show(tr::lng_group_invite_copied(tr::now));
|
||||
}
|
||||
|
||||
void ShareInviteLinkBox(not_null<PeerData*> peer, const QString &link) {
|
||||
const auto session = &peer->session();
|
||||
const auto sending = std::make_shared<bool>();
|
||||
const auto box = std::make_shared<QPointer<ShareBox>>();
|
||||
|
||||
auto copyCallback = [=] {
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
Ui::Toast::Show(tr::lng_group_invite_copied(tr::now));
|
||||
};
|
||||
auto submitCallback = [=](
|
||||
std::vector<not_null<PeerData*>> &&result,
|
||||
TextWithTags &&comment,
|
||||
Api::SendOptions options) {
|
||||
if (*sending || result.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto error = [&] {
|
||||
for (const auto peer : result) {
|
||||
const auto error = GetErrorTextForSending(
|
||||
peer,
|
||||
{},
|
||||
comment);
|
||||
if (!error.isEmpty()) {
|
||||
return std::make_pair(error, peer);
|
||||
}
|
||||
}
|
||||
return std::make_pair(QString(), result.front());
|
||||
}();
|
||||
if (!error.first.isEmpty()) {
|
||||
auto text = TextWithEntities();
|
||||
if (result.size() > 1) {
|
||||
text.append(
|
||||
Ui::Text::Bold(error.second->name)
|
||||
).append("\n\n");
|
||||
}
|
||||
text.append(error.first);
|
||||
Ui::show(
|
||||
Box<InformBox>(text),
|
||||
Ui::LayerOption::KeepOther);
|
||||
return;
|
||||
}
|
||||
|
||||
*sending = true;
|
||||
if (!comment.text.isEmpty()) {
|
||||
comment.text = link + "\n" + comment.text;
|
||||
const auto add = link.size() + 1;
|
||||
for (auto &tag : comment.tags) {
|
||||
tag.offset += add;
|
||||
}
|
||||
}
|
||||
const auto owner = &peer->owner();
|
||||
auto &api = peer->session().api();
|
||||
auto &histories = owner->histories();
|
||||
const auto requestType = Data::Histories::RequestType::Send;
|
||||
for (const auto peer : result) {
|
||||
const auto history = owner->history(peer);
|
||||
auto message = ApiWrap::MessageToSend(history);
|
||||
message.textWithTags = comment;
|
||||
message.action.options = options;
|
||||
message.action.clearDraft = false;
|
||||
api.sendMessage(std::move(message));
|
||||
}
|
||||
Ui::Toast::Show(tr::lng_share_done(tr::now));
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
};
|
||||
auto filterCallback = [](PeerData *peer) {
|
||||
return peer->canWrite();
|
||||
};
|
||||
*box = Ui::show(
|
||||
Box<ShareBox>(
|
||||
App::wnd()->sessionController(),
|
||||
std::move(copyCallback),
|
||||
std::move(submitCallback),
|
||||
std::move(filterCallback)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void RevokeLink(not_null<PeerData*> peer, const QString &link) {
|
||||
const auto box = std::make_shared<QPointer<ConfirmBox>>();
|
||||
const auto revoke = [=] {
|
||||
const auto done = [=](const LinkData &data) {
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
};
|
||||
peer->session().api().inviteLinks().revoke(peer, link, done);
|
||||
};
|
||||
*box = Ui::show(
|
||||
Box<ConfirmBox>(
|
||||
tr::lng_group_invite_revoke_about(tr::now),
|
||||
revoke),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void ShowInviteLinkBox(
|
||||
not_null<PeerData*> peer,
|
||||
const Api::InviteLink &link) {
|
||||
auto initBox = [=](not_null<Ui::BoxContent*> box) {
|
||||
box->setTitle((link.permanent && !link.revoked)
|
||||
? tr::lng_manage_peer_link_permanent()
|
||||
: tr::lng_manage_peer_link_invite());
|
||||
peer->session().api().inviteLinks().updates(
|
||||
peer
|
||||
) | rpl::start_with_next([=](const Api::InviteLinkUpdate &update) {
|
||||
if (update.was == link.link
|
||||
&& (!update.now || (!link.revoked && update.now->revoked))) {
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
|
||||
};
|
||||
if (link.usage > 0) {
|
||||
Ui::show(
|
||||
Box<PeerListBox>(
|
||||
std::make_unique<Controller>(peer, link),
|
||||
std::move(initBox)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
} else {
|
||||
Ui::show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
initBox(box);
|
||||
const auto container = box->verticalLayout();
|
||||
AddHeader(container, peer, link);
|
||||
}), Ui::LayerOption::KeepOther);
|
||||
}
|
||||
}
|
||||
32
Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Api {
|
||||
struct InviteLink;
|
||||
} // namespace Api
|
||||
|
||||
namespace Ui {
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
void AddPermanentLinkBlock(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<PeerData*> peer);
|
||||
|
||||
void CopyInviteLink(const QString &link);
|
||||
void ShareInviteLinkBox(not_null<PeerData*> peer, const QString &link);
|
||||
void RevokeLink(not_null<PeerData*> peer, const QString &link);
|
||||
|
||||
void ShowInviteLinkBox(
|
||||
not_null<PeerData*> peer,
|
||||
const Api::InviteLink &link);
|
||||
776
Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp
Normal file
@@ -0,0 +1,776 @@
|
||||
/*
|
||||
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 "boxes/peers/edit_peer_invite_links.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "main/main_session.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "ui/boxes/edit_invite_link.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/peers/edit_peer_invite_link.h"
|
||||
#include "settings/settings_common.h" // AddDivider.
|
||||
#include "apiwrap.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h" // st::boxDividerLabel
|
||||
#include "styles/style_settings.h" // st::settingsDividerLabelPadding
|
||||
|
||||
#include <xxhash.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kPreloadPages = 2;
|
||||
constexpr auto kFullArcLength = 360 * 16;
|
||||
|
||||
enum class Color {
|
||||
Permanent,
|
||||
Expiring,
|
||||
ExpireSoon,
|
||||
Expired,
|
||||
Revoked,
|
||||
|
||||
Count,
|
||||
};
|
||||
|
||||
using InviteLinkData = Api::InviteLink;
|
||||
using InviteLinksSlice = Api::PeerInviteLinks;
|
||||
|
||||
struct InviteLinkAction {
|
||||
enum class Type {
|
||||
Copy,
|
||||
Share,
|
||||
Edit,
|
||||
Revoke,
|
||||
Delete,
|
||||
};
|
||||
QString link;
|
||||
Type type = Type::Copy;
|
||||
};
|
||||
|
||||
class Row;
|
||||
|
||||
class RowDelegate {
|
||||
public:
|
||||
virtual void rowUpdateRow(not_null<Row*> row) = 0;
|
||||
virtual void rowPaintIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size,
|
||||
float64 progress,
|
||||
Color color) = 0;
|
||||
};
|
||||
|
||||
class Row final : public PeerListRow {
|
||||
public:
|
||||
Row(
|
||||
not_null<RowDelegate*> delegate,
|
||||
const InviteLinkData &data,
|
||||
TimeId now);
|
||||
|
||||
void update(const InviteLinkData &data, TimeId now);
|
||||
void updateExpireProgress(TimeId now);
|
||||
|
||||
[[nodiscard]] InviteLinkData data() const;
|
||||
[[nodiscard]] crl::time updateExpireIn() const;
|
||||
|
||||
QString generateName() override;
|
||||
QString generateShortName() override;
|
||||
PaintRoundImageCallback generatePaintUserpicCallback() override;
|
||||
|
||||
QSize actionSize() const override;
|
||||
QMargins actionMargins() const override;
|
||||
void paintAction(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
private:
|
||||
const not_null<RowDelegate*> _delegate;
|
||||
InviteLinkData _data;
|
||||
QString _status;
|
||||
float64 _progressTillExpire = 0.;
|
||||
Color _color = Color::Permanent;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] uint64 ComputeRowId(const QString &link) {
|
||||
return XXH64(link.data(), link.size() * sizeof(ushort), 0);
|
||||
}
|
||||
|
||||
[[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) {
|
||||
return ComputeRowId(data.link);
|
||||
}
|
||||
|
||||
[[nodiscard]] float64 ComputeProgress(
|
||||
const InviteLinkData &link,
|
||||
TimeId now) {
|
||||
const auto startDate = link.startDate ? link.startDate : link.date;
|
||||
if (link.expireDate <= startDate && link.usageLimit <= 0) {
|
||||
return -1;
|
||||
}
|
||||
const auto expireProgress = (link.expireDate <= startDate
|
||||
|| now <= startDate)
|
||||
? 0.
|
||||
: (link.expireDate <= now)
|
||||
? 1.
|
||||
: (now - startDate) / float64(link.expireDate - startDate);
|
||||
const auto usageProgress = (link.usageLimit <= 0 || link.usage <= 0)
|
||||
? 0.
|
||||
: (link.usageLimit <= link.usage)
|
||||
? 1.
|
||||
: link.usage / float64(link.usageLimit);
|
||||
return std::max(expireProgress, usageProgress);
|
||||
}
|
||||
|
||||
[[nodiscard]] Color ComputeColor(
|
||||
const InviteLinkData &link,
|
||||
float64 progress) {
|
||||
return link.revoked
|
||||
? Color::Revoked
|
||||
: (progress >= 1.)
|
||||
? Color::Expired
|
||||
: (progress >= 3 / 4.)
|
||||
? Color::ExpireSoon
|
||||
: (progress >= 0.)
|
||||
? Color::Expiring
|
||||
: Color::Permanent;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ComputeStatus(const InviteLinkData &link, TimeId now) {
|
||||
auto result = link.usage
|
||||
? tr::lng_group_invite_joined(tr::now, lt_count_decimal, link.usage)
|
||||
: tr::lng_group_invite_no_joined(tr::now);
|
||||
const auto add = [&](const QString &text) {
|
||||
result += QString::fromUtf8(" \xE2\xB8\xB1 ") + text;
|
||||
};
|
||||
if (link.revoked) {
|
||||
add(tr::lng_group_invite_link_revoked(tr::now));
|
||||
} else if ((link.usageLimit > 0 && link.usage >= link.usageLimit)
|
||||
|| (link.expireDate > 0 && now >= link.expireDate)) {
|
||||
add(tr::lng_group_invite_link_expired(tr::now));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EditLink(not_null<PeerData*> peer, const InviteLinkData &data) {
|
||||
const auto creating = data.link.isEmpty();
|
||||
const auto box = std::make_shared<QPointer<Ui::GenericBox>>();
|
||||
using Fields = Ui::InviteLinkFields;
|
||||
const auto done = [=](Fields result) {
|
||||
const auto finish = [=](Api::InviteLink finished) {
|
||||
if (creating) {
|
||||
ShowInviteLinkBox(peer, finished);
|
||||
}
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
};
|
||||
if (creating) {
|
||||
peer->session().api().inviteLinks().create(
|
||||
peer,
|
||||
finish,
|
||||
result.expireDate,
|
||||
result.usageLimit);
|
||||
} else {
|
||||
peer->session().api().inviteLinks().edit(
|
||||
peer,
|
||||
result.link,
|
||||
result.expireDate,
|
||||
result.usageLimit,
|
||||
finish);
|
||||
}
|
||||
};
|
||||
*box = Ui::show(
|
||||
(creating
|
||||
? Box(Ui::CreateInviteLinkBox, done)
|
||||
: Box(
|
||||
Ui::EditInviteLinkBox,
|
||||
Fields{
|
||||
.link = data.link,
|
||||
.expireDate = data.expireDate,
|
||||
.usageLimit = data.usageLimit
|
||||
},
|
||||
done)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void DeleteLink(not_null<PeerData*> peer, const QString &link) {
|
||||
const auto box = std::make_shared<QPointer<ConfirmBox>>();
|
||||
const auto sure = [=] {
|
||||
const auto finish = [=] {
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
};
|
||||
peer->session().api().inviteLinks().destroy(peer, link, finish);
|
||||
};
|
||||
*box = Ui::show(
|
||||
Box<ConfirmBox>(tr::lng_group_invite_delete_sure(tr::now), sure),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void DeleteAllRevoked(not_null<PeerData*> peer) {
|
||||
const auto box = std::make_shared<QPointer<ConfirmBox>>();
|
||||
const auto sure = [=] {
|
||||
const auto finish = [=] {
|
||||
if (*box) {
|
||||
(*box)->closeBox();
|
||||
}
|
||||
};
|
||||
peer->session().api().inviteLinks().destroyAllRevoked(peer, finish);
|
||||
};
|
||||
*box = Ui::show(
|
||||
Box<ConfirmBox>(tr::lng_group_invite_delete_all_sure(tr::now), sure),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
not_null<Ui::SettingsButton*> AddCreateLinkButton(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
const auto result = container->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
tr::lng_group_invite_add(),
|
||||
st::inviteLinkCreate),
|
||||
style::margins(0, st::inviteLinkCreateSkip, 0, 0));
|
||||
const auto icon = Ui::CreateChild<Ui::RpWidget>(result);
|
||||
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
const auto size = st::inviteLinkCreateIconSize;
|
||||
icon->resize(size, size);
|
||||
result->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
const auto &st = st::inviteLinkList.item;
|
||||
icon->move(
|
||||
st.photoPosition.x() + (st.photoSize - size) / 2,
|
||||
(height - size) / 2);
|
||||
}, icon->lifetime());
|
||||
icon->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
auto p = QPainter(icon);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::windowBgActive);
|
||||
const auto rect = icon->rect();
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawEllipse(rect);
|
||||
st::inviteLinkCreateIcon.paintInCenter(p, rect);
|
||||
}, icon->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
Row::Row(
|
||||
not_null<RowDelegate*> delegate,
|
||||
const InviteLinkData &data,
|
||||
TimeId now)
|
||||
: PeerListRow(ComputeRowId(data))
|
||||
, _delegate(delegate)
|
||||
, _data(data)
|
||||
, _progressTillExpire(ComputeProgress(data, now))
|
||||
, _color(ComputeColor(data, _progressTillExpire)) {
|
||||
setCustomStatus(ComputeStatus(data, now));
|
||||
}
|
||||
|
||||
void Row::update(const InviteLinkData &data, TimeId now) {
|
||||
_data = data;
|
||||
_progressTillExpire = ComputeProgress(data, now);
|
||||
_color = ComputeColor(data, _progressTillExpire);
|
||||
setCustomStatus(ComputeStatus(data, now));
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
void Row::updateExpireProgress(TimeId now) {
|
||||
const auto updated = ComputeProgress(_data, now);
|
||||
if (std::round(_progressTillExpire * 360) != std::round(updated * 360)) {
|
||||
_progressTillExpire = updated;
|
||||
const auto color = ComputeColor(_data, _progressTillExpire);
|
||||
if (_color != color) {
|
||||
_color = color;
|
||||
setCustomStatus(ComputeStatus(_data, now));
|
||||
}
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
}
|
||||
|
||||
InviteLinkData Row::data() const {
|
||||
return _data;
|
||||
}
|
||||
|
||||
crl::time Row::updateExpireIn() const {
|
||||
if (_color != Color::Expiring && _color != Color::ExpireSoon) {
|
||||
return 0;
|
||||
}
|
||||
const auto start = _data.startDate ? _data.startDate : _data.date;
|
||||
if (_data.expireDate <= start) {
|
||||
return 0;
|
||||
}
|
||||
return std::round((_data.expireDate - start) * crl::time(1000) / 720.);
|
||||
}
|
||||
|
||||
QString Row::generateName() {
|
||||
auto result = _data.link;
|
||||
return result.replace(qstr("https://"), QString());
|
||||
}
|
||||
|
||||
QString Row::generateShortName() {
|
||||
return generateName();
|
||||
}
|
||||
|
||||
PaintRoundImageCallback Row::generatePaintUserpicCallback() {
|
||||
return [=](
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int size) {
|
||||
_delegate->rowPaintIcon(p, x, y, size, _progressTillExpire, _color);
|
||||
};
|
||||
}
|
||||
|
||||
QSize Row::actionSize() const {
|
||||
return QSize(
|
||||
st::inviteLinkThreeDotsIcon.width(),
|
||||
st::inviteLinkThreeDotsIcon.height());
|
||||
}
|
||||
|
||||
QMargins Row::actionMargins() const {
|
||||
return QMargins(
|
||||
0,
|
||||
(st::inviteLinkList.item.height - actionSize().height()) / 2,
|
||||
st::inviteLinkThreeDotsSkip,
|
||||
0);
|
||||
}
|
||||
|
||||
void Row::paintAction(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
(actionSelected
|
||||
? st::inviteLinkThreeDotsIconOver
|
||||
: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);
|
||||
}
|
||||
|
||||
class Controller final
|
||||
: public PeerListController
|
||||
, public RowDelegate
|
||||
, public base::has_weak_ptr {
|
||||
public:
|
||||
Controller(not_null<PeerData*> peer, bool revoked);
|
||||
|
||||
void prepare() override;
|
||||
void loadMoreRows() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
void rowActionClicked(not_null<PeerListRow*> row) override;
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override;
|
||||
Main::Session &session() const override;
|
||||
|
||||
void rowUpdateRow(not_null<Row*> row) override;
|
||||
void rowPaintIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size,
|
||||
float64 progress,
|
||||
Color color) override;
|
||||
|
||||
private:
|
||||
void appendRow(const InviteLinkData &data, TimeId now);
|
||||
void prependRow(const InviteLinkData &data, TimeId now);
|
||||
void updateRow(const InviteLinkData &data, TimeId now);
|
||||
bool removeRow(const QString &link);
|
||||
|
||||
void appendSlice(const InviteLinksSlice &slice);
|
||||
void checkExpiringTimer(not_null<Row*> row);
|
||||
void expiringProgressTimer();
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row);
|
||||
|
||||
const not_null<PeerData*> _peer;
|
||||
const bool _revoked = false;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
QString _offsetLink;
|
||||
TimeId _offsetDate = 0;
|
||||
bool _requesting = false;
|
||||
bool _allLoaded = false;
|
||||
|
||||
base::flat_set<not_null<Row*>> _expiringRows;
|
||||
base::Timer _updateExpiringTimer;
|
||||
|
||||
std::array<QImage, int(Color::Count)> _icons;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
Controller::Controller(not_null<PeerData*> peer, bool revoked)
|
||||
: _peer(peer)
|
||||
, _revoked(revoked)
|
||||
, _updateExpiringTimer([=] { expiringProgressTimer(); }) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &image : _icons) {
|
||||
image = QImage();
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
peer->session().api().inviteLinks().updates(
|
||||
peer
|
||||
) | rpl::start_with_next([=](const Api::InviteLinkUpdate &update) {
|
||||
const auto now = base::unixtime::now();
|
||||
if (!update.now || update.now->revoked != _revoked) {
|
||||
if (removeRow(update.was)) {
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
} else if (update.was.isEmpty()) {
|
||||
prependRow(*update.now, now);
|
||||
delegate()->peerListRefreshRows();
|
||||
} else {
|
||||
updateRow(*update.now, now);
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
if (_revoked) {
|
||||
peer->session().api().inviteLinks().allRevokedDestroyed(
|
||||
peer
|
||||
) | rpl::start_with_next([=] {
|
||||
_requesting = false;
|
||||
_allLoaded = true;
|
||||
while (delegate()->peerListFullRowsCount()) {
|
||||
delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}, _lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::prepare() {
|
||||
if (!_revoked) {
|
||||
appendSlice(_peer->session().api().inviteLinks().links(_peer));
|
||||
}
|
||||
if (!delegate()->peerListFullRowsCount()) {
|
||||
loadMoreRows();
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::loadMoreRows() {
|
||||
if (_requesting || _allLoaded) {
|
||||
return;
|
||||
}
|
||||
_requesting = true;
|
||||
const auto done = [=](const InviteLinksSlice &slice) {
|
||||
if (!_requesting) {
|
||||
return;
|
||||
}
|
||||
_requesting = false;
|
||||
if (slice.links.empty()) {
|
||||
_allLoaded = true;
|
||||
return;
|
||||
}
|
||||
appendSlice(slice);
|
||||
};
|
||||
_peer->session().api().inviteLinks().requestMoreLinks(
|
||||
_peer,
|
||||
_offsetDate,
|
||||
_offsetLink,
|
||||
_revoked,
|
||||
crl::guard(this, done));
|
||||
}
|
||||
|
||||
void Controller::appendSlice(const InviteLinksSlice &slice) {
|
||||
const auto now = base::unixtime::now();
|
||||
for (const auto &link : slice.links) {
|
||||
if (!link.permanent || link.revoked) {
|
||||
appendRow(link, now);
|
||||
}
|
||||
_offsetLink = link.link;
|
||||
_offsetDate = link.date;
|
||||
}
|
||||
if (slice.links.size() >= slice.count) {
|
||||
_allLoaded = true;
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void Controller::rowClicked(not_null<PeerListRow*> row) {
|
||||
ShowInviteLinkBox(_peer, static_cast<Row*>(row.get())->data());
|
||||
}
|
||||
|
||||
void Controller::rowActionClicked(not_null<PeerListRow*> row) {
|
||||
delegate()->peerListShowRowMenu(row, nullptr);
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> Controller::rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
auto result = createRowContextMenu(parent, row);
|
||||
|
||||
if (result) {
|
||||
// First clear _menu value, so that we don't check row positions yet.
|
||||
base::take(_menu);
|
||||
|
||||
// Here unique_qptr is used like a shared pointer, where
|
||||
// not the last destroyed pointer destroys the object, but the first.
|
||||
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> Controller::createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
const auto real = static_cast<Row*>(row.get());
|
||||
const auto data = real->data();
|
||||
const auto link = data.link;
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(parent);
|
||||
if (data.revoked) {
|
||||
result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {
|
||||
DeleteLink(_peer, link);
|
||||
});
|
||||
} else {
|
||||
result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {
|
||||
CopyInviteLink(link);
|
||||
});
|
||||
result->addAction(tr::lng_group_invite_context_share(tr::now), [=] {
|
||||
ShareInviteLinkBox(_peer, link);
|
||||
});
|
||||
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
|
||||
EditLink(_peer, data);
|
||||
});
|
||||
result->addAction(tr::lng_group_invite_context_revoke(tr::now), [=] {
|
||||
RevokeLink(_peer, link);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Main::Session &Controller::session() const {
|
||||
return _peer->session();
|
||||
}
|
||||
|
||||
void Controller::appendRow(const InviteLinkData &data, TimeId now) {
|
||||
delegate()->peerListAppendRow(std::make_unique<Row>(this, data, now));
|
||||
}
|
||||
|
||||
void Controller::prependRow(const InviteLinkData &data, TimeId now) {
|
||||
delegate()->peerListPrependRow(std::make_unique<Row>(this, data, now));
|
||||
}
|
||||
|
||||
void Controller::updateRow(const InviteLinkData &data, TimeId now) {
|
||||
if (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {
|
||||
const auto real = static_cast<Row*>(row);
|
||||
real->update(data, now);
|
||||
checkExpiringTimer(real);
|
||||
delegate()->peerListUpdateRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::removeRow(const QString &link) {
|
||||
if (const auto row = delegate()->peerListFindRow(ComputeRowId(link))) {
|
||||
delegate()->peerListRemoveRow(row);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Controller::checkExpiringTimer(not_null<Row*> row) {
|
||||
const auto updateIn = row->updateExpireIn();
|
||||
if (updateIn > 0) {
|
||||
_expiringRows.emplace(row);
|
||||
if (!_updateExpiringTimer.isActive()
|
||||
|| updateIn < _updateExpiringTimer.remainingTime()) {
|
||||
_updateExpiringTimer.callOnce(updateIn);
|
||||
}
|
||||
} else {
|
||||
_expiringRows.remove(row);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::expiringProgressTimer() {
|
||||
const auto now = base::unixtime::now();
|
||||
auto minimalIn = 0;
|
||||
for (auto i = begin(_expiringRows); i != end(_expiringRows);) {
|
||||
(*i)->updateExpireProgress(now);
|
||||
const auto updateIn = (*i)->updateExpireIn();
|
||||
if (!updateIn) {
|
||||
i = _expiringRows.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
if (!minimalIn || minimalIn > updateIn) {
|
||||
minimalIn = updateIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minimalIn) {
|
||||
_updateExpiringTimer.callOnce(minimalIn);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::rowUpdateRow(not_null<Row*> row) {
|
||||
delegate()->peerListUpdateRow(row);
|
||||
}
|
||||
|
||||
void Controller::rowPaintIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int size,
|
||||
float64 progress,
|
||||
Color color) {
|
||||
const auto skip = st::inviteLinkIconSkip;
|
||||
const auto inner = size - 2 * skip;
|
||||
const auto bg = [&] {
|
||||
switch (color) {
|
||||
case Color::Permanent: return &st::msgFile1Bg;
|
||||
case Color::Expiring: return &st::msgFile2Bg;
|
||||
case Color::ExpireSoon: return &st::msgFile4Bg;
|
||||
case Color::Expired: return &st::msgFile3Bg;
|
||||
case Color::Revoked: return &st::windowSubTextFg;
|
||||
}
|
||||
Unexpected("Color in Controller::rowPaintIcon.");
|
||||
}();
|
||||
auto &icon = _icons[int(color)];
|
||||
if (icon.isNull()) {
|
||||
icon = QImage(
|
||||
QSize(inner, inner) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
icon.fill(Qt::transparent);
|
||||
icon.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
|
||||
auto p = QPainter(&icon);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(*bg);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawEllipse(0, 0, inner, inner);
|
||||
st::inviteLinkIcon.paintInCenter(p, { 0, 0, inner, inner });
|
||||
}
|
||||
p.drawImage(x + skip, y + skip, icon);
|
||||
if (progress >= 0. && progress < 1.) {
|
||||
const auto stroke = st::inviteLinkIconStroke;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = QPen((*bg)->c);
|
||||
pen.setWidth(stroke);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
p.setPen(pen);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
|
||||
const auto margins = 1.5 * stroke;
|
||||
p.drawArc(QRectF(x + skip, y + skip, inner, inner).marginsAdded({
|
||||
margins,
|
||||
margins,
|
||||
margins,
|
||||
margins,
|
||||
}), (kFullArcLength / 4), kFullArcLength * (1. - progress));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
not_null<Ui::RpWidget*> AddLinksList(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<PeerData*> peer,
|
||||
bool revoked) {
|
||||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime().make_state<Controller>(
|
||||
peer,
|
||||
revoked);
|
||||
controller->setStyleOverrides(&st::inviteLinkList);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
controller));
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
void ManageInviteLinksBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<PeerData*> peer) {
|
||||
using namespace Settings;
|
||||
|
||||
box->setTitle(tr::lng_group_invite_title());
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
AddSubsectionTitle(container, tr::lng_create_permanent_link_title());
|
||||
AddPermanentLinkBlock(container, peer);
|
||||
AddDivider(container);
|
||||
|
||||
const auto add = AddCreateLinkButton(container);
|
||||
add->setClickedCallback([=] {
|
||||
EditLink(peer, InviteLinkData{ .admin = peer->session().user() });
|
||||
});
|
||||
|
||||
const auto list = AddLinksList(container, peer, false);
|
||||
const auto dividerAbout = container->add(object_ptr<Ui::SlideWrap<>>(
|
||||
container,
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
container,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_group_invite_add_about(),
|
||||
st::boxDividerLabel),
|
||||
st::settingsDividerLabelPadding)),
|
||||
style::margins(0, st::inviteLinkCreateSkip, 0, 0));
|
||||
const auto divider = container->add(object_ptr<Ui::SlideWrap<>>(
|
||||
container,
|
||||
object_ptr<Ui::BoxContentDivider>(container)));
|
||||
const auto header = container->add(object_ptr<Ui::SlideWrap<>>(
|
||||
container,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_group_invite_revoked_title(),
|
||||
st::settingsSubsectionTitle),
|
||||
st::inviteLinkRevokedTitlePadding));
|
||||
const auto revoked = AddLinksList(container, peer, true);
|
||||
|
||||
const auto deleteAll = Ui::CreateChild<Ui::LinkButton>(
|
||||
container.get(),
|
||||
tr::lng_group_invite_context_delete_all(tr::now),
|
||||
st::defaultLinkButton);
|
||||
rpl::combine(
|
||||
header->topValue(),
|
||||
container->widthValue()
|
||||
) | rpl::start_with_next([=](int top, int outerWidth) {
|
||||
deleteAll->moveToRight(
|
||||
st::inviteLinkRevokedTitlePadding.left(),
|
||||
top + st::inviteLinkRevokedTitlePadding.top(),
|
||||
outerWidth);
|
||||
}, deleteAll->lifetime());
|
||||
deleteAll->setClickedCallback([=] {
|
||||
DeleteAllRevoked(peer);
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
list->heightValue(),
|
||||
revoked->heightValue()
|
||||
) | rpl::start_with_next([=](int list, int revoked) {
|
||||
dividerAbout->toggle(!list, anim::type::instant);
|
||||
divider->toggle(list > 0 && revoked > 0, anim::type::instant);
|
||||
header->toggle(revoked > 0, anim::type::instant);
|
||||
deleteAll->setVisible(revoked > 0);
|
||||
}, header->lifetime());
|
||||
|
||||
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
|
||||
}
|
||||
@@ -7,8 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#pragma warning(push)
|
||||
// class has virtual functions, but destructor is not virtual
|
||||
#pragma warning(disable:4265)
|
||||
#include <wrl/implements.h>
|
||||
#pragma warning(pop)
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
void ManageInviteLinksBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<PeerData*> peer);
|
||||
@@ -8,11 +8,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/peers/edit_peer_type_box.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "main/main_session.h"
|
||||
#include "boxes/add_contact_box.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/peers/edit_participants_box.h"
|
||||
#include "boxes/peers/edit_peer_info_box.h" // CreateButton.
|
||||
#include "boxes/peers/edit_peer_invite_link.h"
|
||||
#include "boxes/peers/edit_peer_invite_links.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "core/application.h"
|
||||
#include "data/data_channel.h"
|
||||
@@ -31,15 +35,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/box_content_divider.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/special_fields.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
@@ -57,7 +64,7 @@ public:
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<PeerData*> peer,
|
||||
bool useLocationPhrases,
|
||||
std::optional<Privacy> privacySavedValue,
|
||||
Privacy privacySavedValue,
|
||||
std::optional<QString> usernameSavedValue);
|
||||
|
||||
void createContent();
|
||||
@@ -65,17 +72,11 @@ public:
|
||||
void setFocusUsername();
|
||||
|
||||
rpl::producer<QString> getTitle() {
|
||||
return _isInviteLink
|
||||
? tr::lng_profile_invite_link_section()
|
||||
: _isGroup
|
||||
return _isGroup
|
||||
? tr::lng_manage_peer_group_type()
|
||||
: tr::lng_manage_peer_channel_type();
|
||||
}
|
||||
|
||||
bool isInviteLink() {
|
||||
return _isInviteLink;
|
||||
}
|
||||
|
||||
bool isAllowSave() {
|
||||
return _isAllowSave;
|
||||
}
|
||||
@@ -97,19 +98,14 @@ private:
|
||||
base::unique_qptr<Ui::FlatLabel> usernameResult;
|
||||
const style::FlatLabel *usernameResultStyle = nullptr;
|
||||
|
||||
Ui::SlideWrap<Ui::RpWidget> *createInviteLinkWrap = nullptr;
|
||||
Ui::SlideWrap<Ui::RpWidget> *editInviteLinkWrap = nullptr;
|
||||
Ui::SlideWrap<Ui::RpWidget> *inviteLinkWrap = nullptr;
|
||||
Ui::FlatLabel *inviteLink = nullptr;
|
||||
};
|
||||
|
||||
Controls _controls;
|
||||
|
||||
object_ptr<Ui::RpWidget> createPrivaciesEdit();
|
||||
object_ptr<Ui::RpWidget> createUsernameEdit();
|
||||
object_ptr<Ui::RpWidget> createInviteLinkCreate();
|
||||
object_ptr<Ui::RpWidget> createInviteLinkEdit();
|
||||
|
||||
void observeInviteLink();
|
||||
object_ptr<Ui::RpWidget> createInviteLinkBlock();
|
||||
|
||||
void privacyChanged(Privacy value);
|
||||
|
||||
@@ -122,16 +118,9 @@ private:
|
||||
rpl::producer<QString> &&text,
|
||||
not_null<const style::FlatLabel*> st);
|
||||
|
||||
bool canEditInviteLink() const;
|
||||
void refreshEditInviteLink();
|
||||
void refreshCreateInviteLink();
|
||||
void createInviteLink();
|
||||
void revokeInviteLink();
|
||||
void exportInviteLink(const QString &confirmation);
|
||||
|
||||
void fillPrivaciesButtons(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
std::optional<Privacy> savedValue = std::nullopt);
|
||||
Privacy savedValue);
|
||||
void addRoundButton(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
Privacy value,
|
||||
@@ -143,12 +132,11 @@ private:
|
||||
|
||||
not_null<PeerData*> _peer;
|
||||
MTP::Sender _api;
|
||||
std::optional<Privacy> _privacySavedValue;
|
||||
Privacy _privacySavedValue = Privacy();
|
||||
std::optional<QString> _usernameSavedValue;
|
||||
|
||||
bool _useLocationPhrases = false;
|
||||
bool _isGroup = false;
|
||||
bool _isInviteLink = false;
|
||||
bool _isAllowSave = false;
|
||||
|
||||
base::unique_qptr<Ui::VerticalLayout> _wrap;
|
||||
@@ -165,7 +153,7 @@ Controller::Controller(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<PeerData*> peer,
|
||||
bool useLocationPhrases,
|
||||
std::optional<Privacy> privacySavedValue,
|
||||
Privacy privacySavedValue,
|
||||
std::optional<QString> usernameSavedValue)
|
||||
: _peer(peer)
|
||||
, _api(&_peer->session().mtp())
|
||||
@@ -173,8 +161,6 @@ Controller::Controller(
|
||||
, _usernameSavedValue(usernameSavedValue)
|
||||
, _useLocationPhrases(useLocationPhrases)
|
||||
, _isGroup(_peer->isChat() || _peer->isMegagroup())
|
||||
, _isInviteLink(!_privacySavedValue.has_value()
|
||||
&& !_usernameSavedValue.has_value())
|
||||
, _isAllowSave(!_usernameSavedValue.value_or(QString()).isEmpty())
|
||||
, _wrap(container)
|
||||
, _checkUsernameTimer([=] { checkUsernameAvailability(); }) {
|
||||
@@ -184,23 +170,37 @@ Controller::Controller(
|
||||
void Controller::createContent() {
|
||||
_controls = Controls();
|
||||
|
||||
if (_isInviteLink) {
|
||||
_wrap->add(createInviteLinkCreate());
|
||||
_wrap->add(createInviteLinkEdit());
|
||||
return;
|
||||
}
|
||||
|
||||
fillPrivaciesButtons(_wrap, _privacySavedValue);
|
||||
// Skip.
|
||||
_wrap->add(object_ptr<Ui::BoxContentDivider>(_wrap));
|
||||
//
|
||||
_wrap->add(createInviteLinkCreate());
|
||||
_wrap->add(createInviteLinkEdit());
|
||||
_wrap->add(createInviteLinkBlock());
|
||||
_wrap->add(createUsernameEdit());
|
||||
|
||||
//using namespace Settings; // #TODO links
|
||||
//AddSkip(_wrap.get());
|
||||
//_wrap->add(EditPeerInfoBox::CreateButton(
|
||||
// _wrap.get(),
|
||||
// tr::lng_group_invite_manage(),
|
||||
// rpl::single(QString()),
|
||||
// [=] { Ui::show(
|
||||
// Box(ManageInviteLinksBox, _peer),
|
||||
// Ui::LayerOption::KeepOther);
|
||||
// },
|
||||
// st::manageGroupButton,
|
||||
// &st::infoIconInviteLinks));
|
||||
//AddSkip(_wrap.get());
|
||||
//AddDividerText(_wrap.get(), tr::lng_group_invite_manage_about());
|
||||
|
||||
if (_controls.privacy->value() == Privacy::NoUsername) {
|
||||
checkUsernameAvailability();
|
||||
}
|
||||
_controls.inviteLinkWrap->toggle(
|
||||
(_privacySavedValue != Privacy::HasUsername),
|
||||
anim::type::instant);
|
||||
_controls.usernameWrap->toggle(
|
||||
(_privacySavedValue == Privacy::HasUsername),
|
||||
anim::type::instant);
|
||||
}
|
||||
|
||||
void Controller::addRoundButton(
|
||||
@@ -228,7 +228,7 @@ void Controller::addRoundButton(
|
||||
|
||||
void Controller::fillPrivaciesButtons(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
std::optional<Privacy> savedValue) {
|
||||
Privacy savedValue) {
|
||||
const auto canEditUsername = [&] {
|
||||
if (const auto chat = _peer->asChat()) {
|
||||
return chat->canEditUsername();
|
||||
@@ -251,8 +251,7 @@ void Controller::fillPrivaciesButtons(
|
||||
const auto isPublic = _peer->isChannel()
|
||||
&& _peer->asChannel()->hasUsername();
|
||||
_controls.privacy = std::make_shared<Ui::RadioenumGroup<Privacy>>(
|
||||
savedValue.value_or(
|
||||
isPublic ? Privacy::HasUsername : Privacy::NoUsername));
|
||||
savedValue);
|
||||
|
||||
addRoundButton(
|
||||
container,
|
||||
@@ -312,21 +311,23 @@ object_ptr<Ui::RpWidget> Controller::createUsernameEdit() {
|
||||
|
||||
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_wrap,
|
||||
object_ptr<Ui::VerticalLayout>(_wrap),
|
||||
st::editPeerUsernameMargins);
|
||||
object_ptr<Ui::VerticalLayout>(_wrap));
|
||||
_controls.usernameWrap = result.data();
|
||||
|
||||
const auto container = result->entity();
|
||||
container->add(object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
container,
|
||||
|
||||
using namespace Settings;
|
||||
AddSkip(container);
|
||||
container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_create_group_link(),
|
||||
st::editPeerSectionLabel),
|
||||
st::editPeerUsernameTitleLabelMargins));
|
||||
st::settingsSubsectionTitle),
|
||||
st::settingsSubsectionTitlePadding);
|
||||
|
||||
const auto placeholder = container->add(object_ptr<Ui::RpWidget>(
|
||||
container));
|
||||
const auto placeholder = container->add(
|
||||
object_ptr<Ui::RpWidget>(container),
|
||||
st::editPeerUsernameFieldMargins);
|
||||
placeholder->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
_controls.usernameInput = Ui::AttachParentChild(
|
||||
container,
|
||||
@@ -348,13 +349,9 @@ object_ptr<Ui::RpWidget> Controller::createUsernameEdit() {
|
||||
}, placeholder->lifetime());
|
||||
_controls.usernameInput->move(placeholder->pos());
|
||||
|
||||
container->add(object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
AddDividerText(
|
||||
container,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_create_channel_link_about(),
|
||||
st::editPeerPrivacyLabel),
|
||||
st::editPeerUsernameAboutLabelMargins));
|
||||
tr::lng_create_channel_link_about());
|
||||
|
||||
QObject::connect(
|
||||
_controls.usernameInput,
|
||||
@@ -368,6 +365,11 @@ object_ptr<Ui::RpWidget> Controller::createUsernameEdit() {
|
||||
}
|
||||
|
||||
void Controller::privacyChanged(Privacy value) {
|
||||
const auto toggleInviteLink = [&] {
|
||||
_controls.inviteLinkWrap->toggle(
|
||||
(value != Privacy::HasUsername),
|
||||
anim::type::instant);
|
||||
};
|
||||
const auto toggleEditUsername = [&] {
|
||||
_controls.usernameWrap->toggle(
|
||||
(value == Privacy::HasUsername),
|
||||
@@ -378,16 +380,14 @@ void Controller::privacyChanged(Privacy value) {
|
||||
// Otherwise box will change own Y position.
|
||||
|
||||
if (value == Privacy::HasUsername) {
|
||||
refreshCreateInviteLink();
|
||||
refreshEditInviteLink();
|
||||
toggleInviteLink();
|
||||
toggleEditUsername();
|
||||
|
||||
_controls.usernameResult = nullptr;
|
||||
checkUsernameAvailability();
|
||||
} else {
|
||||
toggleEditUsername();
|
||||
refreshCreateInviteLink();
|
||||
refreshEditInviteLink();
|
||||
toggleInviteLink();
|
||||
}
|
||||
};
|
||||
if (value == Privacy::HasUsername) {
|
||||
@@ -535,187 +535,57 @@ void Controller::showUsernameResult(
|
||||
_usernameResultTexts.fire(std::move(text));
|
||||
}
|
||||
|
||||
void Controller::createInviteLink() {
|
||||
exportInviteLink((_isGroup
|
||||
? tr::lng_group_invite_about
|
||||
: tr::lng_group_invite_about_channel)(tr::now));
|
||||
}
|
||||
|
||||
void Controller::revokeInviteLink() {
|
||||
exportInviteLink(tr::lng_group_invite_about_new(tr::now));
|
||||
}
|
||||
|
||||
void Controller::exportInviteLink(const QString &confirmation) {
|
||||
const auto callback = crl::guard(this, [=](Fn<void()> &&close) {
|
||||
close();
|
||||
_peer->session().api().exportInviteLink(_peer->migrateToOrMe());
|
||||
});
|
||||
auto box = Box<ConfirmBox>(
|
||||
confirmation,
|
||||
std::move(callback));
|
||||
Ui::show(std::move(box), Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
bool Controller::canEditInviteLink() const {
|
||||
if (const auto channel = _peer->asChannel()) {
|
||||
return channel->amCreator()
|
||||
|| (channel->adminRights() & ChatAdminRight::f_invite_users);
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
return chat->amCreator()
|
||||
|| (chat->adminRights() & ChatAdminRight::f_invite_users);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Controller::observeInviteLink() {
|
||||
if (!_controls.editInviteLinkWrap) {
|
||||
return;
|
||||
}
|
||||
_peer->session().changes().peerFlagsValue(
|
||||
_peer,
|
||||
Data::PeerUpdate::Flag::InviteLink
|
||||
) | rpl::start_with_next([=] {
|
||||
refreshCreateInviteLink();
|
||||
refreshEditInviteLink();
|
||||
}, _controls.editInviteLinkWrap->lifetime());
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> Controller::createInviteLinkEdit() {
|
||||
object_ptr<Ui::RpWidget> Controller::createInviteLinkBlock() {
|
||||
Expects(_wrap != nullptr);
|
||||
|
||||
if (!canEditInviteLink()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_wrap,
|
||||
object_ptr<Ui::VerticalLayout>(_wrap),
|
||||
st::editPeerInvitesMargins);
|
||||
_controls.editInviteLinkWrap = result.data();
|
||||
object_ptr<Ui::VerticalLayout>(_wrap));
|
||||
_controls.inviteLinkWrap = result.data();
|
||||
|
||||
const auto container = result->entity();
|
||||
if (!_isInviteLink) {
|
||||
container->add(object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_profile_invite_link_section(),
|
||||
st::editPeerSectionLabel));
|
||||
container->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
container,
|
||||
st::editPeerInviteLinkBoxBottomSkip));
|
||||
}
|
||||
|
||||
_controls.inviteLink = container->add(object_ptr<Ui::FlatLabel>(
|
||||
using namespace Settings;
|
||||
AddSkip(container);
|
||||
|
||||
AddSubsectionTitle(container, tr::lng_create_invite_link_title());
|
||||
// tr::lng_create_permanent_link_title()); // #TODO links
|
||||
AddPermanentLinkBlock(container, _peer);
|
||||
|
||||
AddSkip(container);
|
||||
|
||||
AddDividerText(
|
||||
container,
|
||||
st::editPeerInviteLink));
|
||||
_controls.inviteLink->setSelectable(true);
|
||||
_controls.inviteLink->setContextCopyText(QString());
|
||||
_controls.inviteLink->setBreakEverywhere(true);
|
||||
_controls.inviteLink->setClickHandlerFilter([=](auto&&...) {
|
||||
QGuiApplication::clipboard()->setText(inviteLinkText());
|
||||
Ui::Toast::Show(tr::lng_group_invite_copied(tr::now));
|
||||
return false;
|
||||
});
|
||||
((_peer->isMegagroup() || _peer->asChat())
|
||||
? tr::lng_group_invite_about_permanent_group()
|
||||
: tr::lng_group_invite_about_permanent_channel()));
|
||||
|
||||
container->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
container,
|
||||
st::editPeerInviteLinkSkip));
|
||||
container->add(object_ptr<Ui::LinkButton>(
|
||||
container,
|
||||
tr::lng_group_invite_create_new(tr::now),
|
||||
st::editPeerInviteLinkButton)
|
||||
)->addClickHandler([=] { revokeInviteLink(); });
|
||||
|
||||
observeInviteLink();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Controller::refreshEditInviteLink() {
|
||||
const auto link = inviteLinkText();
|
||||
auto text = TextWithEntities();
|
||||
if (!link.isEmpty()) {
|
||||
text.text = link;
|
||||
const auto remove = qstr("https://");
|
||||
if (text.text.startsWith(remove)) {
|
||||
text.text.remove(0, remove.size());
|
||||
if (_peer->wasFullUpdated()) {
|
||||
const auto link = _peer->isChat()
|
||||
? _peer->asChat()->inviteLink()
|
||||
: _peer->asChannel()->inviteLink();
|
||||
if (link.isEmpty()) {
|
||||
// #TODO links remove this auto-export link.
|
||||
_peer->session().api().inviteLinks().revokePermanent(_peer);
|
||||
}
|
||||
text.entities.push_back({
|
||||
EntityType::CustomUrl,
|
||||
0,
|
||||
text.text.size(),
|
||||
link });
|
||||
}
|
||||
_controls.inviteLink->setMarkedText(text);
|
||||
|
||||
// Hack to expand FlatLabel width to naturalWidth again.
|
||||
_controls.editInviteLinkWrap->resizeToWidth(st::boxWideWidth);
|
||||
|
||||
_controls.editInviteLinkWrap->toggle(
|
||||
inviteLinkShown() && !link.isEmpty(),
|
||||
anim::type::instant);
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> Controller::createInviteLinkCreate() {
|
||||
Expects(_wrap != nullptr);
|
||||
|
||||
if (!canEditInviteLink()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_wrap,
|
||||
object_ptr<Ui::VerticalLayout>(_wrap),
|
||||
st::editPeerInvitesMargins);
|
||||
const auto container = result->entity();
|
||||
|
||||
if (!_isInviteLink) {
|
||||
container->add(object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_profile_invite_link_section(),
|
||||
st::editPeerSectionLabel));
|
||||
container->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
container,
|
||||
st::editPeerInviteLinkSkip));
|
||||
}
|
||||
|
||||
container->add(object_ptr<Ui::LinkButton>(
|
||||
_wrap,
|
||||
tr::lng_group_invite_create(tr::now),
|
||||
st::editPeerInviteLinkButton)
|
||||
)->addClickHandler([this] {
|
||||
createInviteLink();
|
||||
});
|
||||
_controls.createInviteLinkWrap = result.data();
|
||||
|
||||
observeInviteLink();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Controller::refreshCreateInviteLink() {
|
||||
_controls.createInviteLinkWrap->toggle(
|
||||
inviteLinkShown() && inviteLinkText().isEmpty(),
|
||||
anim::type::instant);
|
||||
}
|
||||
|
||||
bool Controller::inviteLinkShown() {
|
||||
return !_controls.privacy
|
||||
|| (_controls.privacy->value() == Privacy::NoUsername)
|
||||
|| _isInviteLink;
|
||||
|| (_controls.privacy->value() == Privacy::NoUsername);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditPeerTypeBox::EditPeerTypeBox(QWidget*, not_null<PeerData*> peer)
|
||||
: EditPeerTypeBox(nullptr, peer, false, {}, {}, {}, {}) {
|
||||
}
|
||||
|
||||
EditPeerTypeBox::EditPeerTypeBox(
|
||||
QWidget*,
|
||||
not_null<PeerData*> peer,
|
||||
bool useLocationPhrases,
|
||||
std::optional<FnMut<void(Privacy, QString)>> savedCallback,
|
||||
std::optional<Privacy> privacySaved,
|
||||
Privacy privacySaved,
|
||||
std::optional<QString> usernameSaved,
|
||||
std::optional<rpl::producer<QString>> usernameError)
|
||||
: _peer(peer)
|
||||
@@ -733,11 +603,11 @@ void EditPeerTypeBox::setInnerFocus() {
|
||||
void EditPeerTypeBox::prepare() {
|
||||
_peer->updateFull();
|
||||
|
||||
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
||||
auto content = object_ptr<Ui::VerticalLayout>(this);
|
||||
|
||||
const auto controller = Ui::CreateChild<Controller>(
|
||||
this,
|
||||
content,
|
||||
content.data(),
|
||||
_peer,
|
||||
_useLocationPhrases,
|
||||
_privacySavedValue,
|
||||
@@ -756,7 +626,7 @@ void EditPeerTypeBox::prepare() {
|
||||
|
||||
setTitle(controller->getTitle());
|
||||
|
||||
if (!controller->isInviteLink() && _savedCallback.has_value()) {
|
||||
if (_savedCallback.has_value()) {
|
||||
addButton(tr::lng_settings_save(), [=] {
|
||||
const auto v = controller->getPrivacy();
|
||||
if (!controller->isAllowSave() && (v == Privacy::HasUsername)) {
|
||||
@@ -768,13 +638,12 @@ void EditPeerTypeBox::prepare() {
|
||||
local(v,
|
||||
(v == Privacy::HasUsername)
|
||||
? controller->getUsernameInput()
|
||||
: QString()); // We dont need username with private type.
|
||||
: QString()); // We don't need username with private type.
|
||||
closeBox();
|
||||
});
|
||||
}
|
||||
addButton(
|
||||
controller->isInviteLink() ? tr::lng_close() : tr::lng_cancel(),
|
||||
[=] { closeBox(); });
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
setDimensionsToContent(st::boxWideWidth, content);
|
||||
setDimensionsToContent(st::boxWideWidth, content.data());
|
||||
setInnerWidget(std::move(content));
|
||||
}
|
||||
|
||||
@@ -32,15 +32,12 @@ enum class UsernameState {
|
||||
|
||||
class EditPeerTypeBox : public Ui::BoxContent {
|
||||
public:
|
||||
// Edit just the invite link.
|
||||
EditPeerTypeBox(QWidget*, not_null<PeerData*> peer);
|
||||
|
||||
EditPeerTypeBox(
|
||||
QWidget*,
|
||||
not_null<PeerData*> peer,
|
||||
bool useLocationPhrases,
|
||||
std::optional<FnMut<void(Privacy, QString)>> savedCallback,
|
||||
std::optional<Privacy> privacySaved,
|
||||
Privacy privacySaved,
|
||||
std::optional<QString> usernameSaved,
|
||||
std::optional<rpl::producer<QString>> usernameError = {});
|
||||
|
||||
@@ -53,7 +50,7 @@ private:
|
||||
bool _useLocationPhrases = false;
|
||||
std::optional<FnMut<void(Privacy, QString)>> _savedCallback;
|
||||
|
||||
std::optional<Privacy> _privacySavedValue;
|
||||
Privacy _privacySavedValue = Privacy();
|
||||
std::optional<QString> _usernameSavedValue;
|
||||
std::optional<rpl::producer<QString>> _usernameError;
|
||||
|
||||
|
||||
@@ -73,10 +73,9 @@ void ReportBox::prepare() {
|
||||
st::defaultBoxCheckbox);
|
||||
};
|
||||
createButton(_reasonSpam, Reason::Spam, tr::lng_report_reason_spam(tr::now));
|
||||
createButton(_reasonFake, Reason::Fake, tr::lng_report_reason_fake(tr::now));
|
||||
createButton(_reasonViolence, Reason::Violence, tr::lng_report_reason_violence(tr::now));
|
||||
if (_ids) {
|
||||
createButton(_reasonChildAbuse, Reason::ChildAbuse, tr::lng_report_reason_child_abuse(tr::now));
|
||||
}
|
||||
createButton(_reasonChildAbuse, Reason::ChildAbuse, tr::lng_report_reason_child_abuse(tr::now));
|
||||
createButton(_reasonPornography, Reason::Pornography, tr::lng_report_reason_pornography(tr::now));
|
||||
createButton(_reasonOther, Reason::Other, tr::lng_report_reason_other(tr::now));
|
||||
_reasonGroup->setChangedCallback([=](Reason value) {
|
||||
@@ -90,14 +89,10 @@ void ReportBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
_reasonSpam->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), st::boxOptionListPadding.top() + _reasonSpam->getMargins().top());
|
||||
_reasonViolence->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonSpam->bottomNoMargins() + st::boxOptionListSkip);
|
||||
if (_ids) {
|
||||
_reasonChildAbuse->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonViolence->bottomNoMargins() + st::boxOptionListSkip);
|
||||
_reasonPornography->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonChildAbuse->bottomNoMargins() + st::boxOptionListSkip);
|
||||
}
|
||||
else{
|
||||
_reasonPornography->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonViolence->bottomNoMargins() + st::boxOptionListSkip);
|
||||
}
|
||||
_reasonFake->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonSpam->bottomNoMargins() + st::boxOptionListSkip);
|
||||
_reasonViolence->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonFake->bottomNoMargins() + st::boxOptionListSkip);
|
||||
_reasonChildAbuse->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonViolence->bottomNoMargins() + st::boxOptionListSkip);
|
||||
_reasonPornography->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonChildAbuse->bottomNoMargins() + st::boxOptionListSkip);
|
||||
_reasonOther->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), _reasonPornography->bottomNoMargins() + st::boxOptionListSkip);
|
||||
|
||||
if (_reasonOtherText) {
|
||||
@@ -156,6 +151,7 @@ void ReportBox::report() {
|
||||
const auto reason = [&] {
|
||||
switch (_reasonGroup->value()) {
|
||||
case Reason::Spam: return MTP_inputReportReasonSpam();
|
||||
case Reason::Fake: return MTP_inputReportReasonFake();
|
||||
case Reason::Violence: return MTP_inputReportReasonViolence();
|
||||
case Reason::ChildAbuse: return MTP_inputReportReasonChildAbuse();
|
||||
case Reason::Pornography: return MTP_inputReportReasonPornography();
|
||||
@@ -203,7 +199,7 @@ void ReportBox::reportFail(const RPCError &error) {
|
||||
}
|
||||
|
||||
void ReportBox::updateMaxHeight() {
|
||||
const auto buttonsCount = _ids ? 5 : 4;
|
||||
const auto buttonsCount = 6;
|
||||
auto newHeight = st::boxOptionListPadding.top() + _reasonSpam->getMargins().top() + buttonsCount * _reasonSpam->heightNoMargins() + (buttonsCount - 1) * st::boxOptionListSkip + _reasonSpam->getMargins().bottom() + st::boxOptionListPadding.bottom();
|
||||
|
||||
if (_reasonOtherText) {
|
||||
|
||||
@@ -37,6 +37,7 @@ protected:
|
||||
private:
|
||||
enum class Reason {
|
||||
Spam,
|
||||
Fake,
|
||||
Violence,
|
||||
ChildAbuse,
|
||||
Pornography,
|
||||
@@ -56,6 +57,7 @@ private:
|
||||
|
||||
std::shared_ptr<Ui::RadioenumGroup<Reason>> _reasonGroup;
|
||||
object_ptr<Ui::Radioenum<Reason>> _reasonSpam = { nullptr };
|
||||
object_ptr<Ui::Radioenum<Reason>> _reasonFake = { nullptr };
|
||||
object_ptr<Ui::Radioenum<Reason>> _reasonViolence = { nullptr };
|
||||
object_ptr<Ui::Radioenum<Reason>> _reasonChildAbuse = { nullptr };
|
||||
object_ptr<Ui::Radioenum<Reason>> _reasonPornography = { nullptr };
|
||||
|
||||
@@ -778,6 +778,47 @@ groupCallMajorBlobMaxRadius: 4px;
|
||||
groupCallMinorBlobIdleRadius: 3px;
|
||||
groupCallMinorBlobMaxRadius: 12px;
|
||||
|
||||
groupCallMuteCrossLine: CrossLineAnimation {
|
||||
fg: groupCallIconFg;
|
||||
icon: icon {{ "calls/volume/speaker", groupCallIconFg }};
|
||||
startPosition: point(2px, 5px);
|
||||
endPosition: point(16px, 19px);
|
||||
stroke: 2px;
|
||||
}
|
||||
|
||||
groupCallMenuSpeakerArcsSkip: 1px;
|
||||
groupCallMenuVolumeSkip: 5px;
|
||||
groupCallMenuVolumeSlider: MediaSlider(defaultContinuousSlider) {
|
||||
activeFg: groupCallMembersFg;
|
||||
inactiveFg: groupCallMemberInactiveIcon;
|
||||
activeFgOver: groupCallMembersFg;
|
||||
inactiveFgOver: groupCallMemberInactiveIcon;
|
||||
activeFgDisabled: groupCallMemberInactiveIcon;
|
||||
receivedTillFg: groupCallMemberInactiveIcon;
|
||||
}
|
||||
|
||||
groupCallSpeakerArcsAnimation: ArcsAnimation {
|
||||
fg: groupCallIconFg;
|
||||
stroke: 2px;
|
||||
space: 4px;
|
||||
duration: 200;
|
||||
deltaAngle: 60;
|
||||
deltaHeight: 6px;
|
||||
deltaWidth: 7px;
|
||||
startHeight: 3px;
|
||||
startWidth: 0px;
|
||||
}
|
||||
|
||||
groupCallStatusSpeakerIcon: icon {{ "calls/volume/speaker_small", groupCallIconFg }};
|
||||
groupCallStatusSpeakerArcsSkip: 3px;
|
||||
groupCallStatusSpeakerArcsAnimation: ArcsAnimation(groupCallSpeakerArcsAnimation) {
|
||||
deltaAngle: 68;
|
||||
space: 3px;
|
||||
deltaHeight: 5px;
|
||||
deltaWidth: 4px;
|
||||
startHeight: 1px;
|
||||
}
|
||||
|
||||
callTopBarMuteCrossLine: CrossLineAnimation {
|
||||
fg: callBarFg;
|
||||
icon: icon {{ "calls/call_record_active", callBarFg }};
|
||||
|
||||
@@ -7,10 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_box_controller.h"
|
||||
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "core/application.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "history/history.h"
|
||||
@@ -22,7 +23,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_user.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "api/api_updates.h"
|
||||
#include "app.h"
|
||||
#include "apiwrap.h"
|
||||
#include "styles/style_layers.h" // st::boxLabel.
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
@@ -49,6 +57,7 @@ public:
|
||||
|
||||
bool canAddItem(not_null<const HistoryItem*> item) const {
|
||||
return (ComputeType(item) == _type)
|
||||
&& (!hasItems() || _items.front()->history() == item->history())
|
||||
&& (ItemDateTime(item).date() == _date);
|
||||
}
|
||||
void addItem(not_null<HistoryItem*> item) {
|
||||
@@ -66,20 +75,26 @@ public:
|
||||
refreshStatus();
|
||||
}
|
||||
}
|
||||
bool hasItems() const {
|
||||
[[nodiscard]] bool hasItems() const {
|
||||
return !_items.empty();
|
||||
}
|
||||
|
||||
MsgId minItemId() const {
|
||||
[[nodiscard]] MsgId minItemId() const {
|
||||
Expects(hasItems());
|
||||
|
||||
return _items.back()->id;
|
||||
}
|
||||
|
||||
MsgId maxItemId() const {
|
||||
[[nodiscard]] MsgId maxItemId() const {
|
||||
Expects(hasItems());
|
||||
|
||||
return _items.front()->id;
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::vector<not_null<HistoryItem*>> &items() const {
|
||||
return _items;
|
||||
}
|
||||
|
||||
void paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
@@ -333,6 +348,22 @@ void BoxController::loadMoreRows() {
|
||||
}).send();
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> BoxController::rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
const auto &items = static_cast<Row*>(row.get())->items();
|
||||
const auto session = &this->session();
|
||||
const auto ids = session->data().itemsToIds(items);
|
||||
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(parent);
|
||||
result->addAction(tr::lng_context_delete_selected(tr::now), [=] {
|
||||
Ui::show(
|
||||
Box<DeleteMessagesBox>(session, base::duplicate(ids)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void BoxController::refreshAbout() {
|
||||
setDescriptionText(delegate()->peerListFullRowsCount() ? QString() : tr::lng_call_box_about(tr::now));
|
||||
}
|
||||
@@ -448,4 +479,64 @@ std::unique_ptr<PeerListRow> BoxController::createRow(
|
||||
return std::make_unique<Row>(item);
|
||||
}
|
||||
|
||||
void ClearCallsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window) {
|
||||
const auto weak = Ui::MakeWeak(box);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_call_box_clear_sure(),
|
||||
st::boxLabel),
|
||||
st::boxPadding);
|
||||
const auto revokeCheckbox = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box,
|
||||
tr::lng_delete_for_everyone_check(tr::now),
|
||||
false,
|
||||
st::defaultBoxCheckbox),
|
||||
style::margins(
|
||||
st::boxPadding.left(),
|
||||
st::boxPadding.bottom(),
|
||||
st::boxPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
|
||||
const auto api = &window->session().api();
|
||||
const auto sendRequest = [=](bool revoke, auto self) -> void {
|
||||
using Flag = MTPmessages_DeletePhoneCallHistory::Flag;
|
||||
api->request(MTPmessages_DeletePhoneCallHistory(
|
||||
MTP_flags(revoke ? Flag::f_revoke : Flag(0))
|
||||
)).done([=](const MTPmessages_AffectedFoundMessages &result) {
|
||||
result.match([&](
|
||||
const MTPDmessages_affectedFoundMessages &data) {
|
||||
api->applyUpdates(MTP_updates(
|
||||
MTP_vector<MTPUpdate>(
|
||||
1,
|
||||
MTP_updateDeleteMessages(
|
||||
data.vmessages(),
|
||||
data.vpts(),
|
||||
data.vpts_count())),
|
||||
MTP_vector<MTPUser>(),
|
||||
MTP_vector<MTPChat>(),
|
||||
MTP_int(base::unixtime::now()),
|
||||
MTP_int(0)));
|
||||
const auto offset = data.voffset().v;
|
||||
if (offset > 0) {
|
||||
self(revoke, self);
|
||||
} else {
|
||||
api->session().data().destroyAllCallItems();
|
||||
if (const auto strong = weak.data()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).send();
|
||||
};
|
||||
|
||||
box->addButton(tr::lng_call_box_clear_button(), [=] {
|
||||
sendRequest(revokeCheckbox->checked(), sendRequest);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
@@ -25,6 +26,10 @@ public:
|
||||
void rowActionClicked(not_null<PeerListRow*> row) override;
|
||||
void loadMoreRows() override;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override;
|
||||
|
||||
private:
|
||||
void receivedCalls(const QVector<MTPMessage> &result);
|
||||
void refreshAbout();
|
||||
@@ -49,4 +54,8 @@ private:
|
||||
|
||||
};
|
||||
|
||||
void ClearCallsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window);
|
||||
|
||||
} // namespace Calls
|
||||
|
||||
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "calls/calls_panel.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "webrtc/webrtc_media_devices.h"
|
||||
#include "webrtc/webrtc_create_adm.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "facades.h"
|
||||
@@ -231,7 +232,7 @@ void Call::startOutgoing() {
|
||||
_api.request(MTPphone_RequestCall(
|
||||
MTP_flags(flags),
|
||||
_user->inputUser,
|
||||
MTP_int(rand_value<int32>()),
|
||||
MTP_int(openssl::RandomValue<int32>()),
|
||||
MTP_bytes(_gaHash),
|
||||
MTP_phoneCallProtocol(
|
||||
MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p
|
||||
@@ -779,6 +780,8 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
|
||||
sendSignalingData(bytes);
|
||||
});
|
||||
},
|
||||
.createAudioDeviceModule = Webrtc::AudioDeviceModuleCreator(
|
||||
settings.callAudioBackend()),
|
||||
};
|
||||
if (Logs::DebugEnabled()) {
|
||||
auto callLogFolder = cWorkingDir() + qsl("DebugLogs");
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_group_call.h"
|
||||
|
||||
#include "calls/calls_group_common.h"
|
||||
#include "main/main_session.h"
|
||||
#include "api/api_send_progress.h"
|
||||
#include "apiwrap.h"
|
||||
@@ -24,7 +25,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_session.h"
|
||||
#include "base/global_shortcuts.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "webrtc/webrtc_media_devices.h"
|
||||
#include "webrtc/webrtc_create_adm.h"
|
||||
|
||||
#include <tgcalls/group/GroupInstanceImpl.h>
|
||||
|
||||
@@ -53,6 +56,22 @@ constexpr auto kPlayConnectingEach = crl::time(1056) + 2 * crl::time(1000);
|
||||
settings.callVideoInputDeviceId());
|
||||
}
|
||||
|
||||
[[nodiscard]] const Data::GroupCall::Participant *LookupParticipant(
|
||||
not_null<PeerData*> chat,
|
||||
uint64 id,
|
||||
not_null<UserData*> user) {
|
||||
const auto call = chat->groupCall();
|
||||
if (!id || !call || call->id() != id) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto &participants = call->participants();
|
||||
const auto i = ranges::find(
|
||||
participants,
|
||||
user,
|
||||
&Data::GroupCall::Participant::user);
|
||||
return (i != end(participants)) ? &*i : nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GroupCall::GroupCall(
|
||||
@@ -200,7 +219,7 @@ void GroupCall::playConnectingSoundOnce() {
|
||||
void GroupCall::start() {
|
||||
_createRequestId = _api.request(MTPphone_CreateGroupCall(
|
||||
_peer->input,
|
||||
MTP_int(rand_value<int32>())
|
||||
MTP_int(openssl::RandomValue<int32>())
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
_acceptFields = true;
|
||||
_peer->session().api().applyUpdates(result);
|
||||
@@ -236,11 +255,25 @@ void GroupCall::join(const MTPInputGroupCall &inputCall) {
|
||||
using Update = Data::GroupCall::ParticipantUpdate;
|
||||
_peer->groupCall()->participantUpdated(
|
||||
) | rpl::filter([=](const Update &update) {
|
||||
return (_instance != nullptr) && !update.now;
|
||||
return (_instance != nullptr);
|
||||
}) | rpl::start_with_next([=](const Update &update) {
|
||||
Expects(update.was.has_value());
|
||||
|
||||
_instance->removeSsrcs({ update.was->ssrc });
|
||||
if (!update.now) {
|
||||
_instance->removeSsrcs({ update.was->ssrc });
|
||||
} else {
|
||||
const auto &now = *update.now;
|
||||
const auto &was = update.was;
|
||||
const auto volumeChanged = was
|
||||
? (was->volume != now.volume || was->mutedByMe != now.mutedByMe)
|
||||
: (now.volume != Group::kDefaultVolume || now.mutedByMe);
|
||||
if (volumeChanged) {
|
||||
_instance->setVolume(
|
||||
now.ssrc,
|
||||
(now.mutedByMe
|
||||
? 0.
|
||||
: (now.volume
|
||||
/ float64(Group::kDefaultVolume))));
|
||||
}
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
SubscribeToMigration(_peer, _lifetime, [=](not_null<ChannelData*> group) {
|
||||
@@ -359,7 +392,42 @@ void GroupCall::applySelfInCallLocally() {
|
||||
MTP_int(self->bareId()),
|
||||
MTP_int(date),
|
||||
MTP_int(lastActive),
|
||||
MTP_int(_mySsrc))),
|
||||
MTP_int(_mySsrc),
|
||||
MTP_int(Group::kDefaultVolume))), // volume
|
||||
MTP_int(0)).c_updateGroupCallParticipants());
|
||||
}
|
||||
|
||||
void GroupCall::applyParticipantLocally(
|
||||
not_null<UserData*> user,
|
||||
bool mute,
|
||||
std::optional<int> volume) {
|
||||
const auto participant = LookupParticipant(_peer, _id, user);
|
||||
if (!participant || !participant->ssrc) {
|
||||
return;
|
||||
}
|
||||
const auto canSelfUnmute = participant->canSelfUnmute;
|
||||
const auto mutedCount = 0/*participant->mutedCount*/;
|
||||
using Flag = MTPDgroupCallParticipant::Flag;
|
||||
const auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0))
|
||||
| Flag::f_volume // Without flag the volume is reset to 100%.
|
||||
| (participant->lastActive ? Flag::f_active_date : Flag(0))
|
||||
| (!mute
|
||||
? Flag(0)
|
||||
: _peer->canManageGroupCall()
|
||||
? Flag::f_muted
|
||||
: Flag::f_muted_by_you);
|
||||
_peer->groupCall()->applyUpdateChecked(
|
||||
MTP_updateGroupCallParticipants(
|
||||
inputCall(),
|
||||
MTP_vector<MTPGroupCallParticipant>(
|
||||
1,
|
||||
MTP_groupCallParticipant(
|
||||
MTP_flags(flags),
|
||||
MTP_int(user->bareId()),
|
||||
MTP_int(participant->date),
|
||||
MTP_int(participant->lastActive),
|
||||
MTP_int(participant->ssrc),
|
||||
MTP_int(volume.value_or(participant->volume)))),
|
||||
MTP_int(0)).c_updateGroupCallParticipants());
|
||||
}
|
||||
|
||||
@@ -525,10 +593,25 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto handleOtherParticipants = [=](
|
||||
const MTPDgroupCallParticipant &data) {
|
||||
const auto user = _peer->owner().user(data.vuser_id().v);
|
||||
const auto participant = LookupParticipant(_peer, _id, user);
|
||||
if (!participant) {
|
||||
return;
|
||||
}
|
||||
_otherParticipantStateValue.fire(Group::ParticipantState{
|
||||
.user = user,
|
||||
.volume = data.vvolume().value_or_empty(),
|
||||
.mutedByMe = data.is_muted_by_you(),
|
||||
});
|
||||
};
|
||||
|
||||
const auto self = _peer->session().userId();
|
||||
for (const auto &participant : data.vparticipants().v) {
|
||||
participant.match([&](const MTPDgroupCallParticipant &data) {
|
||||
if (data.vuser_id().v != self) {
|
||||
handleOtherParticipants(data);
|
||||
return;
|
||||
}
|
||||
if (data.is_left() && data.vsource().v == _mySsrc) {
|
||||
@@ -548,6 +631,8 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) {
|
||||
setMuted(MuteState::ForceMuted);
|
||||
} else if (muted() == MuteState::ForceMuted) {
|
||||
setMuted(MuteState::Muted);
|
||||
} else if (data.is_muted() && muted() != MuteState::Muted) {
|
||||
setMuted(MuteState::Muted);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -581,6 +666,8 @@ void GroupCall::createAndStartController() {
|
||||
},
|
||||
.initialInputDeviceId = _audioInputId.toStdString(),
|
||||
.initialOutputDeviceId = _audioOutputId.toStdString(),
|
||||
.createAudioDeviceModule = Webrtc::AudioDeviceModuleCreator(
|
||||
settings.callAudioBackend()),
|
||||
};
|
||||
if (Logs::DebugEnabled()) {
|
||||
auto callLogFolder = cWorkingDir() + qsl("DebugLogs");
|
||||
@@ -602,6 +689,7 @@ void GroupCall::createAndStartController() {
|
||||
std::move(descriptor));
|
||||
|
||||
updateInstanceMuteState();
|
||||
updateInstanceVolumes();
|
||||
|
||||
//raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());
|
||||
}
|
||||
@@ -614,6 +702,26 @@ void GroupCall::updateInstanceMuteState() {
|
||||
&& state != MuteState::PushToTalk);
|
||||
}
|
||||
|
||||
void GroupCall::updateInstanceVolumes() {
|
||||
const auto real = _peer->groupCall();
|
||||
if (!real || real->id() != _id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &participants = real->participants();
|
||||
for (const auto &participant : participants) {
|
||||
const auto setVolume = participant.mutedByMe
|
||||
|| (participant.volume != Group::kDefaultVolume);
|
||||
if (setVolume && participant.ssrc) {
|
||||
_instance->setVolume(
|
||||
participant.ssrc,
|
||||
(participant.mutedByMe
|
||||
? 0.
|
||||
: (participant.volume / float64(Group::kDefaultVolume))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data) {
|
||||
Expects(!data.updates.empty());
|
||||
|
||||
@@ -752,7 +860,8 @@ void GroupCall::sendMutedUpdate() {
|
||||
? MTPphone_EditGroupCallMember::Flag::f_muted
|
||||
: MTPphone_EditGroupCallMember::Flag(0)),
|
||||
inputCall(),
|
||||
MTP_inputUserSelf()
|
||||
MTP_inputUserSelf(),
|
||||
MTP_int(100000) // volume
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
_updateMuteRequestId = 0;
|
||||
_peer->session().api().applyUpdates(result);
|
||||
@@ -783,16 +892,40 @@ void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) {
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::toggleMute(not_null<UserData*> user, bool mute) {
|
||||
if (!_id) {
|
||||
void GroupCall::toggleMute(const Group::MuteRequest &data) {
|
||||
if (data.locallyOnly) {
|
||||
applyParticipantLocally(data.user, data.mute, std::nullopt);
|
||||
} else {
|
||||
editParticipant(data.user, data.mute, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::changeVolume(const Group::VolumeRequest &data) {
|
||||
if (data.locallyOnly) {
|
||||
applyParticipantLocally(data.user, false, data.volume);
|
||||
} else {
|
||||
editParticipant(data.user, false, data.volume);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::editParticipant(
|
||||
not_null<UserData*> user,
|
||||
bool mute,
|
||||
std::optional<int> volume) {
|
||||
const auto participant = LookupParticipant(_peer, _id, user);
|
||||
if (!participant) {
|
||||
return;
|
||||
}
|
||||
applyParticipantLocally(user, mute, volume);
|
||||
|
||||
using Flag = MTPphone_EditGroupCallMember::Flag;
|
||||
const auto flags = (mute ? Flag::f_muted : Flag(0))
|
||||
| (volume.has_value() ? Flag::f_volume : Flag(0));
|
||||
_api.request(MTPphone_EditGroupCallMember(
|
||||
MTP_flags(mute
|
||||
? MTPphone_EditGroupCallMember::Flag::f_muted
|
||||
: MTPphone_EditGroupCallMember::Flag(0)),
|
||||
MTP_flags(flags),
|
||||
inputCall(),
|
||||
user->inputUser
|
||||
user->inputUser,
|
||||
MTP_int(std::clamp(volume.value_or(0), 1, Group::kMaxVolume))
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
_peer->session().api().applyUpdates(result);
|
||||
}).fail([=](const RPCError &error) {
|
||||
@@ -918,6 +1051,11 @@ void GroupCall::pushToTalkCancel() {
|
||||
}
|
||||
}
|
||||
|
||||
auto GroupCall::otherParticipantStateValue() const
|
||||
-> rpl::producer<Group::ParticipantState> {
|
||||
return _otherParticipantStateValue.events();
|
||||
}
|
||||
|
||||
//void GroupCall::setAudioVolume(bool input, float level) {
|
||||
// if (_instance) {
|
||||
// if (input) {
|
||||
|
||||
@@ -35,6 +35,12 @@ struct LastSpokeTimes;
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
} // namespace Group
|
||||
|
||||
enum class MuteState {
|
||||
Active,
|
||||
PushToTalk,
|
||||
@@ -104,6 +110,9 @@ public:
|
||||
return _muted.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto otherParticipantStateValue() const
|
||||
-> rpl::producer<Group::ParticipantState>;
|
||||
|
||||
enum State {
|
||||
Creating,
|
||||
Joining,
|
||||
@@ -131,7 +140,8 @@ public:
|
||||
//void setAudioVolume(bool input, float level);
|
||||
void setAudioDuckingEnabled(bool enabled);
|
||||
|
||||
void toggleMute(not_null<UserData*> user, bool mute);
|
||||
void toggleMute(const Group::MuteRequest &data);
|
||||
void changeVolume(const Group::VolumeRequest &data);
|
||||
std::variant<int, not_null<UserData*>> inviteUsers(
|
||||
const std::vector<not_null<UserData*>> &users);
|
||||
|
||||
@@ -163,6 +173,7 @@ private:
|
||||
void maybeSendMutedUpdate(MuteState previous);
|
||||
void sendMutedUpdate();
|
||||
void updateInstanceMuteState();
|
||||
void updateInstanceVolumes();
|
||||
void applySelfInCallLocally();
|
||||
void rejoin();
|
||||
|
||||
@@ -178,6 +189,15 @@ private:
|
||||
void stopConnectingSound();
|
||||
void playConnectingSoundOnce();
|
||||
|
||||
void editParticipant(
|
||||
not_null<UserData*> user,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyParticipantLocally(
|
||||
not_null<UserData*> user,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
@@ -190,6 +210,8 @@ private:
|
||||
rpl::variable<MuteState> _muted = MuteState::Muted;
|
||||
bool _acceptFields = false;
|
||||
|
||||
rpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;
|
||||
|
||||
uint64 _id = 0;
|
||||
uint64 _accessHash = 0;
|
||||
uint32 _mySsrc = 0;
|
||||
|
||||
36
Telegram/SourceFiles/calls/calls_group_common.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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 UserData;
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
constexpr auto kDefaultVolume = 10000;
|
||||
constexpr auto kMaxVolume = 20000;
|
||||
|
||||
struct MuteRequest {
|
||||
not_null<UserData*> user;
|
||||
bool mute = false;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
struct VolumeRequest {
|
||||
not_null<UserData*> user;
|
||||
int volume = kDefaultVolume;
|
||||
bool finalized = true;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
struct ParticipantState {
|
||||
not_null<UserData*> user;
|
||||
std::optional<int> volume;
|
||||
bool mutedByMe = false;
|
||||
bool locallyOnly = false;
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "calls/calls_group_members.h"
|
||||
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_group_common.h"
|
||||
#include "calls/calls_volume_item.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_user.h"
|
||||
@@ -16,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_peer_values.h" // Data::CanWriteValue.
|
||||
#include "data/data_session.h" // Data::Session::invitedToCallUsers.
|
||||
#include "settings/settings_common.h" // Settings::CreateButton.
|
||||
#include "ui/paint/arcs.h"
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
@@ -44,6 +47,12 @@ constexpr auto kUserpicMinScale = 0.8;
|
||||
constexpr auto kMaxLevel = 1.;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
const auto kSpeakerThreshold = std::vector<float>{
|
||||
Group::kDefaultVolume * 0.1f / Group::kMaxVolume,
|
||||
Group::kDefaultVolume * 0.9f / Group::kMaxVolume };
|
||||
|
||||
constexpr auto kArcsStrokeRatio = 0.8;
|
||||
|
||||
auto RowBlobs() -> std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
@@ -76,7 +85,8 @@ public:
|
||||
QRect rect,
|
||||
float64 speaking,
|
||||
float64 active,
|
||||
float64 muted) = 0;
|
||||
float64 muted,
|
||||
bool mutedByMe) = 0;
|
||||
};
|
||||
|
||||
class Row final : public PeerListRow {
|
||||
@@ -87,6 +97,7 @@ public:
|
||||
Active,
|
||||
Inactive,
|
||||
Muted,
|
||||
MutedByMe,
|
||||
Invited,
|
||||
};
|
||||
|
||||
@@ -106,6 +117,9 @@ public:
|
||||
[[nodiscard]] bool speaking() const {
|
||||
return _speaking;
|
||||
}
|
||||
[[nodiscard]] int volume() const {
|
||||
return _volume;
|
||||
}
|
||||
|
||||
void addActionRipple(QPoint point, Fn<void()> updateCallback) override;
|
||||
void stopLastActionRipple() override;
|
||||
@@ -172,11 +186,37 @@ private:
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
struct StatusIcon {
|
||||
StatusIcon(float volume)
|
||||
: speaker(st::groupCallStatusSpeakerIcon)
|
||||
, arcs(std::make_unique<Ui::Paint::ArcsAnimation>(
|
||||
st::groupCallStatusSpeakerArcsAnimation,
|
||||
kSpeakerThreshold,
|
||||
volume,
|
||||
Ui::Paint::ArcsAnimation::HorizontalDirection::Right)) {
|
||||
}
|
||||
const style::icon &speaker;
|
||||
const std::unique_ptr<Ui::Paint::ArcsAnimation> arcs;
|
||||
int arcsWidth = 0;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
int statusIconWidth() const;
|
||||
int statusIconHeight() const;
|
||||
void paintStatusIcon(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected);
|
||||
|
||||
void refreshStatus() override;
|
||||
void setSounding(bool sounding);
|
||||
void setSpeaking(bool speaking);
|
||||
void setState(State state);
|
||||
void setSsrc(uint32 ssrc);
|
||||
void setVolume(int volume);
|
||||
|
||||
void ensureUserpicCache(
|
||||
std::shared_ptr<Data::CloudImageView> &view,
|
||||
@@ -186,10 +226,13 @@ private:
|
||||
State _state = State::Inactive;
|
||||
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
|
||||
std::unique_ptr<BlobsAnimation> _blobsAnimation;
|
||||
std::unique_ptr<StatusIcon> _statusIcon;
|
||||
Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon.
|
||||
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
|
||||
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
|
||||
Ui::Animations::Simple _arcsAnimation; // For volume arcs animation.
|
||||
uint32 _ssrc = 0;
|
||||
int _volume = Group::kDefaultVolume;
|
||||
bool _sounding = false;
|
||||
bool _speaking = false;
|
||||
bool _skipLevelUpdate = false;
|
||||
@@ -206,7 +249,8 @@ public:
|
||||
not_null<QWidget*> menuParent);
|
||||
~MembersController();
|
||||
|
||||
using MuteRequest = GroupMembers::MuteRequest;
|
||||
using MuteRequest = Group::MuteRequest;
|
||||
using VolumeRequest = Group::VolumeRequest;
|
||||
|
||||
Main::Session &session() const override;
|
||||
void prepare() override;
|
||||
@@ -221,6 +265,7 @@ public:
|
||||
return _fullCount.value();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<MuteRequest> toggleMuteRequests() const;
|
||||
[[nodiscard]] rpl::producer<VolumeRequest> changeVolumeRequests() const;
|
||||
[[nodiscard]] auto kickMemberRequests() const
|
||||
-> rpl::producer<not_null<UserData*>>;
|
||||
|
||||
@@ -231,7 +276,8 @@ public:
|
||||
QRect rect,
|
||||
float64 speaking,
|
||||
float64 active,
|
||||
float64 muted) override;
|
||||
float64 muted,
|
||||
bool mutedByMe) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::unique_ptr<Row> createSelfRow();
|
||||
@@ -246,6 +292,11 @@ private:
|
||||
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row);
|
||||
void addMuteActionsToContextMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<UserData*> user,
|
||||
bool userIsCallAdmin,
|
||||
not_null<Row*> row);
|
||||
void setupListChangeViewers(not_null<GroupCall*> call);
|
||||
void subscribeToChanges(not_null<Data::GroupCall*> real);
|
||||
void updateRow(
|
||||
@@ -271,8 +322,11 @@ private:
|
||||
bool _prepared = false;
|
||||
|
||||
rpl::event_stream<MuteRequest> _toggleMuteRequests;
|
||||
rpl::event_stream<VolumeRequest> _changeVolumeRequests;
|
||||
rpl::event_stream<not_null<UserData*>> _kickMemberRequests;
|
||||
rpl::variable<int> _fullCount = 1;
|
||||
rpl::variable<int> _fullCountMin = 0;
|
||||
rpl::variable<int> _fullCountMax = std::numeric_limits<int>::max();
|
||||
|
||||
not_null<QWidget*> _menuParent;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
@@ -303,17 +357,24 @@ void Row::setSkipLevelUpdate(bool value) {
|
||||
|
||||
void Row::updateState(const Data::GroupCall::Participant *participant) {
|
||||
setSsrc(participant ? participant->ssrc : 0);
|
||||
setVolume(participant
|
||||
? participant->volume
|
||||
: Group::kDefaultVolume);
|
||||
if (!participant) {
|
||||
setState(State::Invited);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
} else if (!participant->muted
|
||||
|| (participant->sounding && participant->ssrc != 0)) {
|
||||
setState(State::Active);
|
||||
setState(participant->mutedByMe
|
||||
? State::MutedByMe
|
||||
: (participant->sounding || participant->speaking)
|
||||
? State::Active
|
||||
: State::Inactive);
|
||||
setSounding(participant->sounding && participant->ssrc != 0);
|
||||
setSpeaking(participant->speaking && participant->ssrc != 0);
|
||||
} else if (participant->canSelfUnmute) {
|
||||
setState(State::Inactive);
|
||||
setState(participant->mutedByMe ? State::MutedByMe : State::Inactive);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
} else {
|
||||
@@ -333,6 +394,42 @@ void Row::setSpeaking(bool speaking) {
|
||||
_speaking ? 0. : 1.,
|
||||
_speaking ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
|
||||
if (!_speaking
|
||||
|| (_state == State::MutedByMe)
|
||||
|| (_state == State::Muted)) {
|
||||
_statusIcon = nullptr;
|
||||
} else if (!_statusIcon) {
|
||||
_statusIcon = std::make_unique<StatusIcon>(
|
||||
(float)_volume / Group::kMaxVolume);
|
||||
_statusIcon->arcs->setStrokeRatio(kArcsStrokeRatio);
|
||||
_statusIcon->arcsWidth = _statusIcon->arcs->finishedWidth();
|
||||
|
||||
const auto wasArcsWidth = _statusIcon->lifetime.make_state<int>(0);
|
||||
|
||||
_statusIcon->arcs->startUpdateRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (!_arcsAnimation.animating()) {
|
||||
*wasArcsWidth = _statusIcon->arcsWidth;
|
||||
}
|
||||
auto callback = [=](float64 value) {
|
||||
if (_statusIcon) {
|
||||
_statusIcon->arcs->update(crl::now());
|
||||
|
||||
_statusIcon->arcsWidth = anim::interpolate(
|
||||
*wasArcsWidth,
|
||||
_statusIcon->arcs->finishedWidth(),
|
||||
value);
|
||||
}
|
||||
_delegate->rowUpdateRow(this);
|
||||
};
|
||||
_arcsAnimation.start(
|
||||
std::move(callback),
|
||||
0.,
|
||||
1.,
|
||||
st::groupCallSpeakerArcsAnimation.duration);
|
||||
}, _statusIcon->lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
void Row::setSounding(bool sounding) {
|
||||
@@ -382,6 +479,13 @@ void Row::setSsrc(uint32 ssrc) {
|
||||
_ssrc = ssrc;
|
||||
}
|
||||
|
||||
void Row::setVolume(int volume) {
|
||||
_volume = volume;
|
||||
if (_statusIcon) {
|
||||
_statusIcon->arcs->setValue((float)volume / Group::kMaxVolume);
|
||||
}
|
||||
}
|
||||
|
||||
void Row::updateLevel(float level) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
@@ -449,13 +553,16 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback {
|
||||
auto userpic = ensureUserpicView();
|
||||
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
|
||||
if (_blobsAnimation) {
|
||||
const auto mutedByMe = (_state == State::MutedByMe);
|
||||
const auto shift = QPointF(x + size / 2., y + size / 2.);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.translate(shift);
|
||||
const auto brush = anim::brush(
|
||||
st::groupCallMemberInactiveStatus,
|
||||
st::groupCallMemberActiveStatus,
|
||||
_speakingAnimation.value(_speaking ? 1. : 0.));
|
||||
const auto brush = mutedByMe
|
||||
? st::groupCallMemberMutedIcon->b
|
||||
: anim::brush(
|
||||
st::groupCallMemberInactiveStatus,
|
||||
st::groupCallMemberActiveStatus,
|
||||
_speakingAnimation.value(_speaking ? 1. : 0.));
|
||||
_blobsAnimation->blobs.paint(p, brush);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
@@ -492,6 +599,60 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback {
|
||||
};
|
||||
}
|
||||
|
||||
int Row::statusIconWidth() const {
|
||||
if (!_statusIcon) {
|
||||
return 0;
|
||||
}
|
||||
return _speaking
|
||||
? (_statusIcon->speaker.width() + _statusIcon->arcsWidth)
|
||||
: 0;
|
||||
}
|
||||
|
||||
int Row::statusIconHeight() const {
|
||||
if (!_statusIcon) {
|
||||
return 0;
|
||||
}
|
||||
return _speaking
|
||||
? _statusIcon->speaker.height()
|
||||
: 0;
|
||||
}
|
||||
|
||||
void Row::paintStatusIcon(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected) {
|
||||
if (!_statusIcon) {
|
||||
return;
|
||||
}
|
||||
p.setFont(font);
|
||||
const auto color = (_speaking
|
||||
? st.statusFgActive
|
||||
: (selected ? st.statusFgOver : st.statusFg))->c;
|
||||
p.setPen(color);
|
||||
|
||||
const auto speakerRect = QRect(
|
||||
st.statusPosition
|
||||
+ QPoint(0, (font->height - statusIconHeight()) / 2),
|
||||
_statusIcon->speaker.size());
|
||||
const auto arcPosition = speakerRect.topLeft()
|
||||
+ QPoint(
|
||||
speakerRect.width() - st::groupCallStatusSpeakerArcsSkip,
|
||||
speakerRect.height() / 2);
|
||||
|
||||
const auto volume = std::round(_volume / 100.);
|
||||
_statusIcon->speaker.paint(
|
||||
p,
|
||||
speakerRect.topLeft(),
|
||||
speakerRect.width(),
|
||||
color);
|
||||
|
||||
p.save();
|
||||
p.translate(arcPosition);
|
||||
_statusIcon->arcs->paint(p, color);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void Row::paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
@@ -500,24 +661,36 @@ void Row::paintStatusText(
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) {
|
||||
if (_state != State::Invited) {
|
||||
const auto &font = st::normalFont;
|
||||
if (_state != State::Invited && _state != State::MutedByMe) {
|
||||
p.save();
|
||||
paintStatusIcon(p, st, font, selected);
|
||||
const auto translatedWidth = statusIconWidth();
|
||||
p.translate(translatedWidth, 0);
|
||||
const auto guard = gsl::finally([&] { p.restore(); });
|
||||
PeerListRow::paintStatusText(
|
||||
p,
|
||||
st,
|
||||
x,
|
||||
y,
|
||||
availableWidth,
|
||||
availableWidth - translatedWidth,
|
||||
outerWidth,
|
||||
selected);
|
||||
return;
|
||||
}
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st::groupCallMemberNotJoinedStatus);
|
||||
p.setFont(font);
|
||||
if (_state == State::MutedByMe) {
|
||||
p.setPen(st::groupCallMemberMutedIcon);
|
||||
} else {
|
||||
p.setPen(st::groupCallMemberNotJoinedStatus);
|
||||
}
|
||||
p.drawTextLeft(
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
(peer()->isSelf()
|
||||
(_state == State::MutedByMe
|
||||
? tr::lng_group_call_muted_by_me_status(tr::now)
|
||||
: peer()->isSelf()
|
||||
? tr::lng_status_connecting(tr::now)
|
||||
: tr::lng_group_call_invited_status(tr::now)));
|
||||
}
|
||||
@@ -559,13 +732,16 @@ void Row::paintAction(
|
||||
(_state == State::Active) ? 1. : 0.);
|
||||
const auto muted = _mutedAnimation.value(
|
||||
(_state == State::Muted) ? 1. : 0.);
|
||||
_delegate->rowPaintIcon(p, iconRect, speaking, active, muted);
|
||||
const auto mutedByMe = (_state == State::MutedByMe);
|
||||
_delegate->rowPaintIcon(p, iconRect, speaking, active, muted, mutedByMe);
|
||||
}
|
||||
|
||||
void Row::refreshStatus() {
|
||||
setCustomStatus(
|
||||
(_speaking
|
||||
? tr::lng_group_call_active(tr::now)
|
||||
? u"%1% %2"_q
|
||||
.arg(std::round(_volume / 100.))
|
||||
.arg(tr::lng_group_call_active(tr::now))
|
||||
: tr::lng_group_call_inactive(tr::now)),
|
||||
_speaking);
|
||||
}
|
||||
@@ -682,9 +858,12 @@ void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
||||
_realCallRawValue = real;
|
||||
_realId = real->id();
|
||||
|
||||
_fullCount = real->fullCountValue(
|
||||
) | rpl::map([](int value) {
|
||||
return std::max(value, 1);
|
||||
_fullCount = rpl::combine(
|
||||
real->fullCountValue(),
|
||||
_fullCountMin.value(),
|
||||
_fullCountMax.value()
|
||||
) | rpl::map([](int value, int min, int max) {
|
||||
return std::max(std::clamp(value, min, max), 1);
|
||||
});
|
||||
|
||||
real->participantsSliceAdded(
|
||||
@@ -741,32 +920,48 @@ void MembersController::appendInvitedUsers() {
|
||||
void MembersController::updateRow(
|
||||
const std::optional<Data::GroupCall::Participant> &was,
|
||||
const Data::GroupCall::Participant &now) {
|
||||
auto reorderIfInvitedBeforeIndex = 0;
|
||||
auto countChange = 0;
|
||||
if (const auto row = findRow(now.user)) {
|
||||
if (now.speaking && (!was || !was->speaking)) {
|
||||
checkSpeakingRowPosition(row);
|
||||
}
|
||||
if (row->state() == Row::State::Invited) {
|
||||
reorderIfInvitedBeforeIndex = row->absoluteIndex();
|
||||
countChange = 1;
|
||||
}
|
||||
updateRow(row, &now);
|
||||
} else if (auto row = createRow(now)) {
|
||||
if (row->speaking()) {
|
||||
delegate()->peerListPrependRow(std::move(row));
|
||||
} else {
|
||||
static constexpr auto kInvited = Row::State::Invited;
|
||||
const auto reorder = [&] {
|
||||
const auto count = delegate()->peerListFullRowsCount();
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
const auto row = delegate()->peerListRowAt(count - 1).get();
|
||||
return (static_cast<Row*>(row)->state() == kInvited);
|
||||
}();
|
||||
reorderIfInvitedBeforeIndex = delegate()->peerListFullRowsCount();
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
if (reorder) {
|
||||
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
||||
return static_cast<const Row&>(row).state() != kInvited;
|
||||
});
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
countChange = 1;
|
||||
}
|
||||
static constexpr auto kInvited = Row::State::Invited;
|
||||
const auto reorder = [&] {
|
||||
const auto count = reorderIfInvitedBeforeIndex;
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
const auto row = delegate()->peerListRowAt(
|
||||
reorderIfInvitedBeforeIndex - 1).get();
|
||||
return (static_cast<Row*>(row)->state() == kInvited);
|
||||
}();
|
||||
if (reorder) {
|
||||
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
||||
return static_cast<const Row&>(row).state() != kInvited;
|
||||
});
|
||||
}
|
||||
if (countChange) {
|
||||
const auto fullCountMin = _fullCountMin.current() + countChange;
|
||||
if (_fullCountMax.current() < fullCountMin) {
|
||||
_fullCountMax = fullCountMin;
|
||||
}
|
||||
_fullCountMin = fullCountMin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,10 +1074,11 @@ void MembersController::prepare() {
|
||||
setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
|
||||
|
||||
const auto call = _call.get();
|
||||
if (const auto real = _peer->groupCall();
|
||||
real && call && real->id() == call->id()) {
|
||||
if (const auto real = _peer->groupCall()
|
||||
; real && call && real->id() == call->id()) {
|
||||
prepareRows(real);
|
||||
} else if (auto row = createSelfRow()) {
|
||||
_fullCountMin = (row->state() == Row::State::Invited) ? 0 : 1;
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
@@ -898,6 +1094,7 @@ void MembersController::prepareRows(not_null<Data::GroupCall*> real) {
|
||||
auto foundSelf = false;
|
||||
auto changed = false;
|
||||
const auto &participants = real->participants();
|
||||
auto fullCountMin = 0;
|
||||
auto count = delegate()->peerListFullRowsCount();
|
||||
for (auto i = 0; i != count;) {
|
||||
auto row = delegate()->peerListRowAt(i);
|
||||
@@ -912,6 +1109,7 @@ void MembersController::prepareRows(not_null<Data::GroupCall*> real) {
|
||||
not_null{ user },
|
||||
&Data::GroupCall::Participant::user);
|
||||
if (contains) {
|
||||
++fullCountMin;
|
||||
++i;
|
||||
} else {
|
||||
changed = true;
|
||||
@@ -927,18 +1125,29 @@ void MembersController::prepareRows(not_null<Data::GroupCall*> real) {
|
||||
&Data::GroupCall::Participant::user);
|
||||
auto row = (i != end(participants)) ? createRow(*i) : createSelfRow();
|
||||
if (row) {
|
||||
if (row->state() != Row::State::Invited) {
|
||||
++fullCountMin;
|
||||
}
|
||||
changed = true;
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
}
|
||||
for (const auto &participant : participants) {
|
||||
if (auto row = createRow(participant)) {
|
||||
++fullCountMin;
|
||||
changed = true;
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
delegate()->peerListRefreshRows();
|
||||
if (_fullCountMax.current() < fullCountMin) {
|
||||
_fullCountMax = fullCountMin;
|
||||
}
|
||||
_fullCountMin = fullCountMin;
|
||||
if (real->participantsLoaded()) {
|
||||
_fullCountMax = fullCountMin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -953,6 +1162,11 @@ auto MembersController::toggleMuteRequests() const
|
||||
return _toggleMuteRequests.events();
|
||||
}
|
||||
|
||||
auto MembersController::changeVolumeRequests() const
|
||||
-> rpl::producer<VolumeRequest> {
|
||||
return _changeVolumeRequests.events();
|
||||
}
|
||||
|
||||
bool MembersController::rowCanMuteMembers() {
|
||||
return _peer->canManageGroupCall();
|
||||
}
|
||||
@@ -966,11 +1180,12 @@ void MembersController::rowPaintIcon(
|
||||
QRect rect,
|
||||
float64 speaking,
|
||||
float64 active,
|
||||
float64 muted) {
|
||||
float64 muted,
|
||||
bool mutedByMe) {
|
||||
const auto &greenIcon = st::groupCallMemberColoredCrossLine.icon;
|
||||
const auto left = rect.x() + (rect.width() - greenIcon.width()) / 2;
|
||||
const auto top = rect.y() + (rect.height() - greenIcon.height()) / 2;
|
||||
if (speaking == 1.) {
|
||||
if (speaking == 1. && !mutedByMe) {
|
||||
// Just green icon, no cross, no coloring.
|
||||
greenIcon.paintInCenter(p, rect);
|
||||
return;
|
||||
@@ -998,7 +1213,9 @@ void MembersController::rowPaintIcon(
|
||||
}
|
||||
const auto activeInactiveColor = anim::color(
|
||||
st::groupCallMemberInactiveIcon,
|
||||
st::groupCallMemberActiveIcon,
|
||||
(mutedByMe
|
||||
? st::groupCallMemberMutedIcon
|
||||
: st::groupCallMemberActiveIcon),
|
||||
speaking);
|
||||
const auto iconColor = anim::color(
|
||||
activeInactiveColor,
|
||||
@@ -1059,10 +1276,11 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
|
||||
not_null<PeerListRow*> row) {
|
||||
Expects(row->peer()->isUser());
|
||||
|
||||
if (row->peer()->isSelf()) {
|
||||
const auto real = static_cast<Row*>(row.get());
|
||||
if (row->peer()->isSelf()
|
||||
&& (!_peer->canManageGroupCall() || !real->ssrc())) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto real = static_cast<Row*>(row.get());
|
||||
const auto user = row->peer()->asUser();
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
@@ -1088,15 +1306,6 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
const auto mute = admin
|
||||
? (muteState == Row::State::Active)
|
||||
: (muteState != Row::State::Muted);
|
||||
const auto toggleMute = crl::guard(this, [=] {
|
||||
_toggleMuteRequests.fire(MuteRequest{
|
||||
.user = user,
|
||||
.mute = mute,
|
||||
});
|
||||
});
|
||||
|
||||
const auto session = &user->session();
|
||||
const auto getCurrentWindow = [=]() -> Window::SessionController* {
|
||||
@@ -1147,40 +1356,158 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
|
||||
_kickMemberRequests.fire_copy(user);
|
||||
});
|
||||
|
||||
if ((muteState != Row::State::Invited)
|
||||
&& _peer->canManageGroupCall()
|
||||
&& (!admin || mute)) {
|
||||
result->addAction(
|
||||
(mute
|
||||
? tr::lng_group_call_context_mute(tr::now)
|
||||
: tr::lng_group_call_context_unmute(tr::now)),
|
||||
toggleMute);
|
||||
if (real->ssrc() != 0) {
|
||||
addMuteActionsToContextMenu(result, user, admin, real);
|
||||
}
|
||||
result->addAction(
|
||||
tr::lng_context_view_profile(tr::now),
|
||||
showProfile);
|
||||
result->addAction(
|
||||
tr::lng_context_send_message(tr::now),
|
||||
showHistory);
|
||||
const auto canKick = [&] {
|
||||
if (static_cast<Row*>(row.get())->state() == Row::State::Invited) {
|
||||
return false;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
return chat->amCreator()
|
||||
|| (chat->canBanMembers() && !chat->admins.contains(user));
|
||||
} else if (const auto group = _peer->asMegagroup()) {
|
||||
return group->canRestrictUser(user);
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
if (canKick) {
|
||||
|
||||
if (!user->isSelf()) {
|
||||
result->addAction(
|
||||
tr::lng_context_remove_from_group(tr::now),
|
||||
removeFromGroup);
|
||||
tr::lng_context_view_profile(tr::now),
|
||||
showProfile);
|
||||
result->addAction(
|
||||
tr::lng_context_send_message(tr::now),
|
||||
showHistory);
|
||||
const auto canKick = [&] {
|
||||
if (static_cast<Row*>(row.get())->state() == Row::State::Invited) {
|
||||
return false;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
return chat->amCreator()
|
||||
|| (chat->canBanMembers() && !chat->admins.contains(user));
|
||||
} else if (const auto group = _peer->asMegagroup()) {
|
||||
return group->canRestrictUser(user);
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
if (canKick) {
|
||||
result->addAction(
|
||||
tr::lng_context_remove_from_group(tr::now),
|
||||
removeFromGroup);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void MembersController::addMuteActionsToContextMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
not_null<UserData*> user,
|
||||
bool userIsCallAdmin,
|
||||
not_null<Row*> row) {
|
||||
const auto muteString = [=] {
|
||||
return (_peer->canManageGroupCall()
|
||||
? tr::lng_group_call_context_mute
|
||||
: tr::lng_group_call_context_mute_for_me)(tr::now);
|
||||
};
|
||||
|
||||
const auto unmuteString = [=] {
|
||||
return (_peer->canManageGroupCall()
|
||||
? tr::lng_group_call_context_unmute
|
||||
: tr::lng_group_call_context_unmute_for_me)(tr::now);
|
||||
};
|
||||
|
||||
const auto toggleMute = crl::guard(this, [=](bool mute, bool local) {
|
||||
_toggleMuteRequests.fire(Group::MuteRequest{
|
||||
.user = user,
|
||||
.mute = mute,
|
||||
.locallyOnly = local,
|
||||
});
|
||||
});
|
||||
const auto changeVolume = crl::guard(this, [=](
|
||||
int volume,
|
||||
bool local) {
|
||||
_changeVolumeRequests.fire(Group::VolumeRequest{
|
||||
.user = user,
|
||||
.volume = std::clamp(volume, 1, Group::kMaxVolume),
|
||||
.locallyOnly = local,
|
||||
});
|
||||
});
|
||||
|
||||
const auto muteState = row->state();
|
||||
const auto isMuted = (muteState == Row::State::Muted)
|
||||
|| (muteState == Row::State::MutedByMe);
|
||||
|
||||
auto mutesFromVolume = rpl::never<bool>() | rpl::type_erased();
|
||||
|
||||
if (!isMuted || user->isSelf()) {
|
||||
const auto call = _call.get();
|
||||
auto otherParticipantStateValue = call
|
||||
? call->otherParticipantStateValue(
|
||||
) | rpl::filter([=](const Group::ParticipantState &data) {
|
||||
return data.user == user;
|
||||
})
|
||||
: rpl::never<Group::ParticipantState>() | rpl::type_erased();
|
||||
|
||||
auto volumeItem = base::make_unique_q<MenuVolumeItem>(
|
||||
menu,
|
||||
st::groupCallPopupMenu.menu,
|
||||
otherParticipantStateValue,
|
||||
row->volume(),
|
||||
Group::kMaxVolume,
|
||||
isMuted);
|
||||
|
||||
mutesFromVolume = volumeItem->toggleMuteRequests();
|
||||
|
||||
volumeItem->toggleMuteRequests(
|
||||
) | rpl::start_with_next([=](bool muted) {
|
||||
if (muted) {
|
||||
// Slider value is changed after the callback is called.
|
||||
// To capture good state inside the slider frame we postpone.
|
||||
crl::on_main(menu, [=] {
|
||||
menu->hideMenu();
|
||||
});
|
||||
}
|
||||
toggleMute(muted, false);
|
||||
}, volumeItem->lifetime());
|
||||
|
||||
volumeItem->toggleMuteLocallyRequests(
|
||||
) | rpl::start_with_next([=](bool muted) {
|
||||
if (!user->isSelf()) {
|
||||
toggleMute(muted, true);
|
||||
}
|
||||
}, volumeItem->lifetime());
|
||||
|
||||
volumeItem->changeVolumeRequests(
|
||||
) | rpl::start_with_next([=](int volume) {
|
||||
changeVolume(volume, false);
|
||||
}, volumeItem->lifetime());
|
||||
|
||||
volumeItem->changeVolumeLocallyRequests(
|
||||
) | rpl::start_with_next([=](int volume) {
|
||||
if (!user->isSelf()) {
|
||||
changeVolume(volume, true);
|
||||
}
|
||||
}, volumeItem->lifetime());
|
||||
|
||||
menu->addAction(std::move(volumeItem));
|
||||
};
|
||||
|
||||
const auto muteAction = [&]() -> QAction* {
|
||||
if (muteState == Row::State::Invited
|
||||
|| user->isSelf()
|
||||
|| (muteState == Row::State::Muted
|
||||
&& userIsCallAdmin
|
||||
&& _peer->canManageGroupCall())) {
|
||||
return nullptr;
|
||||
}
|
||||
auto callback = [=] {
|
||||
const auto state = row->state();
|
||||
const auto muted = (state == Row::State::Muted)
|
||||
|| (state == Row::State::MutedByMe);
|
||||
toggleMute(!muted, false);
|
||||
};
|
||||
return menu->addAction(
|
||||
isMuted ? unmuteString() : muteString(),
|
||||
std::move(callback));
|
||||
}();
|
||||
|
||||
if (muteAction) {
|
||||
std::move(
|
||||
mutesFromVolume
|
||||
) | rpl::start_with_next([=](bool muted) {
|
||||
muteAction->setText(muted ? unmuteString() : muteString());
|
||||
}, menu->lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Row> MembersController::createSelfRow() {
|
||||
const auto self = _peer->session().user();
|
||||
auto result = std::make_unique<Row>(this, self);
|
||||
@@ -1207,6 +1534,7 @@ std::unique_ptr<Row> MembersController::createInvitedRow(
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
GroupMembers::GroupMembers(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<GroupCall*> call)
|
||||
@@ -1222,11 +1550,17 @@ GroupMembers::GroupMembers(
|
||||
}
|
||||
|
||||
auto GroupMembers::toggleMuteRequests() const
|
||||
-> rpl::producer<GroupMembers::MuteRequest> {
|
||||
-> rpl::producer<Group::MuteRequest> {
|
||||
return static_cast<MembersController*>(
|
||||
_listController.get())->toggleMuteRequests();
|
||||
}
|
||||
|
||||
auto GroupMembers::changeVolumeRequests() const
|
||||
-> rpl::producer<Group::VolumeRequest> {
|
||||
return static_cast<MembersController*>(
|
||||
_listController.get())->changeVolumeRequests();
|
||||
}
|
||||
|
||||
auto GroupMembers::kickMemberRequests() const
|
||||
-> rpl::producer<not_null<UserData*>> {
|
||||
return static_cast<MembersController*>(
|
||||
@@ -1321,6 +1655,13 @@ void GroupMembers::setupList() {
|
||||
resizeToList();
|
||||
}, _list->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_scroll->scrollTopValue(),
|
||||
_scroll->heightValue()
|
||||
) | rpl::start_with_next([=](int scrollTop, int scrollHeight) {
|
||||
_list->setVisibleTopBottom(scrollTop, scrollTop + scrollHeight);
|
||||
}, _scroll->lifetime());
|
||||
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
@@ -1422,12 +1763,6 @@ void GroupMembers::setupFakeRoundCorners() {
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void GroupMembers::visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
|
||||
}
|
||||
|
||||
void GroupMembers::peerListSetTitle(rpl::producer<QString> title) {
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ class GroupCall;
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct VolumeRequest;
|
||||
struct MuteRequest;
|
||||
} // namespace Group
|
||||
|
||||
class GroupCall;
|
||||
|
||||
class GroupMembers final
|
||||
@@ -30,15 +35,13 @@ public:
|
||||
not_null<QWidget*> parent,
|
||||
not_null<GroupCall*> call);
|
||||
|
||||
struct MuteRequest {
|
||||
not_null<UserData*> user;
|
||||
bool mute = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] int desiredHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const;
|
||||
[[nodiscard]] rpl::producer<MuteRequest> toggleMuteRequests() const;
|
||||
[[nodiscard]] auto toggleMuteRequests() const
|
||||
-> rpl::producer<Group::MuteRequest>;
|
||||
[[nodiscard]] auto changeVolumeRequests() const
|
||||
-> rpl::producer<Group::VolumeRequest>;
|
||||
[[nodiscard]] auto kickMemberRequests() const
|
||||
-> rpl::producer<not_null<UserData*>>;
|
||||
[[nodiscard]] rpl::producer<> addMembersRequests() const {
|
||||
@@ -48,10 +51,6 @@ public:
|
||||
private:
|
||||
using ListWidget = PeerListContent;
|
||||
|
||||
void visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) override;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
// PeerListContentDelegate interface.
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_group_panel.h"
|
||||
|
||||
#include "calls/calls_group_common.h"
|
||||
#include "calls/calls_group_members.h"
|
||||
#include "calls/calls_group_settings.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
@@ -505,9 +506,16 @@ void GroupPanel::initWithCall(GroupCall *call) {
|
||||
}, _callLifetime);
|
||||
|
||||
_members->toggleMuteRequests(
|
||||
) | rpl::start_with_next([=](GroupMembers::MuteRequest request) {
|
||||
) | rpl::start_with_next([=](Group::MuteRequest request) {
|
||||
if (_call) {
|
||||
_call->toggleMute(request.user, request.mute);
|
||||
_call->toggleMute(request);
|
||||
}
|
||||
}, _callLifetime);
|
||||
|
||||
_members->changeVolumeRequests(
|
||||
) | rpl::start_with_next([=](Group::VolumeRequest request) {
|
||||
if (_call) {
|
||||
_call->changeVolume(request);
|
||||
}
|
||||
}, _callLifetime);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "core/application.h"
|
||||
#include "boxes/single_choice_box.h"
|
||||
#include "webrtc/webrtc_audio_input_tester.h"
|
||||
@@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "settings/settings_calls.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_invite_links.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_settings.h"
|
||||
@@ -441,20 +443,9 @@ void GroupCallSettingsBox(
|
||||
)->addClickHandler([=] {
|
||||
if (!copyLink() && !state->generatingLink) {
|
||||
state->generatingLink = true;
|
||||
peer->session().api().request(MTPmessages_ExportChatInvite(
|
||||
peer->input
|
||||
)).done([=](const MTPExportedChatInvite &result) {
|
||||
if (result.type() == mtpc_chatInviteExported) {
|
||||
const auto link = qs(
|
||||
result.c_chatInviteExported().vlink());
|
||||
if (const auto chat = peer->asChat()) {
|
||||
chat->setInviteLink(link);
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
channel->setInviteLink(link);
|
||||
}
|
||||
copyLink();
|
||||
}
|
||||
}).send();
|
||||
peer->session().api().inviteLinks().create(
|
||||
peer,
|
||||
crl::guard(layout, [=](auto&&) { copyLink(); }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -480,6 +471,7 @@ void GroupCallSettingsBox(
|
||||
// Means we finished showing the box.
|
||||
crl::on_main(box, [=] {
|
||||
state->micTester = std::make_unique<Webrtc::AudioInputTester>(
|
||||
Core::App().settings().callAudioBackend(),
|
||||
Core::App().settings().callInputDeviceId());
|
||||
state->levelUpdateTimer.callEach(kMicTestUpdateInterval);
|
||||
});
|
||||
|
||||
@@ -377,6 +377,8 @@ void Instance::handleCallUpdate(
|
||||
} else if (phoneCall.vdate().v + (config.callRingTimeoutMs / 1000)
|
||||
< base::unixtime::now()) {
|
||||
LOG(("Ignoring too old call."));
|
||||
} else if (Core::App().settings().disableCalls()) {
|
||||
LOG(("Ignoring call because of 'accept calls' settings."));
|
||||
} else {
|
||||
createCall(user, Call::Type::Incoming, phoneCall.is_video());
|
||||
_currentCall->handleUpdate(call);
|
||||
|
||||
@@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "platform/platform_specific.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "window/main_window.h"
|
||||
#include "media/view/media_view_pip.h" // Utilities for frame rotation.
|
||||
#include "app.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_calls.h"
|
||||
@@ -98,12 +99,26 @@ Panel::Incoming::Incoming(
|
||||
void Panel::Incoming::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
|
||||
const auto frame = _track->frame(Webrtc::FrameRequest());
|
||||
if (frame.isNull()) {
|
||||
const auto [image, rotation] = _track->frameOriginalWithRotation();
|
||||
if (image.isNull()) {
|
||||
p.fillRect(e->rect(), Qt::black);
|
||||
} else {
|
||||
using namespace Media::View;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawImage(rect(), frame);
|
||||
if (UsePainterRotation(rotation)) {
|
||||
if (rotation) {
|
||||
p.save();
|
||||
p.rotate(rotation);
|
||||
}
|
||||
p.drawImage(RotatedRect(rect(), rotation), image);
|
||||
if (rotation) {
|
||||
p.restore();
|
||||
}
|
||||
} else if (rotation) {
|
||||
p.drawImage(rect(), RotateFrameImage(image, rotation));
|
||||
} else {
|
||||
p.drawImage(rect(), image);
|
||||
}
|
||||
fillBottomShadow(p);
|
||||
fillTopShadow(p);
|
||||
}
|
||||
@@ -462,7 +477,11 @@ void Panel::reinitWithCall(Call *call) {
|
||||
|
||||
_call->videoIncoming()->renderNextFrame(
|
||||
) | rpl::start_with_next([=] {
|
||||
setIncomingSize(_call->videoIncoming()->frame({}).size());
|
||||
const auto track = _call->videoIncoming();
|
||||
const auto [frame, rotation] = track->frameOriginalWithRotation();
|
||||
setIncomingSize((rotation == 90 || rotation == 270)
|
||||
? QSize(frame.height(), frame.width())
|
||||
: frame.size());
|
||||
if (_incoming->isHidden()) {
|
||||
return;
|
||||
}
|
||||
|
||||
306
Telegram/SourceFiles/calls/calls_volume_item.cpp
Normal file
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
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 "calls/calls_volume_item.h"
|
||||
|
||||
#include "calls/calls_group_common.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include "ui/paint/arcs.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxVolumePercent = 200;
|
||||
|
||||
const auto kSpeakerThreshold = std::vector<float>{
|
||||
10.0f / kMaxVolumePercent,
|
||||
50.0f / kMaxVolumePercent,
|
||||
150.0f / kMaxVolumePercent };
|
||||
|
||||
constexpr auto kVolumeStickedValues =
|
||||
std::array<std::pair<float64, float64>, 7>{{
|
||||
{ 25. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 50. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 75. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 100. / kMaxVolumePercent, 5. / kMaxVolumePercent },
|
||||
{ 125. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 150. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
{ 175. / kMaxVolumePercent, 2. / kMaxVolumePercent },
|
||||
}};
|
||||
|
||||
QString VolumeString(int volumePercent) {
|
||||
return u"%1%"_q.arg(volumePercent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MenuVolumeItem::MenuVolumeItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<Group::ParticipantState> participantState,
|
||||
int startVolume,
|
||||
int maxVolume,
|
||||
bool muted)
|
||||
: Ui::Menu::ItemBase(parent, st)
|
||||
, _maxVolume(maxVolume)
|
||||
, _cloudMuted(muted)
|
||||
, _localMuted(muted)
|
||||
, _slider(base::make_unique_q<Ui::MediaSlider>(
|
||||
this,
|
||||
st::groupCallMenuVolumeSlider))
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _st(st)
|
||||
, _stCross(st::groupCallMuteCrossLine)
|
||||
, _crossLineMute(std::make_unique<Ui::CrossLineAnimation>(_stCross, true))
|
||||
, _arcs(std::make_unique<Ui::Paint::ArcsAnimation>(
|
||||
st::groupCallSpeakerArcsAnimation,
|
||||
kSpeakerThreshold,
|
||||
_localMuted ? 0. : (startVolume / float(maxVolume)),
|
||||
Ui::Paint::ArcsAnimation::HorizontalDirection::Right)) {
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
enableMouseSelecting();
|
||||
enableMouseSelecting(_slider.get());
|
||||
|
||||
_slider->setAlwaysDisplayMarker(true);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::start_with_next([=](const QSize &size) {
|
||||
const auto geometry = QRect(QPoint(), size);
|
||||
_itemRect = geometry - _st.itemPadding;
|
||||
_speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());
|
||||
_arcPosition = _speakerRect.center()
|
||||
+ QPoint(0, st::groupCallMenuSpeakerArcsSkip);
|
||||
_volumeRect = QRect(
|
||||
_arcPosition.x()
|
||||
+ st::groupCallMenuVolumeSkip
|
||||
+ _arcs->finishedWidth(),
|
||||
_speakerRect.y(),
|
||||
_st.itemStyle.font->width(VolumeString(kMaxVolumePercent)),
|
||||
_speakerRect.height());
|
||||
|
||||
_slider->setGeometry(_itemRect
|
||||
- style::margins(0, contentHeight() / 2, 0, 0));
|
||||
}, lifetime());
|
||||
|
||||
setCloudVolume(startVolume);
|
||||
|
||||
paintRequest(
|
||||
) | rpl::start_with_next([=](const QRect &clip) {
|
||||
Painter p(this);
|
||||
|
||||
const auto volume = _localMuted
|
||||
? 0
|
||||
: std::round(_slider->value() * kMaxVolumePercent);
|
||||
const auto muteProgress =
|
||||
_crossLineAnimation.value((!volume) ? 1. : 0.);
|
||||
|
||||
const auto selected = isSelected();
|
||||
p.fillRect(clip, selected ? st.itemBgOver : st.itemBg);
|
||||
|
||||
const auto mutePen = anim::color(
|
||||
unmuteColor(),
|
||||
muteColor(),
|
||||
muteProgress);
|
||||
p.setPen(mutePen);
|
||||
p.setFont(_st.itemStyle.font);
|
||||
p.drawText(_volumeRect, VolumeString(volume), style::al_left);
|
||||
|
||||
_crossLineMute->paint(
|
||||
p,
|
||||
_speakerRect.topLeft(),
|
||||
muteProgress,
|
||||
(!muteProgress) ? std::nullopt : std::optional<QColor>(mutePen));
|
||||
|
||||
{
|
||||
p.translate(_arcPosition);
|
||||
_arcs->paint(p);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_slider->setChangeProgressCallback([=](float64 value) {
|
||||
const auto newMuted = (value == 0);
|
||||
if (_localMuted != newMuted) {
|
||||
_localMuted = newMuted;
|
||||
_toggleMuteLocallyRequests.fire_copy(newMuted);
|
||||
|
||||
_crossLineAnimation.start(
|
||||
[=] { update(_speakerRect.united(_volumeRect)); },
|
||||
_localMuted ? 0. : 1.,
|
||||
_localMuted ? 1. : 0.,
|
||||
st::callPanelDuration);
|
||||
}
|
||||
if (value > 0) {
|
||||
_changeVolumeLocallyRequests.fire(value * _maxVolume);
|
||||
}
|
||||
update(_volumeRect);
|
||||
_arcs->setValue(value);
|
||||
});
|
||||
|
||||
const auto returnVolume = [=] {
|
||||
_changeVolumeLocallyRequests.fire_copy(_cloudVolume);
|
||||
};
|
||||
|
||||
_slider->setChangeFinishedCallback([=](float64 value) {
|
||||
const auto newVolume = std::round(value * _maxVolume);
|
||||
const auto muted = (value == 0);
|
||||
|
||||
if (!_cloudMuted && muted) {
|
||||
returnVolume();
|
||||
_localMuted = true;
|
||||
_toggleMuteRequests.fire(true);
|
||||
}
|
||||
if (_cloudMuted && muted) {
|
||||
returnVolume();
|
||||
}
|
||||
if (_cloudMuted && !muted) {
|
||||
_waitingForUpdateVolume = true;
|
||||
_localMuted = false;
|
||||
_toggleMuteRequests.fire(false);
|
||||
}
|
||||
if (!_cloudMuted && !muted) {
|
||||
_changeVolumeRequests.fire_copy(newVolume);
|
||||
}
|
||||
});
|
||||
|
||||
std::move(
|
||||
participantState
|
||||
) | rpl::start_with_next([=](const Group::ParticipantState &state) {
|
||||
const auto newMuted = state.mutedByMe;
|
||||
const auto newVolume = state.volume.value_or(0);
|
||||
|
||||
_cloudMuted = _localMuted = newMuted;
|
||||
|
||||
if (!newVolume) {
|
||||
return;
|
||||
}
|
||||
if (_waitingForUpdateVolume) {
|
||||
const auto localVolume =
|
||||
std::round(_slider->value() * _maxVolume);
|
||||
if ((localVolume != newVolume)
|
||||
&& (_cloudVolume == newVolume)) {
|
||||
_changeVolumeRequests.fire(int(localVolume));
|
||||
}
|
||||
} else {
|
||||
setCloudVolume(newVolume);
|
||||
}
|
||||
_waitingForUpdateVolume = false;
|
||||
}, lifetime());
|
||||
|
||||
_slider->setAdjustCallback([=](float64 value) {
|
||||
for (const auto &snap : kVolumeStickedValues) {
|
||||
if (value > (snap.first - snap.second)
|
||||
&& value < (snap.first + snap.second)) {
|
||||
return snap.first;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
initArcsAnimation();
|
||||
}
|
||||
|
||||
void MenuVolumeItem::initArcsAnimation() {
|
||||
const auto volumeLeftWas = lifetime().make_state<int>(0);
|
||||
const auto lastTime = lifetime().make_state<int>(0);
|
||||
_arcsAnimation.init([=](crl::time now) {
|
||||
_arcs->update(now);
|
||||
update(_speakerRect);
|
||||
|
||||
const auto wasRect = _volumeRect;
|
||||
_volumeRect.moveLeft(anim::interpolate(
|
||||
*volumeLeftWas,
|
||||
_arcPosition.x()
|
||||
+ st::groupCallMenuVolumeSkip
|
||||
+ _arcs->finishedWidth(),
|
||||
std::clamp(
|
||||
(now - (*lastTime))
|
||||
/ float64(st::groupCallSpeakerArcsAnimation.duration),
|
||||
0.,
|
||||
1.)));
|
||||
update(_speakerRect.united(wasRect.united(_volumeRect)));
|
||||
});
|
||||
|
||||
_arcs->startUpdateRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (!_arcsAnimation.animating()) {
|
||||
*volumeLeftWas = _volumeRect.left();
|
||||
*lastTime = crl::now();
|
||||
_arcsAnimation.start();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_arcs->stopUpdateRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
_arcsAnimation.stop();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
QColor MenuVolumeItem::unmuteColor() const {
|
||||
return (isSelected()
|
||||
? _st.itemFgOver
|
||||
: isEnabled()
|
||||
? _st.itemFg
|
||||
: _st.itemFgDisabled)->c;
|
||||
}
|
||||
|
||||
QColor MenuVolumeItem::muteColor() const {
|
||||
return (isSelected()
|
||||
? st::attentionButtonFgOver
|
||||
: st::attentionButtonFg)->c;
|
||||
}
|
||||
|
||||
void MenuVolumeItem::setCloudVolume(int volume) {
|
||||
if (_cloudVolume == volume) {
|
||||
return;
|
||||
}
|
||||
_cloudVolume = volume;
|
||||
if (!_slider->isChanging()) {
|
||||
setSliderVolume(_cloudMuted ? 0. : volume);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuVolumeItem::setSliderVolume(int volume) {
|
||||
_slider->setValue(float64(volume) / _maxVolume);
|
||||
update(_volumeRect);
|
||||
}
|
||||
|
||||
not_null<QAction*> MenuVolumeItem::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool MenuVolumeItem::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
int MenuVolumeItem::contentHeight() const {
|
||||
return _st.itemPadding.top()
|
||||
+ _st.itemPadding.bottom()
|
||||
+ _stCross.icon.height() * 2;
|
||||
}
|
||||
|
||||
rpl::producer<bool> MenuVolumeItem::toggleMuteRequests() const {
|
||||
return _toggleMuteRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<bool> MenuVolumeItem::toggleMuteLocallyRequests() const {
|
||||
return _toggleMuteLocallyRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<int> MenuVolumeItem::changeVolumeRequests() const {
|
||||
return _changeVolumeRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<int> MenuVolumeItem::changeVolumeLocallyRequests() const {
|
||||
return _changeVolumeLocallyRequests.events();
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
87
Telegram/SourceFiles/calls/calls_volume_item.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
|
||||
namespace Ui {
|
||||
class CrossLineAnimation;
|
||||
class MediaSlider;
|
||||
namespace Paint {
|
||||
class ArcsAnimation;
|
||||
} // namespace Paint
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
} // namespace Group
|
||||
|
||||
class MenuVolumeItem final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
MenuVolumeItem(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
rpl::producer<Group::ParticipantState> participantState,
|
||||
int startVolume,
|
||||
int maxVolume,
|
||||
bool muted);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> toggleMuteRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> toggleMuteLocallyRequests() const;
|
||||
[[nodiscard]] rpl::producer<int> changeVolumeRequests() const;
|
||||
[[nodiscard]] rpl::producer<int> changeVolumeLocallyRequests() const;
|
||||
|
||||
protected:
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void initArcsAnimation();
|
||||
|
||||
void setCloudVolume(int volume);
|
||||
void setSliderVolume(int volume);
|
||||
|
||||
QColor unmuteColor() const;
|
||||
QColor muteColor() const;
|
||||
|
||||
const int _maxVolume;
|
||||
int _cloudVolume = 0;
|
||||
bool _waitingForUpdateVolume = false;
|
||||
bool _cloudMuted = false;
|
||||
bool _localMuted = false;
|
||||
|
||||
QRect _itemRect;
|
||||
QRect _speakerRect;
|
||||
QRect _volumeRect;
|
||||
QPoint _arcPosition;
|
||||
|
||||
const base::unique_qptr<Ui::MediaSlider> _slider;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
const style::CrossLineAnimation &_stCross;
|
||||
|
||||
const std::unique_ptr<Ui::CrossLineAnimation> _crossLineMute;
|
||||
Ui::Animations::Simple _crossLineAnimation;
|
||||
const std::unique_ptr<Ui::Paint::ArcsAnimation> _arcs;
|
||||
Ui::Animations::Basic _arcsAnimation;
|
||||
|
||||
rpl::event_stream<bool> _toggleMuteRequests;
|
||||
rpl::event_stream<bool> _toggleMuteLocallyRequests;
|
||||
rpl::event_stream<int> _changeVolumeRequests;
|
||||
rpl::event_stream<int> _changeVolumeLocallyRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
@@ -179,12 +179,15 @@ void SuggestionsWidget::scrollByWheelEvent(not_null<QWheelEvent*> e) {
|
||||
const auto delta = e->pixelDelta().x()
|
||||
? e->pixelDelta().x()
|
||||
: e->angleDelta().x();
|
||||
return snap(current - ((rtl() ? -1 : 1) * delta), 0, _scrollMax);
|
||||
return std::clamp(
|
||||
current - ((rtl() ? -1 : 1) * delta),
|
||||
0,
|
||||
_scrollMax);
|
||||
} else if (vertical) {
|
||||
const auto delta = e->pixelDelta().y()
|
||||
? e->pixelDelta().y()
|
||||
: e->angleDelta().y();
|
||||
return snap(current - delta, 0, _scrollMax);
|
||||
return std::clamp(current - delta, 0, _scrollMax);
|
||||
}
|
||||
return current;
|
||||
}();
|
||||
@@ -241,7 +244,7 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) {
|
||||
|
||||
void SuggestionsWidget::paintFadings(Painter &p) const {
|
||||
const auto scroll = scrollCurrent();
|
||||
const auto o_left = snap(
|
||||
const auto o_left = std::clamp(
|
||||
scroll / float64(st::emojiSuggestionsFadeAfter),
|
||||
0.,
|
||||
1.);
|
||||
@@ -256,7 +259,7 @@ void SuggestionsWidget::paintFadings(Painter &p) const {
|
||||
st::emojiSuggestionsFadeLeft.fill(p, rect);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
const auto o_right = snap(
|
||||
const auto o_right = std::clamp(
|
||||
(_scrollMax - scroll) / float64(st::emojiSuggestionsFadeAfter),
|
||||
0.,
|
||||
1.);
|
||||
@@ -422,7 +425,7 @@ void SuggestionsWidget::mouseMoveEvent(QMouseEvent *e) {
|
||||
const auto globalPosition = e->globalPos();
|
||||
if (_dragScrollStart >= 0) {
|
||||
const auto delta = (_mousePressPosition.x() - globalPosition.x());
|
||||
const auto scroll = snap(
|
||||
const auto scroll = std::clamp(
|
||||
_dragScrollStart + (rtl() ? -1 : 1) * delta,
|
||||
0,
|
||||
_scrollMax);
|
||||
|
||||
@@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_chat.h"
|
||||
@@ -636,7 +637,7 @@ void FieldAutocomplete::showAnimated() {
|
||||
return;
|
||||
}
|
||||
if (_cache.isNull()) {
|
||||
_stickersSeed = rand_value<uint64>();
|
||||
_stickersSeed = openssl::RandomValue<uint64>();
|
||||
_scroll->show();
|
||||
_cache = Ui::GrabWidget(this);
|
||||
}
|
||||
@@ -1138,7 +1139,7 @@ void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send));
|
||||
|
||||
if (!_menu->actions().empty()) {
|
||||
if (!_menu->empty()) {
|
||||
_menu->popup(QCursor::pos());
|
||||
}
|
||||
}
|
||||
|
||||