Merge tag 'v5.14.1' into dev

This commit is contained in:
AlexeyZavar 2025-05-02 01:46:43 +03:00
commit 939c9afeb4
233 changed files with 11311 additions and 1681 deletions

100
.cursor/api_usage.md Normal file
View file

@ -0,0 +1,100 @@
# Telegram Desktop API Usage
## API Schema
The API definitions are described using [TL Language](https://core.telegram.org/mtproto/TL) in two main schema files:
1. **`Telegram/SourceFiles/mtproto/scheme/mtproto.tl`**
* Defines the core MTProto protocol types and methods used for basic communication, encryption, authorization, service messages, etc.
* Some fundamental types and methods from this schema (like basic types, RPC calls, containers) are often implemented directly in the C++ MTProto core (`SourceFiles/mtproto/`) and may be skipped during the C++ code generation phase.
* Other parts of `mtproto.tl` might still be processed by the code generator.
2. **`Telegram/SourceFiles/mtproto/scheme/api.tl`**
* Defines the higher-level Telegram API layer, including all the methods and types related to chat functionality, user profiles, messages, channels, stickers, etc.
* This is the primary schema used when making functional API requests within the application.
Both files use the same TL syntax to describe API methods (functions) and types (constructors).
## Code Generation
A custom code generation tool processes `api.tl` (and parts of `mtproto.tl`) to create corresponding C++ classes and types. These generated headers are typically included via the Precompiled Header (PCH) for the main `Telegram` project.
Generated types often follow the pattern `MTP[Type]` (e.g., `MTPUser`, `MTPMessage`) and methods correspond to functions within the `MTP` namespace or related classes (e.g., `MTPmessages_SendMessage`).
## Making API Requests
API requests are made using a standard pattern involving the `api()` object (providing access to the `MTP::Instance`), the generated `MTP...` request object, callback handlers for success (`.done()`) and failure (`.fail()`), and the `.send()` method.
Here's the general structure:
```cpp
// Include necessary headers if not already in PCH
// Obtain the API instance (usually via api() or MTP::Instance::Get())
api().request(MTPnamespace_MethodName(
// Constructor arguments based on the api.tl definition for the method
MTP_flags(flags_value), // Use MTP_flags if the method has flags
MTP_inputPeer(peer), // Use MTP_... types for parameters
MTP_string(messageText),
MTP_long(randomId),
// ... other arguments matching the TL definition
MTP_vector<MTPMessageEntity>() // Example for a vector argument
)).done([=](const MTPResponseType &result) {
// Handle the successful response (result).
// 'result' will be of the C++ type corresponding to the TL type
// specified after the '=' in the api.tl method definition.
// How to access data depends on whether the TL type has one or multiple constructors:
// 1. Multiple Constructors (e.g., User = User | UserEmpty):
// Use .match() with lambdas for each constructor:
result.match([&](const MTPDuser &data) {
/* use data.vfirst_name().v, etc. */
}, [&](const MTPDuserEmpty &data) {
/* handle empty user */
});
// Alternatively, check the type explicitly and use the constructor getter:
if (result.type() == mtpc_user) {
const auto &data = result.c_user(); // Asserts if type is not mtpc_user!
// use data.vfirst_name().v
} else if (result.type() == mtpc_userEmpty) {
const auto &data = result.c_userEmpty();
// handle empty user
}
// 2. Single Constructor (e.g., Messages = messages { msgs: vector<Message> }):
// Use .match() with a single lambda:
result.match([&](const MTPDmessages &data) { /* use data.vmessages().v */ });
// Or check the type explicitly and use the constructor getter:
if (result.type() == mtpc_messages) {
const auto &data = result.c_messages(); // Asserts if type is not mtpc_messages!
// use data.vmessages().v
}
// Or use the shortcut .data() for single-constructor types:
const auto &data = result.data(); // Only works for single-constructor types!
// use data.vmessages().v
}).fail([=](const MTP::Error &error) {
// Handle the API error (error).
// 'error' is an MTP::Error object containing the error code (error.type())
// and description (error.description()). Check for specific error strings.
if (error.type() == u"FLOOD_WAIT_X"_q) {
// Handle flood wait
} else {
Ui::show(Box<InformBox>(Lang::Hard::ServerError())); // Example generic error handling
}
}).handleFloodErrors().send(); // handleFloodErrors() is common, then send()
```
**Key Points:**
* Always refer to `Telegram/SourceFiles/mtproto/scheme/api.tl` for the correct method names, parameters (names and types), and response types.
* Use the generated `MTP...` types/classes for request parameters (e.g., `MTP_int`, `MTP_string`, `MTP_bool`, `MTP_vector`, `MTPInputUser`, etc.) and response handling.
* The `.done()` lambda receives the specific C++ `MTP...` type corresponding to the TL return type.
* For types with **multiple constructors** (e.g., `User = User | UserEmpty`), use `result.match([&](const MTPDuser &d){ ... }, [&](const MTPDuserEmpty &d){ ... })` to handle each case, or check `result.type() == mtpc_user` / `mtpc_userEmpty` and call the specific `result.c_user()` / `result.c_userEmpty()` getter (which asserts on type mismatch).
* For types with a **single constructor** (e.g., `Messages = messages{...}`), you can use `result.match([&](const MTPDmessages &d){ ... })` with one lambda, or check `type()` and call `c_messages()`, or use the shortcut `result.data()` to access the fields directly.
* The `.fail()` lambda receives an `MTP::Error` object. Check `error.type()` against known error strings (often defined as constants or using `u"..."_q` literals).
* Directly construct the `MTPnamespace_MethodName(...)` object inside `request()`.
* Include `.handleFloodErrors()` before `.send()` for standard flood wait handling.

159
.cursor/localization.md Normal file
View file

@ -0,0 +1,159 @@
# Telegram Desktop Localization
## Coding Style Note
**Use `auto`:** In the actual codebase, variable types are almost always deduced using `auto` (or `const auto`, `const auto &`) rather than being written out explicitly. Examples in this guide may use explicit types for clarity, but prefer `auto` in practice.
```cpp
// Prefer this:
auto currentTitle = tr::lng_settings_title(tr::now);
auto nameProducer = GetNameProducer(); // Returns rpl::producer<...>
// Instead of this:
QString currentTitle = tr::lng_settings_title(tr::now);
rpl::producer<QString> nameProducer = GetNameProducer();
```
## String Resource File
Base user-facing English strings are defined in the `lang.strings` file:
`Telegram/Resources/langs/lang.strings`
This file uses a key-value format with named placeholders:
```
"lng_settings_title" = "Settings";
"lng_confirm_delete_item" = "Are you sure you want to delete {item_name}?";
"lng_files_selected" = "{count} files selected"; // Simple count example (see Pluralization)
```
Placeholders are enclosed in curly braces, e.g., `{name}`, `{user}`. A special placeholder `{count}` is used for pluralization rules.
### Pluralization
For keys that depend on a number (using the `{count}` placeholder), English typically requires two forms: singular and plural. These are defined in `lang.strings` using `#one` and `#other` suffixes:
```
"lng_files_selected#one" = "{count} file selected";
"lng_files_selected#other" = "{count} files selected";
```
While only `#one` and `#other` are defined in the base `lang.strings`, the code generation process creates C++ accessors for all six CLDR plural categories (`#zero`, `#one`, `#two`, `#few`, `#many`, `#other`) to support languages with more complex pluralization rules.
## Translation Process
While `lang.strings` provides the base English text and the keys, the actual translations are managed via Telegram's translations platform (translations.telegram.org) and loaded dynamically at runtime from the API. The keys from `lang.strings` (including the `#one`/`#other` variants) are used on the platform.
## Code Generation
A code generation tool processes `lang.strings` to create C++ structures and accessors within the `tr` namespace. These allow type-safe access to strings and handling of placeholders and pluralization. Generated keys typically follow the pattern `tr::lng_key_name`.
## String Usage in Code
Strings are accessed in C++ code using the generated objects within the `tr::` namespace. There are two main ways to use them: reactively (returning an `rpl::producer`) or immediately (returning the current value).
### 1. Reactive Usage (rpl::producer)
Calling a generated string function directly returns a reactive producer, typically `rpl::producer<QString>`. This producer automatically updates its value whenever the application language changes.
```cpp
// Key: "settings_title" = "Settings";
auto titleProducer = tr::lng_settings_title(); // Type: rpl::producer<QString>
// Key: "confirm_delete_item" = "Are you sure you want to delete {item_name}?";
auto itemNameProducer = /* ... */; // Type: rpl::producer<QString>
auto confirmationProducer = tr::lng_confirm_delete_item( // Type: rpl::producer<QString>
tr::now, // NOTE: tr::now is NOT passed here for reactive result
lt_item_name,
std::move(itemNameProducer)); // Placeholder producers should be moved
```
### 2. Immediate Usage (Current Value)
Passing `tr::now` as the first argument retrieves the string's current value in the active language (typically as a `QString`).
```cpp
// Key: "settings_title" = "Settings";
auto currentTitle = tr::lng_settings_title(tr::now); // Type: QString
// Key: "confirm_delete_item" = "Are you sure you want to delete {item_name}?";
const auto currentItemName = QString("My Document"); // Type: QString
auto currentConfirmation = tr::lng_confirm_delete_item( // Type: QString
tr::now, // Pass tr::now for immediate value
lt_item_name, currentItemName); // Placeholder value is a direct QString (or convertible)
```
### 3. Placeholders (`{tag}`)
Placeholders like `{item_name}` are replaced by providing arguments after `tr::now` (for immediate) or as the initial arguments (for reactive). A corresponding `lt_tag_name` constant is passed before the value.
* **Immediate:** Pass the direct value (e.g., `QString`, `int`).
* **Reactive:** Pass an `rpl::producer` of the corresponding type (e.g., `rpl::producer<QString>`). Remember to `std::move` the producer or use `rpl::duplicate` if you need to reuse the original producer afterwards.
### 4. Pluralization (`{count}`)
Keys using `{count}` require a numeric value for the `lt_count` placeholder. The correct plural form (`#zero`, `#one`, ..., `#other`) is automatically selected based on this value and the current language rules.
* **Immediate (`tr::now`):** Pass a `float64` or `int` (which is auto-converted to `float64`).
```cpp
int count = 1;
auto filesText = tr::lng_files_selected(tr::now, lt_count, count); // Type: QString
count = 5;
filesText = tr::lng_files_selected(tr::now, lt_count, count); // Uses "files_selected#other"
```
* **Reactive:** Pass an `rpl::producer<float64>`. Use the `tr::to_count()` helper to convert an `rpl::producer<int>` or wrap a single value.
```cpp
// From an existing int producer:
auto countProducer = /* ... */; // Type: rpl::producer<int>
auto filesTextProducer = tr::lng_files_selected( // Type: rpl::producer<QString>
lt_count,
countProducer | tr::to_count()); // Use tr::to_count() for conversion
// From a single int value wrapped reactively:
int currentCount = 5;
auto filesTextProducerSingle = tr::lng_files_selected( // Type: rpl::producer<QString>
lt_count,
rpl::single(currentCount) | tr::to_count());
// Alternative for single values (less common): rpl::single(currentCount * 1.)
```
### 5. Custom Projectors
An optional final argument can be a projector function (like `Ui::Text::Upper` or `Ui::Text::WithEntities`) to transform the output.
* If the projector returns `OutputType`, the string function returns `OutputType` (immediate) or `rpl::producer<OutputType>` (reactive).
* Placeholder values must match the projector's *input* requirements. For `Ui::Text::WithEntities`, placeholders expect `TextWithEntities` (immediate) or `rpl::producer<TextWithEntities>` (reactive).
```cpp
// Immediate with Ui::Text::WithEntities projector
// Key: "user_posted_photo" = "{user} posted a photo";
const auto userName = TextWithEntities{ /* ... */ }; // Type: TextWithEntities
auto message = tr::lng_user_posted_photo( // Type: TextWithEntities
tr::now,
lt_user,
userName, // Must be TextWithEntities
Ui::Text::WithEntities); // Projector
// Reactive with Ui::Text::WithEntities projector
auto userNameProducer = /* ... */; // Type: rpl::producer<TextWithEntities>
auto messageProducer = tr::lng_user_posted_photo( // Type: rpl::producer<TextWithEntities>
lt_user,
std::move(userNameProducer), // Move placeholder producers
Ui::Text::WithEntities); // Projector
```
## Key Summary
* Keys are defined in `Resources/langs/lang.strings` using `{tag}` placeholders.
* Plural keys use `{count}` and have `#one`/`#other` variants in `lang.strings`.
* Access keys via `tr::lng_key_name(...)` in C++.
* Call with `tr::now` as the first argument for the immediate `QString` (or projected type).
* Call without `tr::now` for the reactive `rpl::producer<QString>` (or projected type).
* Provide placeholder values (`lt_tag_name, value`) matching the usage (direct value for immediate, `rpl::producer` for reactive). Producers should typically be moved via `std::move`.
* For `{count}`:
* Immediate: Pass `int` or `float64`.
* Reactive: Pass `rpl::producer<float64>`, typically by converting an `int` producer using `| tr::to_count()`.
* Optional projector function as the last argument modifies the output type and required placeholder types.
* Actual translations are loaded at runtime from the API.

211
.cursor/rpl_guide.md Normal file
View file

@ -0,0 +1,211 @@
# RPL (Reactive Programming Library) Guide
## Coding Style Note
**Use `auto`:** In the actual codebase, variable types are almost always deduced using `auto` (or `const auto`, `const auto &`) rather than being written out explicitly. Examples in this guide may use explicit types for clarity, but prefer `auto` in practice.
```cpp
// Prefer this:
auto intProducer = rpl::single(123);
const auto &lifetime = existingLifetime;
// Instead of this:
rpl::producer<int> intProducer = rpl::single(123);
const rpl::lifetime &lifetime = existingLifetime;
// Sometimes needed if deduction is ambiguous or needs help:
auto user = std::make_shared<UserData>();
auto data = QByteArray::fromHex("...");
```
## Introduction
RPL is the reactive programming library used in this project, residing in the `rpl::` namespace. It allows handling asynchronous streams of data over time.
The core concept is the `rpl::producer`, which represents a stream of values that can be generated over a certain lifetime.
## Producers: `rpl::producer<Type, Error = no_error>`
The fundamental building block is `rpl::producer<Type, Error>`. It produces values of `Type` and can optionally signal an error of type `Error`. By default, `Error` is `rpl::no_error`, indicating that the producer does not explicitly handle error signaling through this mechanism.
```cpp
// A producer that emits integers.
auto intProducer = /* ... */; // Type: rpl::producer<int>
// A producer that emits strings and can potentially emit a CustomError.
auto stringProducerWithError = /* ... */; // Type: rpl::producer<QString, CustomError>
```
Producers are typically lazy; they don't start emitting values until someone subscribes to them.
## Lifetime Management: `rpl::lifetime`
Reactive pipelines have a limited duration, managed by `rpl::lifetime`. An `rpl::lifetime` object essentially holds a collection of cleanup callbacks. When the lifetime ends (either explicitly destroyed or goes out of scope), these callbacks are executed, tearing down the associated pipeline and freeing resources.
```cpp
rpl::lifetime myLifetime;
// ... later ...
// myLifetime is destroyed, cleanup happens.
// Or, pass a lifetime instance to manage a pipeline's duration.
rpl::lifetime &parentLifetime = /* ... get lifetime from context ... */;
```
## Starting a Pipeline: `rpl::start_...`
To consume values from a producer, you start a pipeline using one of the `rpl::start_...` methods. These methods subscribe to the producer and execute callbacks for the events they handle.
The most common method is `rpl::start_with_next`:
```cpp
auto counter = /* ... */; // Type: rpl::producer<int>
rpl::lifetime lifetime;
// Counter is consumed here, use std::move if it's an l-value.
std::move(
counter
) | rpl::start_with_next([=](int nextValue) {
// Process the next integer value emitted by the producer.
qDebug() << "Received: " << nextValue;
}, lifetime); // Pass the lifetime to manage the subscription.
// Note: `counter` is now in a moved-from state and likely invalid.
// If you need to start the same producer multiple times, duplicate it:
// rpl::duplicate(counter) | rpl::start_with_next(...);
// If you DON'T pass a lifetime to a start_... method:
auto counter2 = /* ... */; // Type: rpl::producer<int>
rpl::lifetime subscriptionLifetime = std::move(
counter2
) | rpl::start_with_next([=](int nextValue) { /* ... */ });
// The returned lifetime MUST be stored. If it's discarded immediately,
// the subscription stops instantly.
// `counter2` is also moved-from here.
```
Other variants allow handling errors (`_error`) and completion (`_done`):
```cpp
auto dataStream = /* ... */; // Type: rpl::producer<QString, Error>
rpl::lifetime lifetime;
// Assuming dataStream might be used again, we duplicate it for the first start.
// If it's the only use, std::move(dataStream) would be preferred.
rpl::duplicate(
dataStream
) | rpl::start_with_error([=](Error &&error) {
// Handle the error signaled by the producer.
qDebug() << "Error: " << error.text();
}, lifetime);
// Using dataStream again, perhaps duplicated again or moved if last use.
rpl::duplicate(
dataStream
) | rpl::start_with_done([=]() {
// Execute when the producer signals it's finished emitting values.
qDebug() << "Stream finished.";
}, lifetime);
// Last use of dataStream, so we move it.
std::move(
dataStream
) | rpl::start_with_next_error_done(
[=](QString &&value) { /* handle next value */ },
[=](Error &&error) { /* handle error */ },
[=]() { /* handle done */ },
lifetime);
```
## Transforming Producers
RPL provides functions to create new producers by transforming existing ones:
* `rpl::map`: Transforms each value emitted by a producer.
```cpp
auto ints = /* ... */; // Type: rpl::producer<int>
// The pipe operator often handles the move implicitly for chained transformations.
auto strings = std::move(
ints // Explicit move is safer
) | rpl::map([](int value) {
return QString::number(value * 2);
}); // Emits strings like "0", "2", "4", ...
```
* `rpl::filter`: Emits only the values from a producer that satisfy a condition.
```cpp
auto ints = /* ... */; // Type: rpl::producer<int>
auto evenInts = std::move(
ints // Explicit move is safer
) | rpl::filter([](int value) {
return (value % 2 == 0);
}); // Emits only even numbers.
```
## Combining Producers
You can combine multiple producers into one:
* `rpl::combine`: Combines the latest values from multiple producers whenever *any* of them emits a new value. Requires all producers to have emitted at least one value initially.
While it produces a `std::tuple`, subsequent operators like `map`, `filter`, and `start_with_next` can automatically unpack this tuple into separate lambda arguments.
```cpp
auto countProducer = rpl::single(1); // Type: rpl::producer<int>
auto textProducer = rpl::single(u"hello"_q); // Type: rpl::producer<QString>
rpl::lifetime lifetime;
// rpl::combine takes producers by const-ref internally and duplicates,
// so move/duplicate is usually not strictly needed here unless you
// want to signal intent or manage the lifetime of p1/p2 explicitly.
auto combined = rpl::combine(
countProducer, // or rpl::duplicate(countProducer)
textProducer // or rpl::duplicate(textProducer)
);
// Starting the combined producer consumes it.
// The lambda receives unpacked arguments, not the tuple itself.
std::move(
combined
) | rpl::start_with_next([=](int count, const QString &text) {
// No need for std::get<0>(latest), etc.
qDebug() << "Combined: Count=" << count << ", Text=" << text;
}, lifetime);
// This also works with map, filter, etc.
std::move(
combined
) | rpl::filter([=](int count, const QString &text) {
return count > 0 && !text.isEmpty();
}) | rpl::map([=](int count, const QString &text) {
return text.repeated(count);
}) | rpl::start_with_next([=](const QString &result) {
qDebug() << "Mapped & Filtered: " << result;
}, lifetime);
```
* `rpl::merge`: Merges the output of multiple producers of the *same type* into a single producer. It emits a value whenever *any* of the source producers emits a value.
```cpp
auto sourceA = /* ... */; // Type: rpl::producer<QString>
auto sourceB = /* ... */; // Type: rpl::producer<QString>
// rpl::merge also duplicates internally.
auto merged = rpl::merge(sourceA, sourceB);
// Starting the merged producer consumes it.
std::move(
merged
) | rpl::start_with_next([=](QString &&value) {
// Receives values from either sourceA or sourceB as they arrive.
qDebug() << "Merged value: " << value;
}, lifetime);
```
## Key Concepts Summary
* Use `rpl::producer<Type, Error>` to represent streams of values.
* Manage subscription duration using `rpl::lifetime`.
* Pass `rpl::lifetime` to `rpl::start_...` methods.
* If `rpl::lifetime` is not passed, **store the returned lifetime** to keep the subscription active.
* Use operators like `| rpl::map`, `| rpl::filter` to transform streams.
* Use `rpl::combine` or `rpl::merge` to combine streams.
* When starting a chain (`std::move(producer) | rpl::map(...)`), explicitly move the initial producer.
* These functions typically duplicate their input producers internally.
* Starting a pipeline consumes the producer; use `

149
.cursor/styling.md Normal file
View file

@ -0,0 +1,149 @@
# Telegram Desktop UI Styling
## Style Definition Files
UI element styles (colors, fonts, paddings, margins, icons, etc.) are defined in `.style` files using a custom syntax. These files are located alongside the C++ source files they correspond to within specific UI component directories (e.g., `Telegram/SourceFiles/ui/chat/chat.style`).
Definitions from other `.style` files can be included using the `using` directive at the top of the file:
```style
using "ui/basic.style";
using "ui/widgets/widgets.style";
```
The central definition of named colors happens in `Telegram/SourceFiles/ui/colors.palette`. This file allows for theme generation and loading colors from various sources.
### Syntax Overview
1. **Built-in Types:** The syntax recognizes several base types inferred from the value assigned:
* `int`: Integer numbers (e.g., `lineHeight: 20;`)
* `bool`: Boolean values (e.g., `useShadow: true;`)
* `pixels`: Pixel values, ending with `px` (e.g., `borderWidth: 1px;`). Generated as `int` in C++.
* `color`: Named colors defined in `colors.palette` (e.g., `background: windowBg;`)
* `icon`: Defined inline using a specific syntax (see below). Generates `style::icon`.
* `margins`: Four pixel values for margins or padding. Requires `margins(top, right, bottom, left)` syntax (e.g., `margin: margins(10px, 5px, 10px, 5px);` or `padding: margins(8px, 8px, 8px, 8px);`). Generates `style::margins` (an alias for `QMargins`).
* `size`: Two pixel values for width and height (e.g., `iconSize: size(16px, 16px);`). Generates `style::size`.
* `point`: Two pixel values for x and y coordinates (e.g., `textPos: point(5px, 2px);`). Generates `style::point`.
* `align`: Alignment keywords (e.g., `textAlign: align(center);` or `iconAlign: align(left);`). Generates `style::align`.
* `font`: Font definitions (e.g., `font: font(14px semibold);`). Generates `style::font`.
* `double`: Floating point numbers (e.g., `disabledOpacity: 0.5;`)
*Note on Borders:* Borders are typically defined using multiple fields like `border: pixels;` (for width) and `borderFg: color;` (for color), rather than a single CSS-like property.
2. **Structure Definition:** You can define complex data structures directly within the `.style` file:
```style
MyButtonStyle { // Defines a structure named 'MyButtonStyle'
textPadding: margins; // Field 'textPadding' expects margins type
icon: icon; // Field 'icon' of type icon
height: pixels; // Field 'height' of type pixels
}
```
This generates a `struct MyButtonStyle { ... };` inside the `namespace style`. Fields will have corresponding C++ types (`style::margins`, `style::icon`, `int`).
3. **Variable Definition & Inheritance:** Variables are defined using `name: value;` or `groupName { ... }`. They can be of built-in types or custom structures. Structures can be initialized inline or inherit from existing variables.
**Icon Definition Syntax:** Icons are defined inline using the `icon{...}` syntax. The generator probes for `.svg` files or `.png` files (including `@2x`, `@3x` variants) based on the provided path stem.
```style
// Single-part icon definition:
myIconSearch: icon{{ "gui/icons/search", iconColor }};
// Multi-part icon definition (layers drawn bottom-up):
myComplexIcon: icon{
{ "gui/icons/background", iconBgColor },
{ "gui/icons/foreground", iconFgColor }
};
// Icon with path modifiers (PNG only for flips, SVG only for size):
myFlippedIcon: icon{{ "gui/icons/arrow-flip_horizontal", arrowColor }};
myResizedIcon: icon{{ "gui/icons/logo-128x128", logoColor }}; // Forces 128x128 for SVG
```
**Other Variable Examples:**
```style
// Simple variables
buttonHeight: 30px;
activeButtonColor: buttonBgActive; // Named color from colors.palette
// Variable of a custom structure type, initialized inline
defaultButton: MyButtonStyle {
textPadding: margins(10px, 15px, 10px, 15px); // Use margins(...) syntax
icon: myIconSearch; // Assign the previously defined icon variable
height: buttonHeight; // Reference another variable
}
// Another variable inheriting from 'defaultButton' and overriding/adding fields
primaryButton: MyButtonStyle(defaultButton) {
icon: myComplexIcon; // Override icon with the multi-part one
backgroundColor: activeButtonColor; // Add a field not in MyButtonStyle definition
}
// Style group (often used for specific UI elements)
chatInput { // Example using separate border properties and explicit padding
border: 1px; // Border width
borderFg: defaultInputFieldBorder; // Border color (named color)
padding: margins(5px, 10px, 5px, 10px); // Use margins(...) syntax for padding field
backgroundColor: defaultChatBg; // Background color
}
```
## Code Generation
A code generation tool processes these `.style` files and `colors.palette` to create C++ objects.
- The `using` directives resolve dependencies between `.style` files.
- Custom structure definitions (like `MyButtonStyle`) generate corresponding `struct MyButtonStyle { ... };` within the `namespace style`.
- Style variables/groups (like `defaultButton`, `primaryButton`, `chatInput`) are generated as objects/structs within the `st` namespace (e.g., `st::defaultButton`, `st::primaryButton`, `st::chatInput`). These generated structs contain members corresponding to the fields defined in the `.style` file.
- Color objects are generated into the `st` namespace as well, based on their names in `colors.palette` (e.g., `st::windowBg`, `st::buttonBgActive`).
- The generated header files for styles are placed in the `Telegram/SourceFiles/styles/` directory with a `style_` prefix (e.g., `styles/style_widgets.h` for `ui/widgets/widgets.style`). You include them like `#include "styles/style_widgets.h"`.
Generated C++ types correspond to the `.style` types: `style::color`, `style::font`, `style::margins` (used for both `margin:` and `padding:` fields), `style::icon`, `style::size`, `style::point`, `style::align`, and `int` or `bool` for simple types.
## Style Usage in Code
Styles are applied in C++ code by referencing the generated `st::...` objects and their members.
```cpp
// Example: Including the generated style header
#include "styles/style_widgets.h" // For styles defined in ui/widgets/widgets.style
// ... inside some UI class code ...
// Accessing members of a generated style struct
int height = st::primaryButton.height; // Accessing the 'height' field (pixels -> int)
const style::icon &icon = st::primaryButton.icon; // Accessing the 'icon' field (st::myComplexIcon)
style::margins padding = st::primaryButton.textPadding; // Accessing 'textPadding'
style::color bgColor = st::primaryButton.backgroundColor; // Accessing the color (st::activeButtonColor)
// Applying styles (conceptual examples)
myButton->setIcon(st::primaryButton.icon);
myButton->setHeight(st::primaryButton.height);
myButton->setPadding(st::primaryButton.textPadding);
myButton->setBackgroundColor(st::primaryButton.backgroundColor);
// Using styles directly in painting
void MyWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
p.fillRect(rect(), st::chatInput.backgroundColor); // Use color from chatInput style
// Border painting requires width and color
int borderWidth = st::chatInput.border; // Access border width (pixels -> int)
style::color borderColor = st::chatInput.borderFg; // Access border color
if (borderWidth > 0) {
p.setPen(QPen(borderColor, borderWidth));
// Adjust rect for pen width if needed before drawing
p.drawRect(rect().adjusted(borderWidth / 2, borderWidth / 2, -borderWidth / 2, -borderWidth / 2));
}
// Access padding (style::margins)
style::margins inputPadding = st::chatInput.padding;
// ... use inputPadding.top(), inputPadding.left() etc. for content layout ...
}
```
**Key Points:**
* Styles are defined in `.style` files next to their corresponding C++ source files.
* `using "path/to/other.style";` includes definitions from other style files.
* Named colors are defined centrally in `ui/colors.palette`.
* `.style` syntax supports built-in types (like `pixels`, `color`, `margins`, `point`, `size`, `align`, `font`, `double`), custom structure definitions (`Name { field: type; ... }`), variable definitions (`name: value;`), and inheritance (`child: Name(parent) { ... }`).
* Values must match the expected type (e.g., fields declared as `margins` type, like `margin:` or `padding:`, require `margins(...)` syntax). Borders are typically set via separate `border: pixels;` and `borderFg: color;` fields.
* Icons are defined inline using `name: icon{{ "path_stem", color }};` or `name: icon{ { "path1", c1 }, ... };` syntax, with optional path modifiers.
* Code generation creates `struct` definitions in the `style` namespace for custom types and objects/structs in the `st` namespace for defined variables/groups.
* Generated headers are in `styles/` with a `style_` prefix and must be included.
* Access style properties via the generated `st::` objects (e.g., `st::primaryButton.height`, `st::chatInput.backgroundColor`).

2
.cursorignore Normal file
View file

@ -0,0 +1,2 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
Telegram/ThirdParty/

View file

@ -26,7 +26,6 @@ get_filename_component(res_loc Resources REALPATH)
include(cmake/telegram_options.cmake)
include(cmake/lib_ffmpeg.cmake)
include(cmake/lib_stripe.cmake)
include(cmake/lib_tgvoip.cmake)
include(cmake/lib_tgcalls.cmake)
include(cmake/lib_prisma.cmake)
include(cmake/td_export.cmake)
@ -34,6 +33,7 @@ include(cmake/td_iv.cmake)
include(cmake/td_lang.cmake)
include(cmake/td_mtproto.cmake)
include(cmake/td_scheme.cmake)
include(cmake/td_tde2e.cmake)
include(cmake/td_ui.cmake)
include(cmake/generate_appdata_changelog.cmake)
@ -47,17 +47,15 @@ if (WIN32)
platform/win/windows_quiethours.idl
platform/win/windows_toastactivator.idl
)
nuget_add_winrt(Telegram)
endif()
set_target_properties(Telegram PROPERTIES AUTOMOC ON)
target_link_libraries(Telegram
PRIVATE
tdesktop::lib_tgcalls_legacy
# tdesktop::lib_tgcalls_legacy
tdesktop::lib_tgcalls
tdesktop::lib_tgvoip
# tdesktop::lib_tgvoip
# Order in this list defines the order of include paths in command line.
# We need to place desktop-app::external_minizip this early to have its
@ -71,6 +69,7 @@ PRIVATE
tdesktop::td_lang
tdesktop::td_mtproto
tdesktop::td_scheme
tdesktop::td_tde2e
tdesktop::td_ui
desktop-app::lib_webrtc
desktop-app::lib_base
@ -478,6 +477,8 @@ PRIVATE
calls/calls_video_bubble.h
calls/calls_video_incoming.cpp
calls/calls_video_incoming.h
calls/calls_window.cpp
calls/calls_window.h
chat_helpers/compose/compose_features.h
chat_helpers/compose/compose_show.cpp
chat_helpers/compose/compose_show.h
@ -527,6 +528,8 @@ PRIVATE
chat_helpers/ttl_media_layer_widget.h
core/application.cpp
core/application.h
core/bank_card_click_handler.cpp
core/bank_card_click_handler.h
core/base_integration.cpp
core/base_integration.h
core/changelogs.cpp
@ -774,6 +777,8 @@ PRIVATE
dialogs/dialogs_search_from_controllers.h
dialogs/dialogs_search_tags.cpp
dialogs/dialogs_search_tags.h
dialogs/dialogs_top_bar_suggestion.cpp
dialogs/dialogs_top_bar_suggestion.h
dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h
editor/color_picker.cpp
@ -1161,6 +1166,8 @@ PRIVATE
inline_bots/inline_bot_result.h
inline_bots/inline_bot_send_data.cpp
inline_bots/inline_bot_send_data.h
inline_bots/inline_bot_storage.cpp
inline_bots/inline_bot_storage.h
inline_bots/inline_results_inner.cpp
inline_bots/inline_results_inner.h
inline_bots/inline_results_widget.cpp
@ -1507,6 +1514,10 @@ PRIVATE
settings/cloud_password/settings_cloud_password_hint.h
settings/cloud_password/settings_cloud_password_input.cpp
settings/cloud_password/settings_cloud_password_input.h
settings/cloud_password/settings_cloud_password_login_email.cpp
settings/cloud_password/settings_cloud_password_login_email.h
settings/cloud_password/settings_cloud_password_login_email_confirm.cpp
settings/cloud_password/settings_cloud_password_login_email_confirm.h
settings/cloud_password/settings_cloud_password_manage.cpp
settings/cloud_password/settings_cloud_password_manage.h
settings/cloud_password/settings_cloud_password_start.cpp
@ -1618,6 +1629,8 @@ PRIVATE
support/support_preload.h
support/support_templates.cpp
support/support_templates.h
tde2e/tde2e_integration.cpp
tde2e/tde2e_integration.h
ui/boxes/edit_invite_link_session.cpp
ui/boxes/edit_invite_link_session.h
ui/boxes/peer_qr_box.cpp
@ -1814,6 +1827,10 @@ if (WIN32)
# COMMENT
# $<IF:${release},"Appending compatibility manifest.","Finalizing build.">
# )
if (QT_VERSION LESS 6)
target_link_libraries(Telegram PRIVATE desktop-app::win_directx_helper)
endif()
elseif (APPLE)
if (NOT DESKTOP_APP_USE_PACKAGED)
target_link_libraries(Telegram PRIVATE desktop-app::external_iconv)
@ -1991,66 +2008,8 @@ if (MSVC)
)
target_link_options(Telegram
PRIVATE
/DELAYLOAD:secur32.dll
/DELAYLOAD:winmm.dll
/DELAYLOAD:ws2_32.dll
/DELAYLOAD:user32.dll
/DELAYLOAD:gdi32.dll
/DELAYLOAD:advapi32.dll
/DELAYLOAD:shell32.dll
/DELAYLOAD:ole32.dll
/DELAYLOAD:oleaut32.dll
/DELAYLOAD:shlwapi.dll
/DELAYLOAD:iphlpapi.dll
/DELAYLOAD:gdiplus.dll
/DELAYLOAD:version.dll
/DELAYLOAD:dwmapi.dll
/DELAYLOAD:uxtheme.dll
/DELAYLOAD:crypt32.dll
/DELAYLOAD:bcrypt.dll
/DELAYLOAD:netapi32.dll
/DELAYLOAD:imm32.dll
/DELAYLOAD:userenv.dll
/DELAYLOAD:wtsapi32.dll
/DELAYLOAD:propsys.dll
)
if (QT_VERSION GREATER 6)
if (NOT build_winarm)
target_link_options(Telegram PRIVATE
/DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll
)
endif()
target_link_options(Telegram
PRIVATE
/DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll
/DELAYLOAD:API-MS-Win-Core-File-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-LibraryLoader-l1-2-0.dll
/DELAYLOAD:API-MS-Win-Core-Localization-l1-2-0.dll
/DELAYLOAD:API-MS-Win-Core-Memory-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Memory-l1-1-1.dll
/DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib
/DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll
/DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll
# /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll # We shadowed GetDpiForMonitor
/DELAYLOAD:authz.dll # Authz.lib
/DELAYLOAD:comdlg32.dll
/DELAYLOAD:dwrite.dll # DWrite.lib
/DELAYLOAD:dxgi.dll # DXGI.lib
/DELAYLOAD:d3d9.dll # D3D9.lib
/DELAYLOAD:d3d11.dll # D3D11.lib
/DELAYLOAD:d3d12.dll # D3D12.lib
/DELAYLOAD:setupapi.dll # SetupAPI.lib
/DELAYLOAD:winhttp.dll
)
endif()
endif()
target_prepare_qrc(Telegram)
@ -2081,22 +2040,6 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins
base/platform/win/base_windows_safe_library.h
)
target_include_directories(Updater PRIVATE ${lib_base_loc})
if (MSVC)
target_link_libraries(Updater
PRIVATE
delayimp
)
target_link_options(Updater
PRIVATE
/DELAYLOAD:user32.dll
/DELAYLOAD:advapi32.dll
/DELAYLOAD:shell32.dll
/DELAYLOAD:ole32.dll
/DELAYLOAD:shlwapi.dll
)
else()
target_link_options(Updater PRIVATE -municode)
endif()
elseif (APPLE)
add_custom_command(TARGET Updater
PRE_LINK

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -882,6 +882,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_cloud_password_email_confirm" = "Confirm and Finish";
"lng_settings_cloud_password_reset_in" = "You can reset your password in {duration}.";
"lng_settings_cloud_login_email_section_title" = "Login Email";
"lng_settings_cloud_login_email_box_about" = "This email address will be used every time you log in to your Telegram account from a new device.";
"lng_settings_cloud_login_email_box_ok" = "Change email";
"lng_settings_cloud_login_email_title" = "Enter New Email";
"lng_settings_cloud_login_email_placeholder" = "Enter Login Email";
"lng_settings_cloud_login_email_about" = "You will receive Telegram login codes via email and not SMS. Please enter an email address to which you have access.";
"lng_settings_cloud_login_email_confirm" = "Confirm";
"lng_settings_cloud_login_email_code_title" = "Check Your New Email";
"lng_settings_cloud_login_email_code_about" = "Please enter the code we have sent to your new email {email}";
"lng_settings_cloud_login_email_success" = "Your email has been changed.";
"lng_settings_error_email_not_alowed" = "Sorry, this email is not allowed";
"lng_settings_ttl_title" = "Auto-Delete Messages";
"lng_settings_ttl_about" = "Automatically delete messages for everyone after a period of time in all new chats you start.";
"lng_settings_ttl_after" = "After {after_duration}";
@ -1195,6 +1207,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_quick_dialog_action_delete" = "Delete";
"lng_settings_quick_dialog_action_disabled" = "Change folder";
"lng_quick_dialog_action_toast_mute_success" = "Notifications for this chat have been muted.";
"lng_quick_dialog_action_toast_unmute_success" = "Notifications enabled for this chat.";
"lng_quick_dialog_action_toast_pin_success" = "The chat has been pinned.";
"lng_quick_dialog_action_toast_unpin_success" = "The chat has been unpinned.";
"lng_quick_dialog_action_toast_read_success" = "The chat has been marked as read.";
"lng_quick_dialog_action_toast_unread_success" = "The chat has been marked as unread.";
"lng_quick_dialog_action_toast_archive_success" = "The chat has been archived.";
"lng_quick_dialog_action_toast_unarchive_success" = "The chat has been unarchived.";
"lng_settings_generic_subscribe" = "Subscribe to {link} to use this setting.";
"lng_settings_generic_subscribe_link" = "Telegram Premium";
@ -2212,6 +2233,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_you_paid_stars#one" = "You paid {count} Star.";
"lng_you_paid_stars#other" = "You paid {count} Stars.";
"lng_you_joined_group" = "You joined this group";
"lng_similar_channels_title" = "Similar channels";
"lng_similar_channels_view_all" = "View all";
"lng_similar_channels_more" = "More Channels";
@ -3444,6 +3467,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_show_on_channel" = "Display in channel's Gifts";
"lng_gift_availability" = "Availability";
"lng_gift_from_hidden" = "Hidden User";
"lng_gift_subtitle_birthdays" = "Birthdays";
"lng_gift_list_birthday_status_today" = "{emoji} Birthday today";
"lng_gift_list_birthday_status_yesterday" = "Birthday yesterday";
"lng_gift_list_birthday_status_tomorrow" = "Birthday tomorrow";
"lng_gift_self_status" = "buy yourself a gift";
"lng_gift_self_title" = "Buy a Gift";
"lng_gift_self_about" = "Buy yourself a gift to display on your page or reserve for later.\n\nLimited-edition gifts upgraded to collectibles can be gifted to others later.";
@ -3677,6 +3704,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_restrict_users" = "Restrict users";
"lng_delete_all_from_user" = "Delete all from {user}";
"lng_delete_all_from_users" = "Delete all from users";
"lng_restrict_user#one" = "Restrict user";
"lng_restrict_user#other" = "Restrict users";
"lng_restrict_user_part" = "Partially restrict this user {emoji}";
"lng_restrict_users_part" = "Partially restrict users {emoji}";
"lng_restrict_user_full" = "Fully ban this user {emoji}";
@ -3854,6 +3883,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_dialogs_skip_archive_in_search" = "Skip results from archive";
"lng_dialogs_show_archive_in_search" = "With results from archive";
"lng_dialogs_suggestions_birthday_title" = "Add your birthday! 🎂";
"lng_dialogs_suggestions_birthday_about" = "Let your contacts know when youre celebrating.";
"lng_dialogs_suggestions_birthday_contact_title" = "Its {text}'s **birthday** today! 🎂";
"lng_dialogs_suggestions_birthday_contact_about" = "Send them a Gift.";
"lng_dialogs_suggestions_birthday_contacts_title#one" = "{count} contact have **birthdays** today! 🎂";
"lng_dialogs_suggestions_birthday_contacts_title#other" = "{count} contacts have **birthdays** today! 🎂";
"lng_dialogs_suggestions_birthday_contacts_about" = "Send them a Gift.";
"lng_dialogs_suggestions_birthday_contact_dismiss" = "You can send a Gift later in Settings";
"lng_dialogs_suggestions_premium_annual_title" = "Telegram Premium with a {text} discount";
"lng_dialogs_suggestions_premium_annual_about" = "Sign up for the annual payment plan for Telegram Premium now to get the discount.";
"lng_dialogs_suggestions_premium_upgrade_title" = "Telegram Premium with a {text} discount";
"lng_dialogs_suggestions_premium_upgrade_about" = "Upgrade to the annual payment plan for Telegram Premium now to get the discount.";
"lng_dialogs_suggestions_premium_restore_title" = "Get Premium back with up to {text} off";
"lng_dialogs_suggestions_premium_restore_about" = "Your Telegram Premium has recently expired. Tap here to extend it.";
"lng_dialogs_suggestions_premium_grace_title" = "⚠️ Your Premium subscription is expiring!";
"lng_dialogs_suggestions_premium_grace_about" = "Dont lose access to exclusive features.";
"lng_dialogs_suggestions_userpics_title" = "Add your photo! 📸";
"lng_dialogs_suggestions_userpics_about" = "Help your friends spot you easily.";
"lng_dialogs_suggestions_credits_sub_low_title#one" = "{emoji} {count} Star needed for {channels}";
"lng_dialogs_suggestions_credits_sub_low_title#other" = "{emoji} {count} Stars needed for {channels}";
"lng_dialogs_suggestions_credits_sub_low_about" = "Insufficient funds to cover your subscription.";
"lng_about_random" = "Send a {emoji} emoji to any chat to try your luck.";
"lng_about_random_send" = "Send";
@ -4606,6 +4657,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_call_status_failed" = "failed to connect";
"lng_call_status_ringing" = "ringing...";
"lng_call_status_busy" = "line busy";
"lng_call_status_group_invite" = "Telegram Group Call";
"lng_call_status_sure" = "Click on the Camera icon if you want to start a video call.";
"lng_call_fingerprint_tooltip" = "If the emoji on {user}'s screen are the same, this call is 100% secure";
@ -4615,6 +4667,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_call_error_camera_not_started" = "You can switch to video call once you're connected.";
"lng_call_error_camera_outdated" = "{user}'s app does not support video calls. They need to update their app before you can call them.";
"lng_call_error_audio_io" = "There seems to be a problem with your sound card. Please make sure that your computer's speakers and microphone are working and try again.";
"lng_call_error_add_not_started" = "You can add more people once you're connected.";
"lng_call_bar_hangup" = "End call";
"lng_call_leave_to_other_sure" = "End your active call and join this video chat?";
@ -4633,16 +4686,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_call_outgoing" = "Outgoing call";
"lng_call_video_outgoing" = "Outgoing video call";
"lng_call_group_outgoing" = "Outgoing group call";
"lng_call_incoming" = "Incoming call";
"lng_call_video_incoming" = "Incoming video call";
"lng_call_group_incoming" = "Incoming group call";
"lng_call_missed" = "Missed call";
"lng_call_video_missed" = "Missed video call";
"lng_call_group_missed" = "Missed group call";
"lng_call_cancelled" = "Canceled call";
"lng_call_video_cancelled" = "Canceled video call";
"lng_call_declined" = "Declined call";
"lng_call_video_declined" = "Declined video call";
"lng_call_group_declined" = "Declined group call";
"lng_call_duration_info" = "{time}, {duration}";
"lng_call_type_and_duration" = "{type} ({duration})";
"lng_call_invitation" = "Group call invitation";
"lng_call_ongoing" = "Ongoing group call";
"lng_call_rate_label" = "Please rate the quality of your call";
"lng_call_rate_comment" = "Comment (optional)";
@ -4651,6 +4710,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_call_start_video" = "Start Video";
"lng_call_stop_video" = "Stop Video";
"lng_call_screencast" = "Screencast";
"lng_call_add_people" = "Add People";
"lng_call_end_call" = "End Call";
"lng_call_mute_audio" = "Mute";
"lng_call_unmute_audio" = "Unmute";
@ -4682,8 +4742,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_connecting" = "Connecting...";
"lng_group_call_leave" = "Leave";
"lng_group_call_leave_title" = "Leave video chat";
"lng_group_call_leave_title_call" = "Leave group call";
"lng_group_call_leave_title_channel" = "Leave live stream";
"lng_group_call_leave_sure" = "Do you want to leave this video chat?";
"lng_group_call_leave_sure_call" = "Do you want to leave this group call?";
"lng_group_call_leave_sure_channel" = "Are you sure you want to leave this live stream?";
"lng_group_call_close" = "Close";
"lng_group_call_close_sure" = "Video chat is scheduled. You can abort it or just close this panel.";
@ -4713,15 +4775,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_also_end_channel" = "End live stream";
"lng_group_call_settings_title" = "Settings";
"lng_group_call_invite" = "Invite Members";
"lng_group_call_invite_conf" = "Add People";
"lng_group_call_invited_status" = "invited";
"lng_group_call_calling_status" = "calling...";
"lng_group_call_blockchain_only_status" = "listening";
"lng_group_call_muted_by_me_status" = "muted for you";
"lng_group_call_invite_title" = "Invite members";
"lng_group_call_invite_button" = "Invite";
"lng_group_call_confcall_add" = "Call";
"lng_group_call_add_to_group_one" = "{user} isn't a member of «{group}». Add them to the group?";
"lng_group_call_add_to_group_some" = "Some of those users aren't members of «{group}». Add them to the group?";
"lng_group_call_add_to_group_all" = "Those users aren't members of «{group}». Add them to the group?";
"lng_group_call_invite_members" = "Group members";
"lng_group_call_invite_search_results" = "Search results";
"lng_group_call_invite_limit" = "This is currently the maximum allowed number of participants.";
"lng_group_call_new_muted" = "Mute new participants";
"lng_group_call_speakers" = "Speakers";
"lng_group_call_microphone" = "Microphone";
@ -4760,6 +4827,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_context_pin_screen" = "Pin screencast";
"lng_group_call_context_unpin_screen" = "Unpin screencast";
"lng_group_call_context_remove" = "Remove";
"lng_group_call_context_cancel_invite" = "Discard invite";
"lng_group_call_context_stop_ringing" = "Stop calling";
"lng_group_call_context_ban_from_call" = "Ban from call";
"lng_group_call_remove_channel" = "Remove {channel} from the video chat and ban them?";
"lng_group_call_remove_channel_from_channel" = "Remove {channel} from the live stream?";
"lng_group_call_mac_access" = "Telegram Desktop does not have access to system wide keyboard input required for Push to Talk.";
@ -4868,6 +4938,54 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_rtmp_viewers#one" = "{count} viewer";
"lng_group_call_rtmp_viewers#other" = "{count} viewers";
"lng_confcall_join_title" = "Group Call";
"lng_confcall_join_text" = "You are invited to join a Telegram Call.";
"lng_confcall_join_text_inviter" = "{user} is inviting you to join a Telegram Call.";
"lng_confcall_already_joined_one" = "{user} already joined this call.";
"lng_confcall_already_joined_two" = "{user} and {other} already joined this call.";
"lng_confcall_already_joined_three" = "{user}, {other} and {third} already joined this call.";
"lng_confcall_already_joined_many#one" = "{user}, {other} and **{count}** other person already joined this call.";
"lng_confcall_already_joined_many#other" = "{user}, {other} and **{count}** other people already joined this call.";
"lng_confcall_join_button" = "Join Group Call";
"lng_confcall_create_call" = "Start New Call";
"lng_confcall_create_call_description#one" = "You can add up to {count} participant to a call.";
"lng_confcall_create_call_description#other" = "You can add up to {count} participants to a call.";
"lng_confcall_create_title" = "New Call";
"lng_confcall_create_link" = "Create Call Link";
"lng_confcall_create_link_description" = "You can create a link that will allow your friends on Telegram to join the call.";
"lng_confcall_link_revoke" = "Revoke link";
"lng_confcall_link_revoked_title" = "Link Revoked";
"lng_confcall_link_inactive" = "This link is no longer active.";
"lng_confcall_link_revoked_text" = "A new link has been generated.";
"lng_confcall_link_title" = "Call Link";
"lng_confcall_link_about" = "Anyone on Telegram can join your call by following the link below.";
"lng_confcall_link_or" = "or";
"lng_confcall_link_join" = "Be the first to join the call and add people from there. {link}";
"lng_confcall_link_join_link" = "Open call {arrow}";
"lng_confcall_inactive_title" = "Start Group Call";
"lng_confcall_inactive_about" = "This call is no longer active.\nYou can start a new one.";
"lng_confcall_invite_done_user" = "You're calling {user} to join.";
"lng_confcall_invite_done_many#one" = "You're calling **{count} person** to join.";
"lng_confcall_invite_done_many#other" = "You're calling **{count} people** to join.";
"lng_confcall_invite_already_user" = "{user} is already in the call.";
"lng_confcall_invite_already_many#one" = "**{count} person** is already in the call.";
"lng_confcall_invite_already_many#other" = "**{count} people** are already in the call.";
"lng_confcall_invite_privacy_user" = "You cannot call {user} because of their privacy settings.";
"lng_confcall_invite_privacy_many#one" = "You cannot call **{count} person** because of their privacy settings.";
"lng_confcall_invite_privacy_many#other" = "You cannot call **{count} people** because of their privacy settings.";
"lng_confcall_invite_fail_user" = "Couldn't call {user} to join.";
"lng_confcall_invite_fail_many#one" = "Couldn't call **{count} person** to join.";
"lng_confcall_invite_fail_many#other" = "Couldn't call **{count} people** to join.";
"lng_confcall_invite_kicked_user" = "{user} was banned from the call.";
"lng_confcall_invite_kicked_many#one" = "**{count} person** was removed from the call.";
"lng_confcall_invite_kicked_many#other" = "**{count} people** were removed from the call.";
"lng_confcall_not_accessible" = "This call is no longer accessible.";
"lng_confcall_participants_limit" = "This call reached the participants limit.";
"lng_confcall_sure_remove" = "Remove {user} from the call?";
"lng_confcall_e2e_badge" = "End-to-End Encrypted";
"lng_confcall_e2e_badge_small" = "E2E Encrypted";
"lng_confcall_e2e_about" = "These four emoji represent the call's encryption key. They must match for all participants and change when someone joins or leaves.";
"lng_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages.";
"lng_player_message_today" = "today at {time}";
@ -5794,6 +5912,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_view_button_stickerset" = "View stickers";
"lng_view_button_emojipack" = "View emoji";
"lng_view_button_collectible" = "View collectible";
"lng_view_button_call" = "Join call";
"lng_sponsored_hide_ads" = "Hide";
"lng_sponsored_title" = "What are sponsored messages?";
@ -6185,6 +6304,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_bot_earn_chart_revenue" = "Revenue";
"lng_bot_earn_overview_title" = "Proceeds overview";
"lng_bot_earn_available" = "Available balance";
"lng_bot_earn_reward" = "Total balance";
"lng_bot_earn_total" = "Total lifetime proceeds";
"lng_bot_earn_balance_title" = "Available balance";
"lng_bot_earn_balance_about" = "Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned.";
@ -6320,6 +6440,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_frozen_text3" = "Appeal via {link} before {date}, or your account will be deleted.";
"lng_frozen_appeal_button" = "Submit an Appeal";
"lng_context_bank_card_copy" = "Copy Card Number";
"lng_context_bank_card_copied" = "Card number copied to clipboard.";
// Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program...";

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.13.1.0" />
Version="5.14.1.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,13,1,0
PRODUCTVERSION 5,13,1,0
FILEVERSION 5,14,1,0
PRODUCTVERSION 5,14,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "5.13.1.0"
VALUE "FileVersion", "5.14.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.13.1.0"
VALUE "ProductVersion", "5.14.1.0"
END
END
BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,13,1,0
PRODUCTVERSION 5,13,1,0
FILEVERSION 5,14,1,0
PRODUCTVERSION 5,14,1,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "5.13.1.0"
VALUE "FileVersion", "5.14.1.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.13.1.0"
VALUE "ProductVersion", "5.14.1.0"
END
END
BLOCK "VarFileInfo"

View file

@ -544,4 +544,38 @@ auto CloudPassword::checkRecoveryEmailAddressCode(const QString &code)
};
}
void RequestLoginEmailCode(
MTP::Sender &api,
const QString &sendToEmail,
Fn<void(int length, const QString &pattern)> done,
Fn<void(const QString &error)> fail) {
api.request(MTPaccount_SendVerifyEmailCode(
MTP_emailVerifyPurposeLoginChange(),
MTP_string(sendToEmail)
)).done([=](const MTPaccount_SentEmailCode &result) {
done(result.data().vlength().v, qs(result.data().vemail_pattern()));
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
void VerifyLoginEmail(
MTP::Sender &api,
const QString &code,
Fn<void()> done,
Fn<void(const QString &error)> fail) {
api.request(MTPaccount_VerifyEmail(
MTP_emailVerifyPurposeLoginChange(),
MTP_emailVerificationCode(MTP_string(code))
)).done([=](const MTPaccount_EmailVerified &result) {
result.match([=](const MTPDaccount_emailVerified &data) {
done();
}, [=](const MTPDaccount_emailVerifiedLogin &data) {
fail(QString());
});
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
} // namespace Api

View file

@ -70,4 +70,15 @@ private:
};
void RequestLoginEmailCode(
MTP::Sender &api,
const QString &sendToEmail,
Fn<void(int length, const QString &pattern)> done,
Fn<void(const QString &error)> fail);
void VerifyLoginEmail(
MTP::Sender &api,
const QString &code,
Fn<void()> done,
Fn<void(const QString &error)> fail);
} // namespace Api

View file

@ -348,12 +348,15 @@ void CreditsHistory::request(
void CreditsHistory::requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done) {
Fn<void(Data::CreditsStatusSlice)> done,
bool missingBalance) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPpayments_GetStarsSubscriptions(
MTP_flags(0),
MTP_flags(missingBalance
? MTPpayments_getStarsSubscriptions::Flag::f_missing_balance
: MTPpayments_getStarsSubscriptions::Flags(0)),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token)
)).done([=](const MTPpayments_StarsStatus &result) {

View file

@ -82,7 +82,8 @@ public:
Fn<void(Data::CreditsStatusSlice)> done);
void requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
Fn<void(Data::CreditsStatusSlice)> done,
bool missingBalance = false);
private:
using HistoryTL = MTPpayments_GetStarsTransactions;

View file

@ -28,8 +28,8 @@ constexpr auto kSearchPerPage = 50;
auto result = MessageIdsList();
for (const auto &message : messages) {
const auto peerId = PeerFromMessage(message);
if (const auto peer = data->peerLoaded(peerId)) {
if (const auto lastDate = DateFromMessage(message)) {
if (data->peerLoaded(peerId)) {
if (DateFromMessage(message)) {
const auto item = data->addNewMessage(
message,
MessageFlags(),

View file

@ -25,6 +25,7 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption(
* kDiscountDivider;
}();
return {
.months = months,
.duration = Ui::FormatTTL(months * 86400 * 31),
.discount = (discount > 0)
? QString::fromUtf8("\xe2\x88\x92%1%").arg(discount)
@ -32,6 +33,9 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption(
.costPerMonth = Ui::FillAmountAndCurrency(
amount / float64(months),
currency),
.costNoDiscount = Ui::FillAmountAndCurrency(
monthlyAmount * months,
currency),
.costTotal = Ui::FillAmountAndCurrency(amount, currency),
.botUrl = botUrl,
};

View file

@ -313,7 +313,7 @@ void PublicForwards::request(
const auto msgId = IdFromMessage(message);
const auto peerId = PeerFromMessage(message);
const auto lastDate = DateFromMessage(message);
if (const auto peer = owner.peerLoaded(peerId)) {
if (owner.peerLoaded(peerId)) {
if (!lastDate) {
return;
}

View file

@ -202,7 +202,11 @@ EntitiesInText EntitiesFromMTP(
d.vlength().v,
});
}, [&](const MTPDmessageEntityBankCard &d) {
// Skipping cards. // #TODO entities
result.push_back({
EntityType::BankCard,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntitySpoiler &d) {
result.push_back({
EntityType::Spoiler,
@ -273,6 +277,9 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
case EntityType::Phone: {
v.push_back(MTP_messageEntityPhone(offset, length));
} break;
case EntityType::BankCard: {
v.push_back(MTP_messageEntityBankCard(offset, length));
} break;
case EntityType::Hashtag: {
v.push_back(MTP_messageEntityHashtag(offset, length));
} break;

View file

@ -307,14 +307,19 @@ void Updates::feedUpdateVector(
auto list = updates.v;
const auto hasGroupCallParticipantUpdates = ranges::contains(
list,
mtpc_updateGroupCallParticipants,
&MTPUpdate::type);
true,
[](const MTPUpdate &update) {
return update.type() == mtpc_updateGroupCallParticipants
|| update.type() == mtpc_updateGroupCallChainBlocks;
});
if (hasGroupCallParticipantUpdates) {
ranges::stable_sort(list, std::less<>(), [](const MTPUpdate &entry) {
if (entry.type() == mtpc_updateGroupCallParticipants) {
if (entry.type() == mtpc_updateGroupCallChainBlocks) {
return 0;
} else {
} else if (entry.type() == mtpc_updateGroupCallParticipants) {
return 1;
} else {
return 2;
}
});
} else if (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants) {
@ -328,7 +333,8 @@ void Updates::feedUpdateVector(
if ((policy == SkipUpdatePolicy::SkipMessageIds
&& type == mtpc_updateMessageID)
|| (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants
&& type != mtpc_updateGroupCallParticipants)) {
&& type != mtpc_updateGroupCallParticipants
&& type != mtpc_updateGroupCallChainBlocks)) {
continue;
}
feedUpdate(entry);
@ -958,7 +964,8 @@ void Updates::applyGroupCallParticipantUpdates(const MTPUpdates &updates) {
data.vupdates(),
SkipUpdatePolicy::SkipExceptGroupCallParticipants);
}, [&](const MTPDupdateShort &data) {
if (data.vupdate().type() == mtpc_updateGroupCallParticipants) {
if (data.vupdate().type() == mtpc_updateGroupCallParticipants
|| data.vupdate().type() == mtpc_updateGroupCallChainBlocks) {
feedUpdate(data.vupdate());
}
}, [](const auto &) {
@ -1317,7 +1324,7 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) {
user->madeAction(base::unixtime::now());
}
}
ClearMediaAsExpired(item);
item->clearMediaAsExpired();
}
} else {
// Perhaps it was an unread mention!
@ -2118,6 +2125,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updatePhoneCall:
case mtpc_updatePhoneCallSignalingData:
case mtpc_updateGroupCallParticipants:
case mtpc_updateGroupCallChainBlocks:
case mtpc_updateGroupCallConnection:
case mtpc_updateGroupCall: {
Core::App().calls().handleUpdate(&session(), update);

View file

@ -24,6 +24,14 @@ UserpicButton {
uploadIcon: icon;
uploadIconPosition: point;
}
UserpicsRow {
button: UserpicButton;
bg: color;
shift: pixels;
stroke: pixels;
complex: bool;
invert: bool;
}
ShortInfoBox {
label: FlatLabel;
labeled: FlatLabel;
@ -1129,3 +1137,10 @@ foldersMenu: PopupMenu(popupMenuWithIcons) {
itemPadding: margins(54px, 8px, 44px, 8px);
}
}
fakeUserpicButton: UserpicButton(defaultUserpicButton) {
size: size(1px, 1px);
photoSize: 1px;
changeIcon: icon {{ "settings/photo", transparent }};
uploadBg: transparent;
}

View file

@ -371,7 +371,9 @@ void CreateModerateMessagesBox(
box,
rpl::conditional(
ownedWrap->toggledValue(),
tr::lng_context_restrict_user(),
tr::lng_restrict_user(
lt_count,
rpl::single(participants.size()) | tr::to_count()),
rpl::conditional(
rpl::single(isSingle),
tr::lng_ban_user(),

View file

@ -142,15 +142,16 @@ void PeerListBox::createMultiSelect() {
}
});
_select->resizeToWidth(_controller->contentWidth());
_select->moveToLeft(0, 0);
_select->moveToLeft(0, topSelectSkip());
}
void PeerListBox::appendQueryChangedCallback(Fn<void(QString)> callback) {
_customQueryChangedCallback = std::move(callback);
}
void PeerListBox::setAddedTopScrollSkip(int skip) {
void PeerListBox::setAddedTopScrollSkip(int skip, bool aboveSearch) {
_addedTopScrollSkip = skip;
_addedTopScrollAboveSearch = aboveSearch;
_scrollBottomFixed = false;
updateScrollSkips();
}
@ -159,7 +160,7 @@ void PeerListBox::showFinished() {
_controller->showFinished();
}
int PeerListBox::getTopScrollSkip() const {
int PeerListBox::topScrollSkip() const {
auto result = _addedTopScrollSkip;
if (_select && !_select->isHidden()) {
result += _select->height();
@ -167,14 +168,21 @@ int PeerListBox::getTopScrollSkip() const {
return result;
}
int PeerListBox::topSelectSkip() const {
return _addedTopScrollAboveSearch ? _addedTopScrollSkip : 0;
}
void PeerListBox::updateScrollSkips() {
// If we show / hide the search field scroll top is fixed.
// If we resize search field by bubbles scroll bottom is fixed.
setInnerTopSkip(getTopScrollSkip(), _scrollBottomFixed);
if (_select && !_select->animating()) {
setInnerTopSkip(topScrollSkip(), _scrollBottomFixed);
if (_select) {
_select->moveToLeft(0, topSelectSkip());
if (!_select->animating()) {
_scrollBottomFixed = true;
}
}
}
void PeerListBox::prepare() {
setContent(setInnerWidget(
@ -236,8 +244,6 @@ void PeerListBox::resizeEvent(QResizeEvent *e) {
if (_select) {
_select->resizeToWidth(width());
_select->moveToLeft(0, 0);
updateScrollSkips();
}
@ -1995,9 +2001,13 @@ PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
_selected.index.value = newSelectedIndex;
_selected.element = 0;
if (newSelectedIndex >= 0) {
auto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : 0;
auto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : _aboveHeight;
auto bottom = (newSelectedIndex + 1 < rowsCount) ? getRowTop(RowIndex(newSelectedIndex + 1)) : height();
_scrollToRequests.fire({ top, bottom });
} else if (!_selected.index.value && direction < 0) {
auto top = 0;
auto bottom = _aboveHeight;
_scrollToRequests.fire({ top, bottom });
}
update();

View file

@ -1142,7 +1142,7 @@ public:
void peerListScrollToTop() override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
void setAddedTopScrollSkip(int skip);
void setAddedTopScrollSkip(int skip, bool aboveSearch = false);
void showFinished() override;
@ -1178,7 +1178,8 @@ private:
PaintRoundImageCallback paintUserpic,
anim::type animated);
void createMultiSelect();
int getTopScrollSkip() const;
[[nodiscard]] int topScrollSkip() const;
[[nodiscard]] int topSelectSkip() const;
void updateScrollSkips();
void searchQueryChanged(const QString &query);
@ -1189,6 +1190,7 @@ private:
std::unique_ptr<PeerListController> _controller;
Fn<void(PeerListBox*)> _init;
bool _scrollBottomFixed = false;
bool _addedTopScrollAboveSearch = false;
int _addedTopScrollSkip = 0;
};

View file

@ -179,6 +179,7 @@ void FillUpgradeToPremiumCover(
CreateUserpicsWithMoreBadge(
container,
rpl::single(std::move(userpicPeers)),
st::boostReplaceUserpicsRow,
kUserpicsLimit),
st::inviteForbiddenUserpicsPadding)
)->entity()->setAttribute(Qt::WA_TransparentForMouseEvents);
@ -808,7 +809,7 @@ void AddParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {
updateTitle();
} else if (const auto channel = _peer ? _peer->asChannel() : nullptr) {
if (!_peer->isMegagroup()) {
showBox(Box<MaxInviteBox>(_peer->asChannel()));
showBox(Box<MaxInviteBox>(channel));
}
} else if (count >= serverConfig.chatSizeMax
&& count < serverConfig.megagroupSizeMax) {

View file

@ -453,7 +453,7 @@ void ChoosePeerBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto onstack = callback;
onstack({ peer });
};
if (const auto user = peer->asUser()) {
if (peer->isUser()) {
done();
} else {
delegate()->peerListUiShow()->showBox(

View file

@ -689,7 +689,7 @@ UserData *ParticipantsAdditionalData::applyAdmin(
const auto user = _peer->owner().userLoaded(data.userId());
if (!user) {
return nullptr;
} else if (const auto chat = _peer->asChat()) {
} else if (_peer->isChat()) {
// This can come from saveAdmin callback.
_admins.emplace(user);
return user;
@ -733,7 +733,7 @@ UserData *ParticipantsAdditionalData::applyRegular(UserId userId) {
const auto user = _peer->owner().userLoaded(userId);
if (!user) {
return nullptr;
} else if (const auto chat = _peer->asChat()) {
} else if (_peer->isChat()) {
// This can come from saveAdmin or saveRestricted callback.
_admins.erase(user);
return user;
@ -913,7 +913,7 @@ void ParticipantsBoxController::setupListChangeViewers() {
return;
}
}
if (const auto row = delegate()->peerListFindRow(user->id.value)) {
if (delegate()->peerListFindRow(user->id.value)) {
delegate()->peerListPartitionRows([&](const PeerListRow &row) {
return (row.peer() == user);
});

View file

@ -361,7 +361,7 @@ bool ProcessCurrent(
&& peer->asUser()->hasPersonalPhoto())
? tr::lng_profile_photo_by_you(tr::now)
: ((state->current.index == (state->current.count - 1))
&& SyncUserFallbackPhotoViewer(peer->asUser()))
&& SyncUserFallbackPhotoViewer(peer->asUser()) == state->photoId)
? tr::lng_profile_public_photo(tr::now)
: QString();
state->waitingLoad = false;

View file

@ -550,13 +550,13 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
rpl::variable<int> count = 0;
bool painting = false;
};
const auto full = st::boostReplaceUserpic.size.height()
const auto st = &st::boostReplaceUserpicsRow;
const auto full = st->button.size.height()
+ st::boostReplaceIconAdd.y()
+ st::lineWidth;
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
const auto raw = result.data();
const auto &st = st::boostReplaceUserpic;
const auto right = CreateChild<Ui::UserpicButton>(raw, to, st);
const auto right = CreateChild<Ui::UserpicButton>(raw, to, st->button);
const auto overlay = CreateChild<Ui::RpWidget>(raw);
const auto state = raw->lifetime().make_state<State>();
@ -564,7 +564,6 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
from
) | rpl::start_with_next([=](
const std::vector<not_null<PeerData*>> &list) {
const auto &st = st::boostReplaceUserpic;
auto was = base::take(state->from);
auto buttons = base::take(state->buttons);
state->from.reserve(list.size());
@ -578,7 +577,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
state->buttons.push_back(std::move(buttons[index]));
} else {
state->buttons.push_back(
std::make_unique<Ui::UserpicButton>(raw, peer, st));
std::make_unique<Ui::UserpicButton>(raw, peer, st->button));
const auto raw = state->buttons.back().get();
base::install_event_filter(raw, [=](not_null<QEvent*> e) {
return (e->type() == QEvent::Paint && !state->painting)
@ -598,7 +597,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
const auto skip = st::boostReplaceUserpicsSkip;
const auto left = width - 2 * right->width() - skip;
const auto shift = std::min(
st::boostReplaceUserpicsShift,
st->shift,
(count > 1 ? (left / (count - 1)) : width));
const auto total = right->width()
+ (count ? (skip + right->width() + (count - 1) * shift) : 0);
@ -630,7 +629,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
auto q = QPainter(&state->layer);
auto hq = PainterHighQualityEnabler(q);
const auto stroke = st::boostReplaceIconOutline;
const auto stroke = st->stroke;
const auto half = stroke / 2.;
auto pen = st::windowBg->p;
pen.setWidthF(stroke * 2.);
@ -684,6 +683,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> peers,
const style::UserpicsRow &st,
int limit) {
struct State {
std::vector<not_null<PeerData*>> from;
@ -693,9 +693,8 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
rpl::variable<int> count = 0;
bool painting = false;
};
const auto full = st::boostReplaceUserpic.size.height()
+ st::boostReplaceIconAdd.y()
+ st::lineWidth;
const auto full = st.button.size.height()
+ (st.complex ? (st::boostReplaceIconAdd.y() + st::lineWidth) : 0);
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
const auto raw = result.data();
const auto overlay = CreateChild<Ui::RpWidget>(raw);
@ -703,9 +702,8 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
const auto state = raw->lifetime().make_state<State>();
std::move(
peers
) | rpl::start_with_next([=](
) | rpl::start_with_next([=, &st](
const std::vector<not_null<PeerData*>> &list) {
const auto &st = st::boostReplaceUserpic;
auto was = base::take(state->from);
auto buttons = base::take(state->buttons);
state->from.reserve(list.size());
@ -719,7 +717,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
state->buttons.push_back(std::move(buttons[index]));
} else {
state->buttons.push_back(
std::make_unique<Ui::UserpicButton>(raw, peer, st));
std::make_unique<Ui::UserpicButton>(raw, peer, st.button));
const auto raw = state->buttons.back().get();
base::install_event_filter(raw, [=](not_null<QEvent*> e) {
return (e->type() == QEvent::Paint && !state->painting)
@ -732,16 +730,21 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
overlay->update();
}, raw->lifetime());
if (const auto count = state->count.current()) {
const auto single = st.button.size.width();
const auto used = std::min(count, int(state->buttons.size()));
const auto shift = st.shift;
raw->resize(used ? (single + (used - 1) * shift) : 0, raw->height());
}
rpl::combine(
raw->widthValue(),
state->count.value()
) | rpl::start_with_next([=](int width, int count) {
const auto &st = st::boostReplaceUserpic;
const auto single = st.size.width();
) | rpl::start_with_next([=, &st](int width, int count) {
const auto single = st.button.size.width();
const auto left = width - single;
const auto used = std::min(count, int(state->buttons.size()));
const auto shift = std::min(
st::boostReplaceUserpicsShift,
st.shift,
(used > 1 ? (left / (used - 1)) : width));
const auto total = used ? (single + (used - 1) * shift) : 0;
auto x = (width - total) / 2;
@ -755,7 +758,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
overlay->paintRequest(
) | rpl::filter([=] {
return !state->buttons.empty();
}) | rpl::start_with_next([=] {
}) | rpl::start_with_next([=, &st] {
const auto outerw = overlay->width();
const auto ratio = style::DevicePixelRatio();
if (state->layer.size() != QSize(outerw, full) * ratio) {
@ -768,19 +771,33 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
auto q = QPainter(&state->layer);
auto hq = PainterHighQualityEnabler(q);
const auto stroke = st::boostReplaceIconOutline;
const auto stroke = st.stroke;
const auto half = stroke / 2.;
auto pen = st::windowBg->p;
auto pen = st.bg->p;
pen.setWidthF(stroke * 2.);
state->painting = true;
for (const auto &button : state->buttons) {
const auto paintOne = [&](not_null<Ui::UserpicButton*> button) {
q.setPen(pen);
q.setBrush(Qt::NoBrush);
q.drawEllipse(button->geometry());
const auto position = button->pos();
button->render(&q, position, QRegion(), QWidget::DrawChildren);
};
if (st.invert) {
for (const auto &button : ranges::views::reverse(state->buttons)) {
paintOne(button.get());
}
} else {
for (const auto &button : state->buttons) {
paintOne(button.get());
}
}
state->painting = false;
const auto text = (state->count.current() > limit)
? ('+' + QString::number(state->count.current() - limit))
: QString();
if (st.complex && !text.isEmpty()) {
const auto last = state->buttons.back().get();
const auto add = st::boostReplaceIconAdd;
const auto skip = st::boostReplaceIconSkip;
@ -788,11 +805,6 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
const auto h = st::boostReplaceIcon.height() + 2 * skip;
const auto x = last->x() + last->width() - w + add.x();
const auto y = last->y() + last->height() - h + add.y();
const auto text = (state->count.current() > limit)
? ('+' + QString::number(state->count.current() - limit))
: QString();
if (!text.isEmpty()) {
const auto &font = st::semiboldFont;
const auto width = font->width(text);
const auto padded = std::max(w, width + 2 * font->spacew);

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/object_ptr.h"
namespace style {
struct UserpicsRow;
} // namespace style
class ChannelData;
namespace Main {
@ -66,4 +70,5 @@ enum class UserpicsTransferType {
[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
not_null<Ui::RpWidget*> parent,
rpl::producer<std::vector<not_null<PeerData*>>> peers,
const style::UserpicsRow &st,
int limit);

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "boxes/peers/edit_peer_color_box.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "boxes/gift_premium_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/premium_preview_box.h"
@ -29,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/data_birthday.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_credits.h"
@ -69,6 +71,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/new_badges.h"
#include "ui/painter.h"
@ -91,6 +94,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_premium.h"
@ -117,6 +121,13 @@ constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
using namespace HistoryView;
using namespace Info::PeerGifts;
enum class PickType {
Activate,
SendMessage,
OpenProfile,
};
using PickCallback = Fn<void(not_null<PeerData*>, PickType)>;
struct PremiumGiftsDescriptor {
std::vector<GiftTypePremium> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
@ -141,6 +152,80 @@ struct GiftDetails {
bool byStars = false;
};
class PeerRow final : public PeerListRow {
public:
using PeerListRow::PeerListRow;
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
void rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) override;
void rightActionStopLastRipple() override;
private:
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
};
QSize PeerRow::rightActionSize() const {
return QSize(
st::inviteLinkThreeDotsIcon.width(),
st::inviteLinkThreeDotsIcon.height());
}
QMargins PeerRow::rightActionMargins() const {
return QMargins(
0,
(st::inviteLinkList.item.height - rightActionSize().height()) / 2,
st::inviteLinkThreeDotsSkip,
0);
}
void PeerRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (_actionRipple) {
_actionRipple->paint(p, x, y, outerWidth);
if (_actionRipple->empty()) {
_actionRipple.reset();
}
}
(actionSelected
? st::inviteLinkThreeDotsIconOver
: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);
}
void PeerRow::rightActionAddRipple(QPoint point, Fn<void()> updateCallback) {
if (!_actionRipple) {
auto mask = Ui::RippleAnimation::EllipseMask(
Size(st::inviteLinkThreeDotsIcon.height()));
_actionRipple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
std::move(updateCallback));
}
_actionRipple->add(point);
}
void PeerRow::rightActionStopLastRipple() {
if (_actionRipple) {
_actionRipple->lastStop();
}
}
class PreviewDelegate final : public DefaultElementDelegate {
public:
PreviewDelegate(
@ -1793,7 +1878,7 @@ void SendGiftBox(
ShowSentToast(window, details.descriptor, details);
}
if (const auto strong = weak.data()) {
box->closeBox();
strong->closeBox();
}
};
SendGift(window, peer, api, details, done);
@ -2191,10 +2276,10 @@ void GiftBox(
&& uniqueDisallowed;
content->add(
object_ptr<CenterWrap<>>(
object_ptr<CenterWrap<UserpicButton>>(
content,
object_ptr<UserpicButton>(content, peer, stUser))
)->setAttribute(Qt::WA_TransparentForMouseEvents);
)->entity()->setClickedCallback([=] { window->showPeerInfo(peer); });
AddSkip(content);
AddSkip(content);
@ -2257,24 +2342,46 @@ void GiftBox(
}
}
struct SelfOption {
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> CreateRowContextMenu(
QWidget *parent,
not_null<PeerData*> peer,
PickCallback pick) {
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
result->addAction(
tr::lng_context_send_message(tr::now),
[=] { pick(peer, PickType::SendMessage); },
&st::menuIconChatBubble);
result->addAction(
tr::lng_context_view_profile(tr::now),
[=] { pick(peer, PickType::OpenProfile); },
&st::menuIconProfile);
return result;
}
struct CustomList {
object_ptr<Ui::RpWidget> content = { nullptr };
Fn<bool(int, int, int)> overrideKey;
Fn<void()> activate;
Fn<bool()> hasSelection;
};
class Controller final : public ContactsBoxController {
public:
Controller(
not_null<Main::Session*> session,
Fn<void(not_null<PeerData*>)> choose);
Controller(not_null<Main::Session*> session, PickCallback pick);
void noSearchSubmit();
bool overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) override;
int toIndex) override final;
void rowRightActionClicked(not_null<PeerListRow*> row) override final;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override final;
private:
std::unique_ptr<PeerListRow> createRow(
@ -2283,41 +2390,71 @@ private:
void prepareViewHook() override;
void rowClicked(not_null<PeerListRow*> row) override;
const Fn<void(not_null<PeerData*>)> _choose;
SelfOption _selfOption;
const PickCallback _pick;
const std::vector<UserId> _contactBirthdays;
CustomList _selfOption;
CustomList _birthdayOptions;
base::unique_qptr<Ui::PopupMenu> _menu;
bool _skipUpDirectionSelect = false;
};
[[nodiscard]] SelfOption MakeSelfOption(
[[nodiscard]] CustomList MakeCustomList(
not_null<Main::Session*> session,
Fn<void()> activate) {
class SelfController final : public PeerListController {
Fn<void(not_null<PeerListController*>)> fill,
PickCallback pick,
rpl::producer<QString> below) {
class CustomController final : public PeerListController {
public:
SelfController(
CustomController(
not_null<Main::Session*> session,
Fn<void()> activate)
Fn<void(not_null<PeerListController*>)> fill,
PickCallback pick)
: _session(session)
, _activate(std::move(activate)) {
, _pick(std::move(pick))
, _fill(std::move(fill)) {
}
void prepare() override {
auto row = std::make_unique<PeerListRow>(_session->user());
row->setCustomStatus(tr::lng_gift_self_status(tr::now));
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListRefreshRows();
if (_fill) {
_fill(this);
}
}
void loadMoreRows() override {
}
void rowClicked(not_null<PeerListRow*> row) override {
_activate();
_pick(row->peer(), PickType::Activate);
}
Main::Session &session() const override {
return *_session;
}
void rowRightActionClicked(not_null<PeerListRow*> row) override {
delegate()->peerListShowRowMenu(row, true);
}
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override {
auto result = CreateRowContextMenu(parent, row->peer(), _pick);
if (result) {
base::take(_menu);
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
}
return result;
}
private:
const not_null<Main::Session*> _session;
Fn<void()> _activate;
PickCallback _pick;
Fn<void(not_null<PeerListController*>)> _fill;
base::unique_qptr<Ui::PopupMenu> _menu;
};
@ -2329,9 +2466,12 @@ private:
const auto delegate = container->lifetime().make_state<
PeerListContentDelegateSimple
>();
const auto controller = container->lifetime().make_state<
SelfController
>(session, activate);
const auto controller
= container->lifetime().make_state<CustomController>(
session,
fill,
pick);
controller->setStyleOverrides(&st::peerListSingleRow);
const auto content = container->add(object_ptr<PeerListContent>(
container,
@ -2339,10 +2479,12 @@ private:
delegate->setContent(content);
controller->setDelegate(delegate);
if (below) {
Ui::AddSkip(container);
container->add(CreatePeerListSectionSubtitle(
container,
tr::lng_contacts_header()));
std::move(below)));
}
const auto overrideKey = [=](int direction, int from, int to) {
if (!content->isVisible()) {
@ -2368,64 +2510,228 @@ private:
}
return false;
};
const auto hasSelection = [=] {
return content->isVisible() && content->hasSelection();
};
return {
.content = std::move(result),
.overrideKey = overrideKey,
.activate = activate,
.activate = [=] {
if (content->hasSelection()) {
pick(
content->rowAt(content->selectedIndex())->peer(),
PickType::Activate);
}
},
.hasSelection = hasSelection,
};
}
Controller::Controller(
not_null<Main::Session*> session,
Fn<void(not_null<PeerData*>)> choose)
Controller::Controller(not_null<Main::Session*> session, PickCallback pick)
: ContactsBoxController(session)
, _choose(std::move(choose))
, _selfOption(MakeSelfOption(session, [=] { _choose(session->user()); })) {
, _pick(std::move(pick))
, _contactBirthdays(
session->data().knownContactBirthdays().value_or(std::vector<UserId>{}))
, _selfOption(
MakeCustomList(
session,
[=](not_null<PeerListController*> controller) {
auto row = std::make_unique<PeerListRow>(session->user());
row->setCustomStatus(tr::lng_gift_self_status(tr::now));
controller->delegate()->peerListAppendRow(std::move(row));
controller->delegate()->peerListRefreshRows();
},
_pick,
_contactBirthdays.empty()
? tr::lng_contacts_header()
: tr::lng_gift_subtitle_birthdays()))
, _birthdayOptions(
MakeCustomList(
session,
[=](not_null<PeerListController*> controller) {
const auto status = [=](const Data::Birthday &date) {
if (Data::IsBirthdayToday(date)) {
return tr::lng_gift_list_birthday_status_today(
tr::now,
lt_emoji,
Data::BirthdayCake());
}
const auto yesterday = QDate::currentDate().addDays(-1);
const auto tomorrow = QDate::currentDate().addDays(1);
if (date.day() == yesterday.day()
&& date.month() == yesterday.month()) {
return tr::lng_gift_list_birthday_status_yesterday(
tr::now);
} else if (date.day() == tomorrow.day()
&& date.month() == tomorrow.month()) {
return tr::lng_gift_list_birthday_status_tomorrow(
tr::now);
}
return QString();
};
auto usersWithBirthdays = ranges::views::all(
_contactBirthdays
) | ranges::views::transform([&](UserId userId) {
return session->data().user(userId);
}) | ranges::to_vector;
ranges::sort(usersWithBirthdays, [](UserData *a, UserData *b) {
const auto aBirthday = a->birthday();
const auto bBirthday = b->birthday();
const auto aIsToday = Data::IsBirthdayToday(aBirthday);
const auto bIsToday = Data::IsBirthdayToday(bBirthday);
if (aIsToday != bIsToday) {
return aIsToday > bIsToday;
}
if (aBirthday.month() != bBirthday.month()) {
return aBirthday.month() < bBirthday.month();
}
return aBirthday.day() < bBirthday.day();
});
for (const auto user : usersWithBirthdays) {
auto row = std::make_unique<PeerRow>(user);
if (auto s = status(user->birthday()); !s.isEmpty()) {
row->setCustomStatus(std::move(s));
}
controller->delegate()->peerListAppendRow(std::move(row));
}
controller->delegate()->peerListRefreshRows();
},
_pick,
_contactBirthdays.empty()
? rpl::producer<QString>(nullptr)
: tr::lng_contacts_header())) {
setStyleOverrides(&st::peerListSmallSkips);
}
void Controller::rowRightActionClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowRowMenu(row, true);
}
base::unique_qptr<Ui::PopupMenu> Controller::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
auto result = CreateRowContextMenu(parent, row->peer(), _pick);
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;
}
void Controller::noSearchSubmit() {
if (const auto onstack = _selfOption.activate) {
onstack();
}
if (const auto onstack = _birthdayOptions.activate) {
onstack();
}
}
bool Controller::overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) {
return _selfOption.overrideKey
&& _selfOption.overrideKey(direction, fromIndex, toIndex);
int from,
int to) {
if (direction == -1 && from == -1 && to == -1 && _skipUpDirectionSelect) {
return true;
}
_skipUpDirectionSelect = false;
if (direction > 0) {
if (!_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
return _selfOption.overrideKey(direction, from, to);
}
if (_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
if (_selfOption.overrideKey(direction, from, to)) {
return true;
} else {
return _birthdayOptions.overrideKey(direction, from, to);
}
}
if (!_selfOption.hasSelection() && _birthdayOptions.hasSelection()) {
if (_birthdayOptions.overrideKey(direction, from, to)) {
return true;
}
}
} else if (direction < 0) {
if (!_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
return _birthdayOptions.overrideKey(direction, from, to);
}
if (!_selfOption.hasSelection() && _birthdayOptions.hasSelection()) {
if (_birthdayOptions.overrideKey(direction, from, to)) {
return true;
} else if (!_birthdayOptions.hasSelection()) {
const auto res = _selfOption.overrideKey(direction, from, to);
_skipUpDirectionSelect = _selfOption.hasSelection();
return res;
}
}
if (_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
if (_selfOption.overrideKey(direction, from, to)) {
_skipUpDirectionSelect = _selfOption.hasSelection();
return true;
}
}
}
return false;
}
std::unique_ptr<PeerListRow> Controller::createRow(
not_null<UserData*> user) {
if (const auto birthday = user->owner().knownContactBirthdays()) {
if (ranges::contains(*birthday, peerToUser(user->id))) {
return nullptr;
}
}
if (user->isSelf()
|| user->isBot()
|| user->isServiceUser()
|| user->isInaccessible()) {
return nullptr;
}
return ContactsBoxController::createRow(user);
return std::make_unique<PeerRow>(user);
}
void Controller::prepareViewHook() {
delegate()->peerListSetAboveWidget(std::move(_selfOption.content));
auto list = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
list->add(std::move(_selfOption.content));
list->add(std::move(_birthdayOptions.content));
delegate()->peerListSetAboveWidget(std::move(list));
}
void Controller::rowClicked(not_null<PeerListRow*> row) {
_choose(row->peer());
_pick(row->peer(), PickType::Activate);
}
} // namespace
void ChooseStarGiftRecipient(
not_null<Window::SessionController*> window) {
const auto session = &window->session();
const auto lifetime = std::make_shared<rpl::lifetime>();
session->data().contactBirthdays(
) | rpl::start_with_next(crl::guard(session, [=] {
lifetime->destroy();
auto controller = std::make_unique<Controller>(
&window->session(),
[=](not_null<PeerData*> peer) {
session,
[=](not_null<PeerData*> peer, PickType type) {
if (type == PickType::Activate) {
ShowStarGiftBox(window, peer);
} else if (type == PickType::SendMessage) {
using Way = Window::SectionShow::Way;
window->showPeerHistory(peer, Way::Forward);
} else if (type == PickType::OpenProfile) {
window->show(PrepareShortInfoBox(peer, window));
}
});
const auto controllerRaw = controller.get();
auto initBox = [=](not_null<PeerListBox*> box) {
@ -2439,6 +2745,7 @@ void ChooseStarGiftRecipient(
window->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)),
LayerOption::KeepOther);
}), *lifetime);
}
void ShowStarGiftBox(

View file

@ -1575,7 +1575,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
sticker->paintRequest(
) | rpl::start_with_next([=] {
auto p = Painter(sticker);
if (const auto strong = weak.data()) {
if ([[maybe_unused]] const auto strong = weak.data()) {
const auto paused = On(PowerSaving::kStickersPanel)
|| show->paused(ChatHelpers::PauseReason::Layer);
paintSticker(p, index, QPoint(), paused, crl::now());
@ -1629,7 +1629,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
Data::StickersType::Stickers);
}, [](const auto &) {
});
if (const auto strong = weak.data()) {
if ([[maybe_unused]] const auto strong = weak.data()) {
applySet(result);
}
if (const auto strongBox = weakBox.data()) {

View file

@ -144,7 +144,7 @@ void UrlAuthBox::Request(
const auto callback = [=](Result result) {
if (result == Result::None) {
finishWithUrl(url);
} else if (const auto msg = session->data().message(itemId)) {
} else if (session->data().message(itemId)) {
const auto allowWrite = (result == Result::AuthAndAllowWrite);
using Flag = MTPmessages_AcceptUrlAuth::Flag;
const auto flags = (allowWrite ? Flag::f_write_allowed : Flag(0))

View file

@ -24,8 +24,8 @@ CallSignalBars {
inactiveOpacity: double;
}
callWidthMin: 300px;
callHeightMin: 440px;
callWidthMin: 380px;
callHeightMin: 520px;
callWidth: 720px;
callHeight: 540px;
@ -37,6 +37,7 @@ CallBodyLayout {
photoSize: pixels;
nameTop: pixels;
statusTop: pixels;
participantsTop: pixels;
muteStroke: pixels;
muteSize: pixels;
mutePosition: point;
@ -48,6 +49,7 @@ callBodyLayout: CallBodyLayout {
photoSize: 160px;
nameTop: 221px;
statusTop: 254px;
participantsTop: 294px;
muteStroke: 3px;
muteSize: 36px;
mutePosition: point(142px, 135px);
@ -58,6 +60,7 @@ callBodyWithPreview: CallBodyLayout {
photoSize: 100px;
nameTop: 132px;
statusTop: 163px;
participantsTop: 193px;
muteStroke: 3px;
muteSize: 0px;
mutePosition: point(90px, 84px);
@ -132,7 +135,6 @@ callHangup: CallButton(callAnswer) {
}
bg: callHangupBg;
outerBg: callHangupBg;
label: callButtonLabel;
}
callCancel: CallButton(callAnswer) {
button: IconButton(callButton) {
@ -143,7 +145,6 @@ callCancel: CallButton(callAnswer) {
}
bg: callIconBgActive;
outerBg: callIconBgActive;
label: callButtonLabel;
}
callMicrophoneMute: CallButton(callAnswer) {
button: IconButton(callButton) {
@ -154,7 +155,6 @@ callMicrophoneMute: CallButton(callAnswer) {
}
bg: callIconBg;
outerBg: callMuteRipple;
label: callButtonLabel;
cornerButtonPosition: point(40px, 4px);
cornerButtonBorder: 2px;
}
@ -183,6 +183,17 @@ callCameraUnmute: CallButton(callMicrophoneUnmute) {
}
}
}
callAddPeople: CallButton(callAnswer) {
button: IconButton(callButton) {
icon: icon {{ "calls/calls_add_people", callIconFg }};
iconPosition: point(-1px, 22px);
ripple: RippleAnimation(defaultRippleAnimation) {
color: callMuteRipple;
}
}
bg: callIconBg;
outerBg: callMuteRipple;
}
callCornerButtonInner: IconButton {
width: 20px;
height: 20px;
@ -521,7 +532,7 @@ callErrorToast: Toast(defaultToast) {
}
groupCallWidth: 380px;
groupCallHeight: 580px;
groupCallHeight: 520px;
groupCallWidthRtmp: 720px;
groupCallWidthRtmpMin: 240px;
groupCallHeightRtmp: 580px;
@ -567,6 +578,13 @@ groupCallPopupMenu: PopupMenu(defaultPopupMenu) {
menu: groupCallMenu;
animation: groupCallPanelAnimation;
}
groupCallPopupMenuWithIcons: PopupMenu(popupMenuWithIcons) {
shadow: groupCallMenuShadow;
menu: Menu(groupCallMenu, menuWithIcons) {
arrow: icon {{ "menu/submenu_arrow", groupCallMemberNotJoinedStatus }};
}
animation: groupCallPanelAnimation;
}
groupCallPopupMenuWithVolume: PopupMenu(groupCallPopupMenu) {
scrollPadding: margins(0px, 3px, 0px, 8px);
menu: Menu(groupCallMenu) {
@ -617,6 +635,23 @@ callDeviceSelectionMenu: PopupMenu(groupCallPopupMenu) {
}
}
createCallListButton: OutlineButton(defaultPeerListButton) {
font: normalFont;
padding: margins(11px, 5px, 11px, 5px);
}
createCallListItem: PeerListItem(defaultPeerListItem) {
button: createCallListButton;
height: 52px;
photoPosition: point(12px, 6px);
namePosition: point(63px, 7px);
statusPosition: point(63px, 26px);
photoSize: 40px;
}
createCallList: PeerList(defaultPeerList) {
item: createCallListItem;
padding: margins(0px, 6px, 0px, 6px);
}
groupCallRecordingTimerPadding: margins(0px, 4px, 0px, 4px);
groupCallRecordingTimerFont: font(12px);
@ -640,26 +675,18 @@ groupCallMembersListCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {
selectFg: groupCallActiveFg;
check: groupCallMembersListCheck;
}
groupCallMembersListItem: PeerListItem(defaultPeerListItem) {
button: OutlineButton(defaultPeerListButton) {
groupCallMembersListItem: PeerListItem(createCallListItem) {
button: OutlineButton(createCallListButton) {
textBg: groupCallMembersBg;
textBgOver: groupCallMembersBgOver;
textFg: groupCallMemberInactiveStatus;
textFgOver: groupCallMemberInactiveStatus;
font: normalFont;
padding: margins(11px, 5px, 11px, 5px);
ripple: groupCallRipple;
}
disabledCheckFg: groupCallMemberNotJoinedStatus;
checkbox: groupCallMembersListCheckbox;
height: 52px;
photoPosition: point(12px, 6px);
namePosition: point(63px, 7px);
statusPosition: point(63px, 26px);
photoSize: 40px;
nameFg: groupCallMembersFg;
nameFgChecked: groupCallMembersFg;
statusFg: groupCallMemberInactiveStatus;
@ -804,6 +831,7 @@ groupCallAddMember: SettingsButton(defaultSettingsButton) {
ripple: groupCallRipple;
}
groupCallAddMemberIcon: icon {{ "info/info_add_member", groupCallMemberInactiveIcon, point(0px, 3px) }};
groupCallShareLinkIcon: icon {{ "menu/links_profile", groupCallMemberInactiveIcon, point(4px, 3px) }};
groupCallSubtitleLabel: FlatLabel(defaultFlatLabel) {
maxHeight: 18px;
textFg: groupCallMemberNotJoinedStatus;
@ -874,6 +902,8 @@ groupCallMemberColoredCrossLine: CrossLineAnimation(groupCallMemberInactiveCross
fg: groupCallMemberMutedIcon;
icon: icon {{ "calls/group_calls_unmuted", groupCallMemberActiveIcon }};
}
groupCallMemberCalling: icon {{ "calls/call_answer", groupCallMemberInactiveIcon }};
groupCallMemberCallingPosition: point(0px, 8px);
groupCallMemberInvited: icon {{ "calls/group_calls_invited", groupCallMemberInactiveIcon }};
groupCallMemberInvitedPosition: point(2px, 12px);
groupCallMemberRaisedHand: icon {{ "calls/group_calls_raised_hand", groupCallMemberInactiveStatus }};
@ -917,7 +947,6 @@ groupCallHangup: CallButton(callHangup) {
button: groupCallHangupInner;
bg: groupCallLeaveBg;
outerBg: groupCallLeaveBg;
label: callButtonLabel;
}
groupCallSettingsSmall: CallButton(groupCallSettings) {
button: IconButton(groupCallSettingsInner) {
@ -1309,6 +1338,7 @@ groupCallNarrowRaisedHand: icon {{ "calls/video_mini_speak", groupCallMemberInac
groupCallNarrowCameraIcon: icon {{ "calls/video_mini_video", groupCallMemberNotJoinedStatus }};
groupCallNarrowScreenIcon: icon {{ "calls/video_mini_screencast", groupCallMemberNotJoinedStatus }};
groupCallNarrowInvitedIcon: icon {{ "calls/video_mini_invited", groupCallMemberNotJoinedStatus }};
groupCallNarrowCallingIcon: icon {{ "calls/video_mini_invited", groupCallMemberNotJoinedStatus }};
groupCallNarrowIconPosition: point(-4px, 2px);
groupCallNarrowIconSkip: 15px;
groupCallOutline: 2px;
@ -1483,4 +1513,174 @@ groupCallCalendarColors: CalendarColors {
titleTextColor: groupCallMembersFg;
}
//
createCallInviteLink: SettingsButton(defaultSettingsButton) {
textFg: windowActiveTextFg;
textFgOver: windowActiveTextFg;
textBg: windowBg;
textBgOver: windowBgOver;
style: TextStyle(defaultTextStyle) {
font: font(14px semibold);
}
height: 20px;
padding: margins(74px, 8px, 8px, 9px);
}
createCallInviteLinkIcon: icon {{ "info/edit/group_manage_links", windowActiveTextFg }};
createCallInviteLinkIconPosition: point(23px, 2px);
createCallVideo: IconButton {
width: 36px;
height: 52px;
icon: icon {{ "info/info_media_video", menuIconFg }};
iconOver: icon {{ "info/info_media_video", menuIconFgOver }};
iconPosition: point(-1px, -1px);
ripple: defaultRippleAnimation;
rippleAreaPosition: point(0px, 8px);
rippleAreaSize: 36px;
}
createCallVideoActive: icon {{ "info/info_media_video", windowActiveTextFg }};
createCallVideoMargins: margins(0px, 0px, 10px, 0px);
createCallAudio: IconButton(createCallVideo) {
icon: icon {{ "menu/phone", menuIconFg }};
iconOver: icon {{ "menu/phone", menuIconFgOver }};
}
createCallAudioActive: icon {{ "menu/phone", windowActiveTextFg }};
createCallAudioMargins: margins(0px, 0px, 4px, 0px);
confcallLinkButton: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
style: semiboldTextStyle;
}
confcallLinkBoxInitial: Box(defaultBox) {
buttonPadding: margins(12px, 11px, 24px, 96px);
buttonHeight: 42px;
button: confcallLinkButton;
shadowIgnoreTopSkip: true;
}
confcallLinkBox: Box(confcallLinkBoxInitial) {
buttonPadding: margins(12px, 11px, 24px, 24px);
}
confcallLinkCopyButton: RoundButton(confcallLinkButton) {
icon: icon {{ "info/edit/links_copy", activeButtonFg }};
iconOver: icon {{ "info/edit/links_copy", activeButtonFgOver }};
iconPosition: point(-1px, 5px);
}
confcallLinkShareButton: RoundButton(confcallLinkCopyButton) {
icon: icon {{ "info/edit/links_share", activeButtonFg }};
iconOver: icon {{ "info/edit/links_share", activeButtonFgOver }};
}
confcallLinkHeaderIconPadding: margins(0px, 32px, 0px, 10px);
confcallLinkTitlePadding: margins(0px, 0px, 0px, 12px);
confcallLinkCenteredText: FlatLabel(defaultFlatLabel) {
align: align(top);
minWidth: 40px;
}
confcallLinkFooterOr: FlatLabel(confcallLinkCenteredText) {
textFg: windowSubTextFg;
}
confcallLinkFooterOrTop: 12px;
confcallLinkFooterOrSkip: 8px;
confcallLinkFooterOrLineTop: 9px;
confcallJoinBox: Box(confcallLinkBox) {
buttonPadding: margins(24px, 24px, 24px, 24px);
}
confcallJoinLogo: icon {{ "calls/group_call_logo", windowFgActive }};
confcallJoinLogoPadding: margins(8px, 8px, 8px, 8px);
confcallJoinSepPadding: margins(0px, 8px, 0px, 8px);
confcallJoinUserpics: UserpicsRow {
button: UserpicButton(defaultUserpicButton) {
size: size(36px, 36px);
photoSize: 36px;
}
bg: boxBg;
shift: 16px;
stroke: 2px;
invert: true;
}
confcallJoinUserpicsPadding: margins(0px, 0px, 0px, 16px);
confcallInviteUserpicsBg: groupCallMembersBg;
confcallInviteUserpics: UserpicsRow {
button: UserpicButton(defaultUserpicButton) {
size: size(24px, 24px);
photoSize: 24px;
}
bg: confcallInviteUserpicsBg;
shift: 10px;
stroke: 2px;
invert: true;
}
confcallInviteParticipants: FlatLabel(defaultFlatLabel) {
textFg: callNameFg;
}
confcallInviteParticipantsPadding: margins(8px, 3px, 12px, 2px);
confcallInviteVideo: IconButton(createCallVideo) {
icon: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }};
iconOver: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }};
ripple: groupCallRipple;
}
confcallInviteVideoActive: icon {{ "info/info_media_video", groupCallActiveFg }};
confcallInviteAudio: IconButton(confcallInviteVideo) {
icon: icon {{ "menu/phone", groupCallMemberInactiveIcon }};
iconOver: icon {{ "menu/phone", groupCallMemberInactiveIcon }};
}
confcallInviteAudioActive: icon {{ "menu/phone", groupCallActiveFg }};
groupCallLinkBox: Box(confcallLinkBox) {
bg: groupCallMembersBg;
title: FlatLabel(boxTitle) {
textFg: groupCallMembersFg;
}
titleAdditionalFg: groupCallMemberNotJoinedStatus;
}
groupCallLinkCenteredText: FlatLabel(confcallLinkCenteredText) {
textFg: groupCallMembersFg;
}
groupCallLinkPreview: InputField(defaultInputField) {
textBg: groupCallMembersBgOver;
textFg: groupCallMembersFg;
textMargins: margins(12px, 8px, 30px, 5px);
style: defaultTextStyle;
heightMin: 35px;
}
groupCallInviteLink: SettingsButton(createCallInviteLink) {
textFg: mediaviewTextLinkFg;
textFgOver: mediaviewTextLinkFg;
textBg: groupCallMembersBg;
textBgOver: groupCallMembersBgOver;
padding: margins(63px, 8px, 8px, 9px);
ripple: groupCallRipple;
}
groupCallInviteLinkIcon: icon {{ "info/edit/group_manage_links", mediaviewTextLinkFg }};
confcallLinkMenu: IconButton(boxTitleClose) {
icon: icon {{ "title_menu_dots", boxTitleCloseFg }};
iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }};
}
groupCallLinkMenu: IconButton(confcallLinkMenu) {
icon: icon {{ "title_menu_dots", groupCallMemberInactiveIcon }};
iconOver: icon {{ "title_menu_dots", groupCallMemberInactiveIcon }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: groupCallMembersBgOver;
}
}
confcallFingerprintBottomSkip: 8px;
confcallFingerprintMargins: margins(8px, 5px, 8px, 5px);
confcallFingerprintTextMargins: margins(3px, 3px, 3px, 0px);
confcallFingerprintText: FlatLabel(defaultFlatLabel) {
textFg: groupCallMembersFg;
style: TextStyle(defaultTextStyle) {
font: font(10px semibold);
}
}
confcallFingerprintSkip: 2px;
confcallFingerprintTooltipLabel: defaultImportantTooltipLabel;
confcallFingerprintTooltip: defaultImportantTooltip;
confcallFingerprintTooltipSkip: 12px;
confcallFingerprintTooltipMaxWidth: 220px;

View file

@ -9,17 +9,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "core/application.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_invite_controller.h"
#include "calls/calls_instance.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "mainwidget.h"
#include "window/window_session_controller.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_changes.h"
@ -32,6 +41,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "api/api_updates.h"
#include "apiwrap.h"
#include "info/profile/info_profile_icon.h"
#include "settings/settings_calls.h"
#include "styles/style_info.h" // infoTopBarMenu
#include "styles/style_layers.h" // st::boxLabel.
#include "styles/style_calls.h"
#include "styles/style_boxes.h"
@ -92,7 +104,7 @@ GroupCallRow::GroupCallRow(not_null<PeerData*> peer)
: PeerListRow(peer)
, _st(st::callGroupCall) {
if (const auto channel = peer->asChannel()) {
const auto status = (channel->isMegagroup()
const auto status = (!channel->isMegagroup()
? (channel->isPublic()
? tr::lng_create_public_channel_title
: tr::lng_create_private_channel_title)
@ -150,7 +162,7 @@ void GroupCallRow::rightActionStopLastRipple() {
namespace GroupCalls {
ListController::ListController(not_null<Window::SessionController*> window)
ListController::ListController(not_null<::Window::SessionController*> window)
: _window(window) {
setStyleOverrides(&st::peerListSingleRow);
}
@ -227,7 +239,7 @@ void ListController::rowClicked(not_null<PeerListRow*> row) {
crl::on_main(window, [=, peer = row->peer()] {
window->showPeerHistory(
peer,
Window::SectionShow::Way::ClearStack);
::Window::SectionShow::Way::ClearStack);
});
}
@ -430,9 +442,9 @@ BoxController::Row::Type BoxController::Row::ComputeType(
return Type::Out;
} else if (auto media = item->media()) {
if (const auto call = media->call()) {
const auto reason = call->finishReason;
if (reason == Data::Call::FinishReason::Busy
|| reason == Data::Call::FinishReason::Missed) {
using State = Data::CallState;
const auto state = call->state;
if (state == State::Busy || state == State::Missed) {
return Type::Missed;
}
}
@ -470,7 +482,7 @@ void BoxController::Row::rightActionStopLastRipple() {
}
}
BoxController::BoxController(not_null<Window::SessionController*> window)
BoxController::BoxController(not_null<::Window::SessionController*> window)
: _window(window)
, _api(&_window->session().mtp()) {
}
@ -591,7 +603,7 @@ void BoxController::rowClicked(not_null<PeerListRow*> row) {
crl::on_main(window, [=, peer = row->peer()] {
window->showPeerHistory(
peer,
Window::SectionShow::Way::ClearStack,
::Window::SectionShow::Way::ClearStack,
itemId);
});
}
@ -611,7 +623,7 @@ void BoxController::receivedCalls(const QVector<MTPMessage> &result) {
for (const auto &message : result) {
const auto msgId = IdFromMessage(message);
const auto peerId = PeerFromMessage(message);
if (const auto peer = session().data().peerLoaded(peerId)) {
if (session().data().peerLoaded(peerId)) {
const auto item = session().data().addNewMessage(
message,
MessageFlags(),
@ -698,7 +710,7 @@ std::unique_ptr<PeerListRow> BoxController::createRow(
void ClearCallsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window) {
not_null<::Window::SessionController*> window) {
const auto weak = Ui::MakeWeak(box);
box->addRow(
object_ptr<Ui::FlatLabel>(
@ -756,4 +768,133 @@ void ClearCallsBox(
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateCallButton(
not_null<Ui::VerticalLayout*> container,
not_null<::Window::SessionController*> controller,
Fn<void()> done) {
const auto result = container->add(object_ptr<Ui::SettingsButton>(
container,
tr::lng_confcall_create_call(),
st::inviteViaLinkButton), QMargins());
Ui::AddSkip(container);
Ui::AddDividerText(
container,
tr::lng_confcall_create_call_description(
lt_count,
rpl::single(controller->session().appConfig().confcallSizeLimit()
* 1.),
Ui::Text::WithEntities));
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
result,
st::inviteViaLinkIcon,
QPoint());
result->heightValue(
) | rpl::start_with_next([=](int height) {
icon->moveToLeft(
st::inviteViaLinkIconPosition.x(),
(height - st::inviteViaLinkIcon.height()) / 2);
}, icon->lifetime());
result->setClickedCallback([=] {
controller->show(Group::PrepareCreateCallBox(controller, done));
});
return result;
}
void ShowCallsBox(not_null<::Window::SessionController*> window) {
struct State {
State(not_null<::Window::SessionController*> window)
: callsController(window)
, groupCallsController(window) {
}
Calls::BoxController callsController;
PeerListContentDelegateSimple callsDelegate;
Calls::GroupCalls::ListController groupCallsController;
PeerListContentDelegateSimple groupCallsDelegate;
base::unique_qptr<Ui::PopupMenu> menu;
};
window->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto state = box->lifetime().make_state<State>(window);
const auto groupCalls = box->addRow(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)),
{});
groupCalls->hide(anim::type::instant);
groupCalls->toggleOn(state->groupCallsController.shownValue());
Ui::AddSubsectionTitle(
groupCalls->entity(),
tr::lng_call_box_groupcalls_subtitle());
state->groupCallsDelegate.setContent(groupCalls->entity()->add(
object_ptr<PeerListContent>(box, &state->groupCallsController),
{}));
state->groupCallsController.setDelegate(&state->groupCallsDelegate);
Ui::AddSkip(groupCalls->entity());
Ui::AddDivider(groupCalls->entity());
Ui::AddSkip(groupCalls->entity());
const auto button = AddCreateCallButton(
box->verticalLayout(),
window,
crl::guard(box, [=] { box->closeBox(); }));
button->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Enter);
}) | rpl::start_with_next([=] {
state->callsDelegate.peerListMouseLeftGeometry();
}, button->lifetime());
const auto content = box->addRow(
object_ptr<PeerListContent>(box, &state->callsController),
{});
state->callsDelegate.setContent(content);
state->callsController.setDelegate(&state->callsDelegate);
box->setWidth(state->callsController.contentWidth());
state->callsController.boxHeightValue(
) | rpl::start_with_next([=](int height) {
box->setMinHeight(height);
}, box->lifetime());
box->setTitle(tr::lng_call_box_title());
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
const auto menuButton = box->addTopButton(st::infoTopBarMenu);
menuButton->setClickedCallback([=] {
state->menu = base::make_unique_q<Ui::PopupMenu>(
menuButton,
st::popupMenuWithIcons);
const auto showSettings = [=] {
window->showSettings(
Settings::Calls::Id(),
::Window::SectionShow(anim::type::instant));
};
const auto clearAll = crl::guard(box, [=] {
box->uiShow()->showBox(Box(Calls::ClearCallsBox, window));
});
state->menu->addAction(
tr::lng_settings_section_call_settings(tr::now),
showSettings,
&st::menuIconSettings);
if (state->callsDelegate.peerListFullRowsCount() > 0) {
Ui::Menu::CreateAddActionCallback(state->menu)({
.text = tr::lng_call_box_clear_all(tr::now),
.handler = clearAll,
.icon = &st::menuIconDeleteAttention,
.isAttention = true,
});
}
state->menu->popup(QCursor::pos());
return true;
});
}));
}
} // namespace Calls

View file

@ -20,7 +20,7 @@ namespace GroupCalls {
class ListController : public PeerListController {
public:
explicit ListController(not_null<Window::SessionController*> window);
explicit ListController(not_null<::Window::SessionController*> window);
[[nodiscard]] rpl::producer<bool> shownValue() const;
@ -30,7 +30,7 @@ public:
void rowRightActionClicked(not_null<PeerListRow*> row) override;
private:
const not_null<Window::SessionController*> _window;
const not_null<::Window::SessionController*> _window;
base::flat_map<PeerId, not_null<PeerListRow*>> _groupCalls;
rpl::variable<int> _fullCount;
@ -40,7 +40,7 @@ private:
class BoxController : public PeerListController {
public:
explicit BoxController(not_null<Window::SessionController*> window);
explicit BoxController(not_null<::Window::SessionController*> window);
Main::Session &session() const override;
void prepare() override;
@ -68,7 +68,7 @@ private:
std::unique_ptr<PeerListRow> createRow(
not_null<HistoryItem*> item) const;
const not_null<Window::SessionController*> _window;
const not_null<::Window::SessionController*> _window;
MTP::Sender _api;
MsgId _offsetId = 0;
@ -79,6 +79,8 @@ private:
void ClearCallsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window);
not_null<::Window::SessionController*> window);
void ShowCallsBox(not_null<::Window::SessionController*> window);
} // namespace Calls

View file

@ -12,10 +12,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/platform/base_platform_info.h"
#include "base/random.h"
#include "boxes/abstract_box.h"
#include "calls/group/calls_group_common.h"
#include "calls/calls_instance.h"
#include "calls/calls_panel.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_group_call.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
@ -39,8 +41,6 @@ namespace tgcalls {
class InstanceImpl;
class InstanceV2Impl;
class InstanceV2ReferenceImpl;
class InstanceImplLegacy;
void SetLegacyGlobalServerConfig(const std::string &serverConfig);
} // namespace tgcalls
namespace Calls {
@ -55,7 +55,6 @@ const auto kDefaultVersion = "2.4.4"_q;
const auto Register = tgcalls::Register<tgcalls::InstanceImpl>();
const auto RegisterV2 = tgcalls::Register<tgcalls::InstanceV2Impl>();
const auto RegV2Ref = tgcalls::Register<tgcalls::InstanceV2ReferenceImpl>();
const auto RegisterLegacy = tgcalls::Register<tgcalls::InstanceImplLegacy>();
[[nodiscard]] base::flat_set<int64> CollectEndpointIds(
const QVector<MTPPhoneConnection> &list) {
@ -246,7 +245,52 @@ Call::Call(
setupOutgoingVideo();
}
Call::Call(
not_null<Delegate*> delegate,
not_null<UserData*> user,
CallId conferenceId,
MsgId conferenceInviteMsgId,
std::vector<not_null<PeerData*>> conferenceParticipants,
bool video)
: _delegate(delegate)
, _user(user)
, _api(&_user->session().mtp())
, _type(Type::Incoming)
, _state(State::WaitingIncoming)
, _discardByTimeoutTimer([=] { hangup(); })
, _playbackDeviceId(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Playback,
Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callPlaybackDeviceIdValue(),
Core::App().settings().playbackDeviceIdValue()))
, _captureDeviceId(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Capture,
Webrtc::DeviceIdValueWithFallback(
Core::App().settings().callCaptureDeviceIdValue(),
Core::App().settings().captureDeviceIdValue()))
, _cameraDeviceId(
&Core::App().mediaDevices(),
Webrtc::DeviceType::Camera,
Core::App().settings().cameraDeviceIdValue())
, _id(base::RandomValue<CallId>())
, _conferenceId(conferenceId)
, _conferenceInviteMsgId(conferenceInviteMsgId)
, _conferenceParticipants(std::move(conferenceParticipants))
, _videoIncoming(
std::make_unique<Webrtc::VideoTrack>(
StartVideoState(video)))
, _videoOutgoing(
std::make_unique<Webrtc::VideoTrack>(
StartVideoState(video))) {
startWaitingTrack();
setupOutgoingVideo();
}
void Call::generateModExpFirst(bytes::const_span randomSeed) {
Expects(!conferenceInvite());
auto first = MTP::CreateModExp(_dhConfig.g, _dhConfig.p, randomSeed);
if (first.modexp.empty()) {
LOG(("Call Error: Could not compute mod-exp first."));
@ -272,6 +316,8 @@ bool Call::isIncomingWaiting() const {
}
void Call::start(bytes::const_span random) {
Expects(!conferenceInvite());
// Save config here, because it is possible that it changes between
// different usages inside the same call.
_dhConfig = _delegate->getDhConfig();
@ -296,6 +342,7 @@ void Call::startOutgoing() {
Expects(_type == Type::Outgoing);
Expects(_state.current() == State::Requesting);
Expects(_gaHash.size() == kSha256Size);
Expects(!conferenceInvite());
const auto flags = _videoCapture
? MTPphone_RequestCall::Flag::f_video
@ -303,7 +350,6 @@ void Call::startOutgoing() {
_api.request(MTPphone_RequestCall(
MTP_flags(flags),
_user->inputUser,
MTPInputGroupCall(),
MTP_int(base::RandomValue<int32>()),
MTP_bytes(_gaHash),
MTP_phoneCallProtocol(
@ -350,6 +396,7 @@ void Call::startOutgoing() {
void Call::startIncoming() {
Expects(_type == Type::Incoming);
Expects(_state.current() == State::Starting);
Expects(!conferenceInvite());
_api.request(MTPphone_ReceivedCall(
MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash))
@ -363,6 +410,8 @@ void Call::startIncoming() {
}
void Call::applyUserConfirmation() {
Expects(!conferenceInvite());
if (_state.current() == State::WaitingUserConfirmation) {
setState(State::Requesting);
}
@ -375,9 +424,51 @@ void Call::answer() {
}), video);
}
StartConferenceInfo Call::migrateConferenceInfo(StartConferenceInfo extend) {
extend.migrating = true;
extend.muted = muted();
extend.videoCapture = isSharingVideo() ? _videoCapture : nullptr;
extend.videoCaptureScreenId = screenSharingDeviceId();
return extend;
}
void Call::acceptConferenceInvite() {
Expects(conferenceInvite());
if (_state.current() != State::WaitingIncoming) {
return;
}
setState(State::ExchangingKeys);
const auto limit = 5;
const auto messageId = _conferenceInviteMsgId;
_api.request(MTPphone_GetGroupCall(
MTP_inputGroupCallInviteMessage(MTP_int(messageId.bare)),
MTP_int(limit)
)).done([=](const MTPphone_GroupCall &result) {
result.data().vcall().match([&](const auto &data) {
auto call = _user->owner().sharedConferenceCall(
data.vid().v,
data.vaccess_hash().v);
call->processFullCall(result);
Core::App().calls().startOrJoinConferenceCall(
migrateConferenceInfo({
.call = std::move(call),
.joinMessageId = messageId,
}));
});
}).fail([=](const MTP::Error &error) {
handleRequestError(error.type());
}).send();
}
void Call::actuallyAnswer() {
Expects(_type == Type::Incoming);
if (conferenceInvite()) {
acceptConferenceInvite();
return;
}
const auto state = _state.current();
if (state != State::Starting && state != State::WaitingIncoming) {
if (state != State::ExchangingKeys
@ -435,6 +526,8 @@ void Call::setMuted(bool mute) {
}
void Call::setupMediaDevices() {
Expects(!conferenceInvite());
_playbackDeviceId.changes() | rpl::filter([=] {
return _instance && _setDeviceIdCallback;
}) | rpl::start_with_next([=](const Webrtc::DeviceResolvedId &deviceId) {
@ -472,7 +565,8 @@ void Call::setupOutgoingVideo() {
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
} else if (_state.current() != State::Established
&& (state != Webrtc::VideoState::Inactive)
&& (started == Webrtc::VideoState::Inactive)) {
&& (started == Webrtc::VideoState::Inactive)
&& !conferenceInvite()) {
_errors.fire({ ErrorType::NotStartedCall });
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
} else if (state != Webrtc::VideoState::Inactive
@ -528,24 +622,30 @@ crl::time Call::getDurationMs() const {
return _startTime ? (crl::now() - _startTime) : 0;
}
void Call::hangup() {
void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) {
const auto state = _state.current();
if (state == State::Busy) {
if (state == State::Busy
|| state == State::MigrationHangingUp) {
_delegate->callFinished(this);
} else {
const auto missed = (state == State::Ringing
|| (state == State::Waiting && _type == Type::Outgoing));
const auto declined = isIncomingWaiting();
const auto reason = missed
const auto reason = !migrateSlug.isEmpty()
? MTP_phoneCallDiscardReasonMigrateConferenceCall(
MTP_string(migrateSlug))
: missed
? MTP_phoneCallDiscardReasonMissed()
: declined
? MTP_phoneCallDiscardReasonBusy()
: MTP_phoneCallDiscardReasonHangup();
finish(FinishType::Ended, reason);
finish(FinishType::Ended, reason, migrateCall);
}
}
void Call::redial() {
Expects(!conferenceInvite());
if (_state.current() != State::Busy) {
return;
}
@ -575,6 +675,8 @@ void Call::startWaitingTrack() {
}
void Call::sendSignalingData(const QByteArray &data) {
Expects(!conferenceInvite());
_api.request(MTPphone_SendSignalingData(
MTP_inputPhoneCall(
MTP_long(_id),
@ -706,7 +808,7 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
}
if (false && data.is_need_rating() && _id && _accessHash) {
const auto window = Core::App().windowFor(
Window::SeparateId(_user));
::Window::SeparateId(_user));
const auto session = &_user->session();
const auto callId = _id;
const auto callAccessHash = _accessHash;
@ -741,7 +843,10 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
&& reason->type() == mtpc_phoneCallDiscardReasonDisconnect) {
LOG(("Call Info: Discarded with DISCONNECT reason."));
}
if (reason && reason->type() == mtpc_phoneCallDiscardReasonBusy) {
if (reason && reason->type() == mtpc_phoneCallDiscardReasonMigrateConferenceCall) {
const auto slug = qs(reason->c_phoneCallDiscardReasonMigrateConferenceCall().vslug());
finishByMigration(slug);
} else if (reason && reason->type() == mtpc_phoneCallDiscardReasonBusy) {
setState(State::Busy);
} else if (_type == Type::Outgoing
|| _state.current() == State::HangingUp) {
@ -769,6 +874,35 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
Unexpected("phoneCall type inside an existing call handleUpdate()");
}
void Call::finishByMigration(const QString &slug) {
Expects(!conferenceInvite());
if (_state.current() == State::MigrationHangingUp) {
return;
}
setState(State::MigrationHangingUp);
const auto limit = 5;
const auto session = &_user->session();
session->api().request(MTPphone_GetGroupCall(
MTP_inputGroupCallSlug(MTP_string(slug)),
MTP_int(limit)
)).done([=](const MTPphone_GroupCall &result) {
result.data().vcall().match([&](const auto &data) {
const auto call = session->data().sharedConferenceCall(
data.vid().v,
data.vaccess_hash().v);
call->processFullCall(result);
Core::App().calls().startOrJoinConferenceCall(
migrateConferenceInfo({
.call = call,
.linkSlug = slug,
}));
});
}).fail(crl::guard(this, [=] {
setState(State::Failed);
})).send();
}
void Call::updateRemoteMediaState(
tgcalls::AudioState audio,
tgcalls::VideoState video) {
@ -809,6 +943,7 @@ bool Call::handleSignalingData(
void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
Expects(_type == Type::Outgoing);
Expects(!conferenceInvite());
if (_state.current() == State::ExchangingKeys
|| _instance) {
@ -861,6 +996,7 @@ void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
void Call::startConfirmedCall(const MTPDphoneCall &call) {
Expects(_type == Type::Incoming);
Expects(!conferenceInvite());
const auto firstBytes = bytes::make_span(call.vg_a_or_b().v);
if (_gaHash != openssl::Sha256(firstBytes)) {
@ -887,11 +1023,15 @@ void Call::startConfirmedCall(const MTPDphoneCall &call) {
}
void Call::createAndStartController(const MTPDphoneCall &call) {
Expects(!conferenceInvite());
_discardByTimeoutTimer.cancel();
if (!checkCallFields(call) || _authKey.size() != kAuthKeySize) {
return;
}
_conferenceSupported = call.is_conference_supported();
const auto &protocol = call.vprotocol().c_phoneCallProtocol();
const auto &serverConfig = _user->session().serverConfig();
@ -1060,6 +1200,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
const auto track = (state != State::FailedHangingUp)
&& (state != State::Failed)
&& (state != State::HangingUp)
&& (state != State::MigrationHangingUp)
&& (state != State::Ended)
&& (state != State::EndedByOtherDevice)
&& (state != State::Busy);
@ -1083,6 +1224,8 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
}
void Call::handleControllerStateChange(tgcalls::State state) {
Expects(!conferenceInvite());
switch (state) {
case tgcalls::State::WaitInit: {
DEBUG_LOG(("Call Info: State changed to WaitingInit."));
@ -1176,6 +1319,11 @@ void Call::setState(State state) {
&& state != State::Failed) {
return;
}
if (was == State::MigrationHangingUp
&& state != State::Ended
&& state != State::Failed) {
return;
}
if (was != state) {
_state = state;
@ -1311,6 +1459,11 @@ void Call::toggleScreenSharing(std::optional<QString> uniqueId) {
_videoOutgoing->setState(Webrtc::VideoState::Active);
}
auto Call::peekVideoCapture() const
-> std::shared_ptr<tgcalls::VideoCaptureInterface> {
return _videoCapture;
}
auto Call::playbackDeviceIdValue() const
-> rpl::producer<Webrtc::DeviceResolvedId> {
return _playbackDeviceId.value();
@ -1324,7 +1477,10 @@ rpl::producer<Webrtc::DeviceResolvedId> Call::cameraDeviceIdValue() const {
return _cameraDeviceId.value();
}
void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
void Call::finish(
FinishType type,
const MTPPhoneCallDiscardReason &reason,
Data::GroupCall *migrateCall) {
Expects(type != FinishType::None);
setSignalBarCount(kSignalBarFinished);
@ -1349,8 +1505,15 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
|| state == State::Ended
|| state == State::Failed) {
return;
} else if (conferenceInvite()) {
if (migrateCall) {
_delegate->callFinished(this);
} else {
Core::App().calls().declineIncomingConferenceInvites(_conferenceId);
setState(finalState);
}
if (!_id) {
return;
} else if (!_id) {
setState(finalState);
return;
}
@ -1372,6 +1535,13 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
// We want to discard request still being sent and processed even if
// the call is already destroyed.
if (migrateCall) {
_user->owner().registerInvitedToCallUser(
migrateCall->id(),
migrateCall,
_user,
true);
}
const auto session = &_user->session();
const auto weak = base::make_weak(this);
session->api().request(MTPphone_DiscardCall( // We send 'discard' here.
@ -1413,10 +1583,10 @@ void Call::handleRequestError(const QString &error) {
? Lang::Hard::CallErrorIncompatible().replace(
"{user}",
_user->name())
: QString();
: error;
if (!inform.isEmpty()) {
if (const auto window = Core::App().windowFor(
Window::SeparateId(_user))) {
::Window::SeparateId(_user))) {
window->show(Ui::MakeInformBox(inform));
} else {
Ui::show(Ui::MakeInformBox(inform));
@ -1435,7 +1605,7 @@ void Call::handleControllerError(const QString &error) {
: QString();
if (!inform.isEmpty()) {
if (const auto window = Core::App().windowFor(
Window::SeparateId(_user))) {
::Window::SeparateId(_user))) {
window->show(Ui::MakeInformBox(inform));
} else {
Ui::show(Ui::MakeInformBox(inform));
@ -1464,7 +1634,6 @@ Call::~Call() {
}
void UpdateConfig(const std::string &data) {
tgcalls::SetLegacyGlobalServerConfig(data);
}
} // namespace Calls

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_auth_key.h"
#include "webrtc/webrtc_device_resolver.h"
namespace Data {
class GroupCall;
} // namespace Data
namespace Media {
namespace Audio {
class Track;
@ -36,6 +40,8 @@ struct DeviceResolvedId;
namespace Calls {
struct StartConferenceInfo;
struct DhConfig {
int32 version = 0;
int32 g = 0;
@ -98,6 +104,13 @@ public:
not_null<UserData*> user,
Type type,
bool video);
Call(
not_null<Delegate*> delegate,
not_null<UserData*> user,
CallId conferenceId,
MsgId conferenceInviteMsgId,
std::vector<not_null<PeerData*>> conferenceParticipants,
bool video);
[[nodiscard]] Type type() const {
return _type;
@ -108,6 +121,19 @@ public:
[[nodiscard]] CallId id() const {
return _id;
}
[[nodiscard]] bool conferenceInvite() const {
return _conferenceId != 0;
}
[[nodiscard]] CallId conferenceId() const {
return _conferenceId;
}
[[nodiscard]] MsgId conferenceInviteMsgId() const {
return _conferenceInviteMsgId;
}
[[nodiscard]] auto conferenceParticipants() const
-> const std::vector<not_null<PeerData*>> & {
return _conferenceParticipants;
}
[[nodiscard]] bool isIncomingWaiting() const;
void start(bytes::const_span random);
@ -122,6 +148,7 @@ public:
FailedHangingUp,
Failed,
HangingUp,
MigrationHangingUp,
Ended,
EndedByOtherDevice,
ExchangingKeys,
@ -143,6 +170,10 @@ public:
return _errors.events();
}
[[nodiscard]] rpl::producer<bool> confereceSupportedValue() const {
return _conferenceSupported.value();
}
enum class RemoteAudioState {
Muted,
Active,
@ -198,7 +229,9 @@ public:
void applyUserConfirmation();
void answer();
void hangup();
void hangup(
Data::GroupCall *migrateCall = nullptr,
const QString &migrateSlug = QString());
void redial();
bool isKeyShaForFingerprintReady() const;
@ -220,6 +253,8 @@ public:
[[nodiscard]] QString screenSharingDeviceId() const;
void toggleCameraSharing(bool enabled);
void toggleScreenSharing(std::optional<QString> uniqueId);
[[nodiscard]] auto peekVideoCapture() const
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
[[nodiscard]] auto playbackDeviceIdValue() const
-> rpl::producer<Webrtc::DeviceResolvedId>;
@ -246,7 +281,9 @@ private:
void finish(
FinishType type,
const MTPPhoneCallDiscardReason &reason
= MTP_phoneCallDiscardReasonDisconnect());
= MTP_phoneCallDiscardReasonDisconnect(),
Data::GroupCall *migrateCall = nullptr);
void finishByMigration(const QString &slug);
void startOutgoing();
void startIncoming();
void startWaitingTrack();
@ -263,6 +300,7 @@ private:
bool checkCallFields(const MTPDphoneCallAccepted &call);
void actuallyAnswer();
void acceptConferenceInvite();
void confirmAcceptedCall(const MTPDphoneCallAccepted &call);
void startConfirmedCall(const MTPDphoneCall &call);
void setState(State state);
@ -280,11 +318,15 @@ private:
tgcalls::AudioState audio,
tgcalls::VideoState video);
[[nodiscard]] StartConferenceInfo migrateConferenceInfo(
StartConferenceInfo extend);
const not_null<Delegate*> _delegate;
const not_null<UserData*> _user;
MTP::Sender _api;
Type _type = Type::Outgoing;
rpl::variable<State> _state = State::Starting;
rpl::variable<bool> _conferenceSupported = false;
rpl::variable<RemoteAudioState> _remoteAudioState
= RemoteAudioState::Active;
rpl::variable<Webrtc::VideoState> _remoteVideoState;
@ -316,6 +358,10 @@ private:
uint64 _accessHash = 0;
uint64 _keyFingerprint = 0;
CallId _conferenceId = 0;
MsgId _conferenceInviteMsgId = 0;
std::vector<not_null<PeerData*>> _conferenceParticipants;
std::unique_ptr<tgcalls::Instance> _instance;
std::shared_ptr<tgcalls::VideoCaptureInterface> _videoCapture;
QString _videoCaptureDeviceId;

View file

@ -7,20 +7,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/calls_emoji_fingerprint.h"
#include "base/random.h"
#include "calls/calls_call.h"
#include "calls/calls_signal_bars.h"
#include "lang/lang_keys.h"
#include "data/data_user.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/tooltip.h"
#include "ui/abstract_button.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "ui/ui_utility.h"
#include "styles/style_calls.h"
namespace Calls {
namespace {
constexpr auto kTooltipShowTimeoutMs = 350;
constexpr auto kTooltipShowTimeoutMs = crl::time(500);
constexpr auto kCarouselOneDuration = crl::time(100);
constexpr auto kStartTimeShift = crl::time(50);
constexpr auto kEmojiInFingerprint = 4;
constexpr auto kEmojiInCarousel = 10;
const ushort Data[] = {
0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21,
@ -109,8 +118,11 @@ const ushort Offsets[] = {
620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641,
642, 643, 644, 646, 648, 650, 652, 654, 656, 658 };
constexpr auto kEmojiCount = (base::array_size(Offsets) - 1);
uint64 ComputeEmojiIndex(bytes::const_span bytes) {
Expects(bytes.size() == 8);
return ((gsl::to_integer<uint64>(bytes[0]) & 0x7F) << 56)
| (gsl::to_integer<uint64>(bytes[1]) << 48)
| (gsl::to_integer<uint64>(bytes[2]) << 40)
@ -121,40 +133,41 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) {
| (gsl::to_integer<uint64>(bytes[7]));
}
[[nodiscard]] EmojiPtr EmojiByIndex(int index) {
Expects(index >= 0 && index < kEmojiCount);
const auto offset = Offsets[index];
const auto size = Offsets[index + 1] - offset;
const auto string = QString::fromRawData(
reinterpret_cast<const QChar*>(Data + offset),
size);
return Ui::Emoji::Find(string);
}
} // namespace
std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
if (!call->isKeyShaForFingerprintReady()) {
return {};
}
return ComputeEmojiFingerprint(call->getKeyShaForFingerprint());
}
std::vector<EmojiPtr> ComputeEmojiFingerprint(
bytes::const_span fingerprint) {
auto result = std::vector<EmojiPtr>();
constexpr auto EmojiCount = (base::array_size(Offsets) - 1);
for (auto index = 0; index != EmojiCount; ++index) {
auto offset = Offsets[index];
auto size = Offsets[index + 1] - offset;
auto string = QString::fromRawData(
reinterpret_cast<const QChar*>(Data + offset),
size);
auto emoji = Ui::Emoji::Find(string);
Assert(emoji != nullptr);
}
if (call->isKeyShaForFingerprintReady()) {
auto sha256 = call->getKeyShaForFingerprint();
constexpr auto kPartSize = 8;
for (auto partOffset = 0; partOffset != sha256.size(); partOffset += kPartSize) {
auto value = ComputeEmojiIndex(gsl::make_span(sha256).subspan(partOffset, kPartSize));
auto index = value % EmojiCount;
auto offset = Offsets[index];
auto size = Offsets[index + 1] - offset;
auto string = QString::fromRawData(
reinterpret_cast<const QChar*>(Data + offset),
size);
auto emoji = Ui::Emoji::Find(string);
Assert(emoji != nullptr);
result.push_back(emoji);
}
for (auto partOffset = 0
; partOffset != fingerprint.size()
; partOffset += kPartSize) {
const auto value = ComputeEmojiIndex(
fingerprint.subspan(partOffset, kPartSize));
result.push_back(EmojiByIndex(value % kEmojiCount));
}
return result;
}
object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
not_null<QWidget*> parent,
not_null<Call*> call) {
class EmojiTooltipShower final : public Ui::AbstractTooltipShower {
@ -180,8 +193,8 @@ object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
};
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
auto result = base::make_unique_q<Ui::RpWidget>(parent);
const auto raw = result.get();
// Emoji tooltip.
const auto shower = raw->lifetime().make_state<EmojiTooltipShower>(
@ -295,4 +308,516 @@ object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
return result;
}
FingerprintBadge SetupFingerprintBadge(
rpl::lifetime &on,
rpl::producer<QByteArray> fingerprint) {
struct State {
FingerprintBadgeState data;
Ui::Animations::Basic animation;
Fn<void(crl::time)> update;
rpl::event_stream<> repaints;
};
const auto state = on.make_state<State>();
state->data.speed = 1. / kCarouselOneDuration;
state->update = [=](crl::time now) {
// speed-up-duration = 2 * one / speed.
const auto one = 1.;
const auto speedUpDuration = 2 * kCarouselOneDuration;
const auto speed0 = one / kCarouselOneDuration;
auto updated = false;
auto animating = false;
for (auto &entry : state->data.entries) {
if (!entry.time) {
continue;
}
animating = true;
if (entry.time >= now) {
continue;
}
updated = true;
const auto elapsed = (now - entry.time) * 1.;
entry.time = now;
Assert(!entry.emoji || entry.sliding.size() > 1);
const auto slideCount = entry.emoji
? (int(entry.sliding.size()) - 1) * one
: (kEmojiInCarousel + (elapsed / kCarouselOneDuration));
const auto finalPosition = slideCount * one;
const auto distance = finalPosition - entry.position;
const auto accelerate0 = speed0 - entry.speed;
const auto decelerate0 = speed0;
const auto acceleration0 = speed0 / speedUpDuration;
const auto taccelerate0 = accelerate0 / acceleration0;
const auto tdecelerate0 = decelerate0 / acceleration0;
const auto paccelerate0 = entry.speed * taccelerate0
+ acceleration0 * taccelerate0 * taccelerate0 / 2.;
const auto pdecelerate0 = 0
+ acceleration0 * tdecelerate0 * tdecelerate0 / 2.;
const auto ttozero = entry.speed / acceleration0;
if (paccelerate0 + pdecelerate0 <= distance) {
// We have time to accelerate to speed0,
// maybe go some time on speed0 and then decelerate to 0.
const auto uaccelerate0 = std::min(taccelerate0, elapsed);
const auto left = distance - paccelerate0 - pdecelerate0;
const auto tconstant = left / speed0;
const auto uconstant = std::min(
tconstant,
elapsed - uaccelerate0);
const auto udecelerate0 = std::min(
tdecelerate0,
elapsed - uaccelerate0 - uconstant);
if (udecelerate0 >= tdecelerate0) {
Assert(entry.emoji != nullptr);
entry = { .emoji = entry.emoji };
} else {
entry.position += entry.speed * uaccelerate0
+ acceleration0 * uaccelerate0 * uaccelerate0 / 2.
+ speed0 * uconstant
+ speed0 * udecelerate0
- acceleration0 * udecelerate0 * udecelerate0 / 2.;
entry.speed += acceleration0
* (uaccelerate0 - udecelerate0);
}
} else if (acceleration0 * ttozero * ttozero / 2 <= distance) {
// We have time to accelerate at least for some time >= 0,
// and then decelerate to 0 to make it to final position.
//
// peak = entry.speed + acceleration0 * t
// tdecelerate = peak / acceleration0
// distance = entry.speed * t
// + acceleration0 * t * t / 2
// + acceleration0 * tdecelerate * tdecelerate / 2
const auto det = entry.speed * entry.speed / 2
+ distance * acceleration0;
const auto t = std::max(
(sqrt(det) - entry.speed) / acceleration0,
0.);
const auto taccelerate = t;
const auto uaccelerate = std::min(taccelerate, elapsed);
const auto tdecelerate = t + (entry.speed / acceleration0);
const auto udecelerate = std::min(
tdecelerate,
elapsed - uaccelerate);
if (udecelerate >= tdecelerate) {
Assert(entry.emoji != nullptr);
entry = { .emoji = entry.emoji };
} else {
const auto topspeed = entry.speed
+ acceleration0 * taccelerate;
entry.position += entry.speed * uaccelerate
+ acceleration0 * uaccelerate * uaccelerate / 2.
+ topspeed * udecelerate
- acceleration0 * udecelerate * udecelerate / 2.;
entry.speed += acceleration0
* (uaccelerate - udecelerate);
}
} else {
// We just need to decelerate to 0,
// faster than acceleration0.
Assert(entry.speed > 0);
const auto tdecelerate = 2 * distance / entry.speed;
const auto udecelerate = std::min(tdecelerate, elapsed);
if (udecelerate >= tdecelerate) {
Assert(entry.emoji != nullptr);
entry = { .emoji = entry.emoji };
} else {
const auto a = entry.speed / tdecelerate;
entry.position += entry.speed * udecelerate
- a * udecelerate * udecelerate / 2;
entry.speed -= a * udecelerate;
}
}
if (entry.position >= kEmojiInCarousel) {
entry.position -= qFloor(entry.position / kEmojiInCarousel)
* kEmojiInCarousel;
}
while (entry.position >= 1.) {
Assert(!entry.sliding.empty());
entry.position -= 1.;
entry.sliding.erase(begin(entry.sliding));
if (entry.emoji && entry.sliding.size() < 2) {
entry = { .emoji = entry.emoji };
break;
} else if (entry.sliding.empty()) {
const auto index = (entry.added++) % kEmojiInCarousel;
entry.sliding.push_back(entry.carousel[index]);
}
}
if (!entry.emoji
&& entry.position > 0.
&& entry.sliding.size() < 2) {
const auto index = (entry.added++) % kEmojiInCarousel;
entry.sliding.push_back(entry.carousel[index]);
}
}
if (!animating) {
state->animation.stop();
} else if (updated) {
state->repaints.fire({});
}
};
state->animation.init(state->update);
state->data.entries.resize(kEmojiInFingerprint);
const auto fillCarousel = [=](
int index,
base::BufferedRandom<uint32> &buffered) {
auto &entry = state->data.entries[index];
auto indices = std::vector<int>();
indices.reserve(kEmojiInCarousel);
auto count = kEmojiCount;
for (auto i = 0; i != kEmojiInCarousel; ++i, --count) {
auto index = base::RandomIndex(count, buffered);
for (const auto &already : indices) {
if (index >= already) {
++index;
}
}
indices.push_back(index);
}
entry.carousel.clear();
entry.carousel.reserve(kEmojiInCarousel);
for (const auto index : indices) {
entry.carousel.push_back(EmojiByIndex(index));
}
};
const auto startTo = [=](
int index,
EmojiPtr emoji,
crl::time now,
base::BufferedRandom<uint32> &buffered) {
auto &entry = state->data.entries[index];
if ((entry.emoji == emoji) && (emoji || entry.time)) {
return;
} else if (!entry.time) {
Assert(entry.sliding.empty());
if (entry.emoji) {
entry.sliding.push_back(entry.emoji);
} else if (emoji) {
// Just initialize if we get emoji right from the start.
entry.emoji = emoji;
return;
}
entry.time = now + index * kStartTimeShift;
fillCarousel(index, buffered);
}
entry.emoji = emoji;
if (entry.emoji) {
entry.sliding.push_back(entry.emoji);
} else {
const auto index = (entry.added++) % kEmojiInCarousel;
entry.sliding.push_back(entry.carousel[index]);
}
};
std::move(
fingerprint
) | rpl::start_with_next([=](const QByteArray &fingerprint) {
auto buffered = base::BufferedRandom<uint32>(
kEmojiInCarousel * kEmojiInFingerprint);
const auto now = crl::now();
const auto emoji = (fingerprint.size() >= 32)
? ComputeEmojiFingerprint(
bytes::make_span(fingerprint).subspan(0, 32))
: std::vector<EmojiPtr>();
state->update(now);
if (emoji.size() == kEmojiInFingerprint) {
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
startTo(i, emoji[i], now, buffered);
}
} else {
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
startTo(i, nullptr, now, buffered);
}
}
if (!state->animation.animating()) {
state->animation.start();
}
}, on);
return { .state = &state->data, .repaints = state->repaints.events() };
}
void SetupFingerprintTooltip(not_null<Ui::RpWidget*> widget) {
struct State {
std::unique_ptr<Ui::ImportantTooltip> tooltip;
Fn<void()> updateGeometry;
Fn<void(bool)> toggleTooltip;
};
const auto state = widget->lifetime().make_state<State>();
state->updateGeometry = [=] {
if (!state->tooltip.get()) {
return;
}
const auto geometry = Ui::MapFrom(
widget->window(),
widget,
widget->rect());
if (geometry.isEmpty()) {
state->toggleTooltip(false);
return;
}
const auto weak = QPointer<QWidget>(state->tooltip.get());
const auto countPosition = [=](QSize size) {
const auto result = geometry.bottomLeft()
+ QPoint(
geometry.width() / 2,
st::confcallFingerprintTooltipSkip)
- QPoint(size.width() / 2, 0);
return result;
};
state->tooltip.get()->pointAt(
geometry,
RectPart::Bottom,
countPosition);
};
state->toggleTooltip = [=](bool show) {
if (const auto was = state->tooltip.release()) {
was->toggleAnimated(false);
}
if (!show) {
return;
}
const auto text = tr::lng_confcall_e2e_about(
tr::now,
Ui::Text::WithEntities);
if (text.empty()) {
return;
}
state->tooltip = std::make_unique<Ui::ImportantTooltip>(
widget->window(),
Ui::MakeNiceTooltipLabel(
widget,
rpl::single(text),
st::confcallFingerprintTooltipMaxWidth,
st::confcallFingerprintTooltipLabel),
st::confcallFingerprintTooltip);
const auto raw = state->tooltip.get();
const auto weak = QPointer<QWidget>(raw);
const auto destroy = [=] {
delete weak.data();
};
raw->setAttribute(Qt::WA_TransparentForMouseEvents);
raw->setHiddenCallback(destroy);
state->updateGeometry();
raw->toggleAnimated(true);
};
widget->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::Enter) {
state->toggleTooltip(true);
} else if (type == QEvent::Leave) {
state->toggleTooltip(false);
}
}, widget->lifetime());
}
QImage MakeVerticalShadow(int height) {
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(1, height) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
auto g = QLinearGradient(0, 0, 0, height);
auto color = st::groupCallMembersBg->c;
auto trans = color;
trans.setAlpha(0);
g.setStops({
{ 0.0, color },
{ 0.4, trans },
{ 0.6, trans },
{ 1.0, color },
});
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(0, 0, 1, height, g);
p.end();
return result;
}
void SetupFingerprintBadgeWidget(
not_null<Ui::RpWidget*> widget,
not_null<const FingerprintBadgeState*> state,
rpl::producer<> repaints) {
auto &lifetime = widget->lifetime();
const auto button = Ui::CreateChild<Ui::RpWidget>(widget);
button->show();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button,
QString(),
st::confcallFingerprintText);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->show();
const auto ratio = style::DevicePixelRatio();
const auto esize = Ui::Emoji::GetSizeNormal();
const auto size = esize / ratio;
widget->widthValue() | rpl::start_with_next([=](int width) {
static_assert(!(kEmojiInFingerprint % 2));
const auto available = width
- st::confcallFingerprintMargins.left()
- st::confcallFingerprintMargins.right()
- (kEmojiInFingerprint * size)
- (kEmojiInFingerprint - 2) * st::confcallFingerprintSkip
- st::confcallFingerprintTextMargins.left()
- st::confcallFingerprintTextMargins.right();
if (available <= 0) {
return;
}
label->setText(tr::lng_confcall_e2e_badge(tr::now));
if (label->textMaxWidth() > available) {
label->setText(tr::lng_confcall_e2e_badge_small(tr::now));
}
const auto use = std::min(available, label->textMaxWidth());
label->resizeToWidth(use);
const auto ontheleft = kEmojiInFingerprint / 2;
const auto ontheside = ontheleft * size
+ (ontheleft - 1) * st::confcallFingerprintSkip;
const auto text = QRect(
(width - use) / 2,
(st::confcallFingerprintMargins.top()
+ st::confcallFingerprintTextMargins.top()),
use,
label->height());
const auto textOuter = text.marginsAdded(
st::confcallFingerprintTextMargins);
const auto withEmoji = QRect(
textOuter.x() - ontheside,
textOuter.y(),
textOuter.width() + ontheside * 2,
size);
const auto outer = withEmoji.marginsAdded(
st::confcallFingerprintMargins);
button->setGeometry(outer);
label->moveToLeft(text.x() - outer.x(), text.y() - outer.y(), width);
widget->resize(
width,
button->height() + st::confcallFingerprintBottomSkip);
}, lifetime);
const auto cache = lifetime.make_state<FingerprintBadgeCache>();
button->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(button);
const auto outer = button->rect();
const auto radius = outer.height() / 2.;
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::groupCallMembersBg);
p.drawRoundedRect(outer, radius, radius);
p.setClipRect(outer);
const auto withEmoji = outer.marginsRemoved(
st::confcallFingerprintMargins);
p.translate(withEmoji.topLeft());
const auto text = label->geometry();
const auto textOuter = text.marginsAdded(
st::confcallFingerprintTextMargins);
const auto count = int(state->entries.size());
cache->entries.resize(count);
cache->shadow = MakeVerticalShadow(outer.height());
for (auto i = 0; i != count; ++i) {
const auto &entry = state->entries[i];
auto &cached = cache->entries[i];
const auto shadowed = entry.speed / state->speed;
PaintFingerprintEntry(p, entry, cached, esize);
if (shadowed > 0.) {
p.setOpacity(shadowed);
p.drawImage(
QRect(0, -st::confcallFingerprintMargins.top(), size, outer.height()),
cache->shadow);
p.setOpacity(1.);
}
if (i + 1 == count / 2) {
p.translate(size + textOuter.width(), 0);
} else {
p.translate(size + st::confcallFingerprintSkip, 0);
}
}
}, lifetime);
std::move(repaints) | rpl::start_with_next([=] {
button->update();
}, lifetime);
SetupFingerprintTooltip(button);
}
void PaintFingerprintEntry(
QPainter &p,
const FingerprintBadgeState::Entry &entry,
FingerprintBadgeCache::Entry &cache,
int esize) {
const auto stationary = !entry.time;
if (stationary) {
Ui::Emoji::Draw(p, entry.emoji, esize, 0, 0);
return;
}
const auto ratio = style::DevicePixelRatio();
const auto size = esize / ratio;
const auto add = 4;
const auto height = size + 2 * add;
const auto validateCache = [&](int index, EmojiPtr e) {
if (cache.emoji.size() <= index) {
cache.emoji.reserve(entry.carousel.size() + 2);
cache.emoji.resize(index + 1);
}
auto &emoji = cache.emoji[index];
if (emoji.ptr != e) {
emoji.ptr = e;
emoji.image = QImage(
QSize(size, height) * ratio,
QImage::Format_ARGB32_Premultiplied);
emoji.image.setDevicePixelRatio(ratio);
emoji.image.fill(Qt::transparent);
auto q = QPainter(&emoji.image);
Ui::Emoji::Draw(q, e, esize, 0, add);
q.end();
//emoji.image = Images::Blur(
// std::move(emoji.image),
// false,
// Qt::Vertical);
}
return &emoji;
};
auto shift = entry.position * height - add;
p.translate(0, shift);
for (const auto &e : entry.sliding) {
const auto index = [&] {
const auto i = ranges::find(entry.carousel, e);
if (i != end(entry.carousel)) {
return int(i - begin(entry.carousel));
}
return int(entry.carousel.size())
+ ((e == entry.sliding.back()) ? 1 : 0);
}();
const auto entry = validateCache(index, e);
p.drawImage(0, 0, entry->image);
p.translate(0, -height);
shift -= height;
}
p.translate(0, -shift);
}
} // namespace Calls

View file

@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
namespace Ui {
class RpWidget;
@ -19,9 +19,54 @@ class Call;
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
not_null<Call*> call);
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
bytes::const_span fingerprint);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
[[nodiscard]] base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
not_null<QWidget*> parent,
not_null<Call*> call);
struct FingerprintBadgeState {
struct Entry {
EmojiPtr emoji = nullptr;
std::vector<EmojiPtr> sliding;
std::vector<EmojiPtr> carousel;
crl::time time = 0;
float64 speed = 0.;
float64 position = 0.;
int added = 0;
};
std::vector<Entry> entries;
float64 speed = 1.;
};
struct FingerprintBadge {
not_null<const FingerprintBadgeState*> state;
rpl::producer<> repaints;
};
FingerprintBadge SetupFingerprintBadge(
rpl::lifetime &on,
rpl::producer<QByteArray> fingerprint);
void SetupFingerprintBadgeWidget(
not_null<Ui::RpWidget*> widget,
not_null<const FingerprintBadgeState*> state,
rpl::producer<> repaints);
struct FingerprintBadgeCache {
struct Emoji {
EmojiPtr ptr = nullptr;
QImage image;
};
struct Entry {
std::vector<Emoji> emoji;
};
std::vector<Entry> entries;
QImage shadow;
};
void PaintFingerprintEntry(
QPainter &p,
const FingerprintBadgeState::Entry &entry,
FingerprintBadgeCache::Entry &cache,
int esize);
} // namespace Calls

View file

@ -12,9 +12,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_choose_join_as.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_rtmp.h"
#include "history/history.h"
#include "history/history_item.h"
#include "mtproto/mtproto_dh_utils.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "main/session/session_show.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "apiwrap.h"
@ -26,8 +30,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/calls_panel.h"
#include "data/data_user.h"
#include "data/data_group_call.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_histories.h"
#include "data/data_session.h"
#include "media/audio/media_audio_track.h"
#include "platform/platform_specific.h"
@ -103,6 +109,8 @@ void Instance::Delegate::callFailed(not_null<Call*> call) {
}
void Instance::Delegate::callRedial(not_null<Call*> call) {
Expects(!call->conferenceInvite());
if (_instance->_currentCall.get() == call) {
_instance->refreshDhConfig();
}
@ -230,6 +238,78 @@ void Instance::startOrJoinGroupCall(
});
}
void Instance::startOrJoinConferenceCall(StartConferenceInfo args) {
Expects(args.call || args.show);
const auto migrationInfo = (args.migrating
&& args.call
&& _currentCallPanel)
? _currentCallPanel->migrationInfo()
: ConferencePanelMigration();
if (!args.migrating) {
destroyCurrentCall();
}
const auto session = args.show
? &args.show->session()
: &args.call->session();
auto call = std::make_unique<GroupCall>(_delegate.get(), args);
const auto raw = call.get();
session->account().sessionChanges(
) | rpl::start_with_next([=] {
destroyGroupCall(raw);
}, raw->lifetime());
if (args.call) {
_currentGroupCallPanel = std::make_unique<Group::Panel>(
raw,
migrationInfo);
_currentGroupCall = std::move(call);
_currentGroupCallChanges.fire_copy(raw);
finishConferenceInvitations(args);
if (args.migrating) {
destroyCurrentCall(args.call.get(), args.linkSlug);
}
} else {
if (const auto was = base::take(_startingGroupCall)) {
destroyGroupCall(was.get());
}
_startingGroupCall = std::move(call);
}
}
void Instance::startedConferenceReady(
not_null<GroupCall*> call,
StartConferenceInfo args) {
if (_startingGroupCall.get() != call) {
return;
}
const auto migrationInfo = _currentCallPanel
? _currentCallPanel->migrationInfo()
: ConferencePanelMigration();
_currentGroupCallPanel = std::make_unique<Group::Panel>(
call,
migrationInfo);
_currentGroupCall = std::move(_startingGroupCall);
_currentGroupCallChanges.fire_copy(call);
const auto real = call->conferenceCall().get();
const auto link = real->conferenceInviteLink();
const auto slug = Group::ExtractConferenceSlug(link);
finishConferenceInvitations(args);
destroyCurrentCall(real, slug);
}
void Instance::finishConferenceInvitations(const StartConferenceInfo &args) {
Expects(_currentGroupCallPanel != nullptr);
if (!args.invite.empty()) {
_currentGroupCallPanel->migrationInviteUsers(std::move(args.invite));
} else if (args.sharingLink) {
_currentGroupCallPanel->migrationShowShareLink();
}
}
void Instance::confirmLeaveCurrent(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
@ -310,7 +390,11 @@ void Instance::playSoundOnce(const QString &key) {
void Instance::destroyCall(not_null<Call*> call) {
if (_currentCall.get() == call) {
_currentCallPanel->closeBeforeDestroy();
const auto groupCallWindow = _currentGroupCallPanel
? _currentGroupCallPanel->window().get()
: nullptr;
const auto reused = (_currentCallPanel->window() == groupCallWindow);
_currentCallPanel->closeBeforeDestroy(reused);
_currentCallPanel = nullptr;
auto taken = base::take(_currentCall);
@ -326,7 +410,7 @@ void Instance::destroyCall(not_null<Call*> call) {
void Instance::createCall(
not_null<UserData*> user,
Call::Type type,
CallType type,
bool isVideo) {
struct Performer final {
explicit Performer(Fn<void(bool, bool, const Performer &)> callback)
@ -385,6 +469,8 @@ void Instance::destroyGroupCall(not_null<GroupCall*> call) {
LOG(("Calls::Instance doesn't prevent quit any more."));
}
Core::App().quitPreventFinished();
} else if (_startingGroupCall.get() == call) {
base::take(_startingGroupCall);
}
}
@ -411,6 +497,7 @@ void Instance::createGroupCall(
void Instance::refreshDhConfig() {
Expects(_currentCall != nullptr);
Expects(!_currentCall->conferenceInvite());
const auto weak = base::make_weak(_currentCall);
_currentCall->user()->session().api().request(MTPmessages_GetDhConfig(
@ -505,6 +592,8 @@ void Instance::handleUpdate(
handleGroupCallUpdate(session, update);
}, [&](const MTPDupdateGroupCallParticipants &data) {
handleGroupCallUpdate(session, update);
}, [&](const MTPDupdateGroupCallChainBlocks &data) {
handleGroupCallUpdate(session, update);
}, [](const auto &) {
Unexpected("Update type in Calls::Instance::handleUpdate.");
});
@ -612,12 +701,14 @@ void Instance::handleCallUpdate(
void Instance::handleGroupCallUpdate(
not_null<Main::Session*> session,
const MTPUpdate &update) {
if (_currentGroupCall
&& (&_currentGroupCall->peer()->session() == session)) {
const auto groupCall = _currentGroupCall
? _currentGroupCall.get()
: _startingGroupCall.get();
if (groupCall && (&groupCall->peer()->session() == session)) {
update.match([&](const MTPDupdateGroupCall &data) {
_currentGroupCall->handlePossibleCreateOrJoinResponse(data);
groupCall->handlePossibleCreateOrJoinResponse(data);
}, [&](const MTPDupdateGroupCallConnection &data) {
_currentGroupCall->handlePossibleCreateOrJoinResponse(data);
groupCall->handlePossibleCreateOrJoinResponse(data);
}, [](const auto &) {
});
}
@ -632,11 +723,24 @@ void Instance::handleGroupCallUpdate(
}, [](const MTPDupdateGroupCallParticipants &data) {
return data.vcall().match([&](const MTPDinputGroupCall &data) {
return data.vid().v;
}, [](const auto &) -> CallId {
Unexpected("slug/msg in Instance::handleGroupCallUpdate");
});
}, [](const MTPDupdateGroupCallChainBlocks &data) {
return data.vcall().match([&](const MTPDinputGroupCall &data) {
return data.vid().v;
}, [](const auto &) -> CallId {
Unexpected("slug/msg in Instance::handleGroupCallUpdate");
});
}, [](const auto &) -> CallId {
Unexpected("Type in Instance::handleGroupCallUpdate.");
});
if (const auto existing = session->data().groupCall(callId)) {
if (update.type() == mtpc_updateGroupCallChainBlocks) {
const auto existing = session->data().groupCall(callId);
if (existing && groupCall && groupCall->lookupReal() == existing) {
groupCall->handleUpdate(update);
}
} else if (const auto existing = session->data().groupCall(callId)) {
existing->enqueueUpdate(update);
} else {
applyGroupCallUpdateChecked(session, update);
@ -646,9 +750,11 @@ void Instance::handleGroupCallUpdate(
void Instance::applyGroupCallUpdateChecked(
not_null<Main::Session*> session,
const MTPUpdate &update) {
if (_currentGroupCall
&& (&_currentGroupCall->peer()->session() == session)) {
_currentGroupCall->handleUpdate(update);
const auto groupCall = _currentGroupCall
? _currentGroupCall.get()
: _startingGroupCall.get();
if (groupCall && (&groupCall->peer()->session() == session)) {
groupCall->handleUpdate(update);
}
}
@ -683,20 +789,25 @@ bool Instance::inGroupCall() const {
&& (state != GroupCall::State::Failed);
}
void Instance::destroyCurrentCall() {
void Instance::destroyCurrentCall(
Data::GroupCall *migrateCall,
const QString &migrateSlug) {
if (const auto current = currentCall()) {
current->hangup();
current->hangup(migrateCall, migrateSlug);
if (const auto still = currentCall()) {
destroyCall(still);
}
}
if (const auto current = currentGroupCall()) {
if (!migrateCall || current->lookupReal() != migrateCall) {
current->hangup();
if (const auto still = currentGroupCall()) {
destroyGroupCall(still);
}
}
}
base::take(_startingGroupCall);
}
bool Instance::hasVisiblePanel(Main::Session *session) const {
if (inCall()) {
@ -849,4 +960,188 @@ std::shared_ptr<tgcalls::VideoCaptureInterface> Instance::getVideoCapture(
return result;
}
const ConferenceInvites &Instance::conferenceInvites(
CallId conferenceId) const {
static const auto kEmpty = ConferenceInvites();
const auto i = _conferenceInvites.find(conferenceId);
return (i != end(_conferenceInvites)) ? i->second : kEmpty;
}
void Instance::registerConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming) {
auto &info = _conferenceInvites[conferenceId].users[user];
(incoming ? info.incoming : info.outgoing).emplace(messageId);
}
void Instance::unregisterConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming,
bool onlyStopCalling) {
const auto i = _conferenceInvites.find(conferenceId);
if (i == end(_conferenceInvites)) {
return;
}
const auto j = i->second.users.find(user);
if (j == end(i->second.users)) {
return;
}
auto &info = j->second;
if (!(incoming ? info.incoming : info.outgoing).remove(messageId)) {
return;
}
if (!incoming) {
user->owner().unregisterInvitedToCallUser(
conferenceId,
user,
onlyStopCalling);
}
if (info.incoming.empty() && info.outgoing.empty()) {
i->second.users.erase(j);
if (i->second.users.empty()) {
_conferenceInvites.erase(i);
}
}
if (_currentCall
&& _currentCall->user() == user
&& _currentCall->conferenceInviteMsgId() == messageId
&& _currentCall->state() == Call::State::WaitingIncoming) {
destroyCurrentCall();
}
}
void Instance::declineIncomingConferenceInvites(CallId conferenceId) {
const auto i = _conferenceInvites.find(conferenceId);
if (i == end(_conferenceInvites)) {
return;
}
for (auto j = begin(i->second.users); j != end(i->second.users);) {
const auto api = &j->first->session().api();
for (const auto &messageId : base::take(j->second.incoming)) {
api->request(MTPphone_DeclineConferenceCallInvite(
MTP_int(messageId.bare)
)).send();
}
if (j->second.outgoing.empty()) {
j = i->second.users.erase(j);
} else {
++j;
}
}
if (i->second.users.empty()) {
_conferenceInvites.erase(i);
}
}
void Instance::declineOutgoingConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
bool discard) {
const auto i = _conferenceInvites.find(conferenceId);
if (i == end(_conferenceInvites)) {
return;
}
const auto j = i->second.users.find(user);
if (j == end(i->second.users)) {
return;
}
const auto api = &user->session().api();
auto ids = base::take(j->second.outgoing);
auto inputs = QVector<MTPint>();
for (const auto &messageId : ids) {
if (discard) {
inputs.push_back(MTP_int(messageId.bare));
} else {
api->request(MTPphone_DeclineConferenceCallInvite(
MTP_int(messageId.bare)
)).send();
}
}
if (!inputs.empty()) {
user->owner().histories().deleteMessages(
user->owner().history(user),
std::move(inputs),
true);
for (const auto &messageId : ids) {
if (const auto item = user->owner().message(user, messageId)) {
item->destroy();
}
}
}
if (j->second.incoming.empty()) {
i->second.users.erase(j);
if (i->second.users.empty()) {
_conferenceInvites.erase(i);
}
}
user->owner().unregisterInvitedToCallUser(conferenceId, user, !discard);
}
void Instance::showConferenceInvite(
not_null<UserData*> user,
MsgId conferenceInviteMsgId) {
const auto item = user->owner().message(user, conferenceInviteMsgId);
const auto media = item ? item->media() : nullptr;
const auto call = media ? media->call() : nullptr;
const auto conferenceId = call ? call->conferenceId : 0;
const auto video = call->video;
if (!conferenceId
|| call->state != Data::CallState::Invitation
|| user->isSelf()
|| user->session().appConfig().callsDisabledForSession()) {
return;
} else if (_currentCall
&& _currentCall->conferenceId() == conferenceId) {
return;
} else if (inGroupCall()
&& _currentGroupCall->conference()
&& _currentGroupCall->conferenceCall()->id() == conferenceId) {
return;
}
auto conferenceParticipants = call->otherParticipants;
if (!ranges::contains(conferenceParticipants, user)) {
conferenceParticipants.push_back(user);
}
const auto &config = user->session().serverConfig();
if (inCall() || inGroupCall()) {
declineIncomingConferenceInvites(conferenceId);
} else if (item->date() + (config.callRingTimeoutMs / 1000)
< base::unixtime::now()) {
declineIncomingConferenceInvites(conferenceId);
LOG(("Ignoring too old conference call invitation."));
} else {
const auto delegate = _delegate.get();
auto call = std::make_unique<Call>(
delegate,
user,
conferenceId,
conferenceInviteMsgId,
std::move(conferenceParticipants),
video);
const auto raw = call.get();
user->session().account().sessionChanges(
) | rpl::start_with_next([=] {
destroyCall(raw);
}, raw->lifetime());
if (_currentCall) {
_currentCallPanel->replaceCall(raw);
std::swap(_currentCall, call);
call->hangup();
} else {
_currentCallPanel = std::make_unique<Panel>(raw);
_currentCall = std::move(call);
}
_currentCallChanges.fire_copy(raw);
}
}
} // namespace Calls

View file

@ -13,6 +13,10 @@ namespace crl {
class semaphore;
} // namespace crl
namespace Data {
class GroupCall;
} // namespace Data
namespace Platform {
enum class PermissionType;
} // namespace Platform
@ -31,6 +35,7 @@ class Show;
namespace Calls::Group {
struct JoinInfo;
struct ConferenceInfo;
class Panel;
class ChooseJoinAsProcess;
class StartRtmpProcess;
@ -47,6 +52,8 @@ enum class CallType;
class GroupCall;
class Panel;
struct DhConfig;
struct InviteRequest;
struct StartConferenceInfo;
struct StartGroupCallArgs {
enum class JoinConfirm {
@ -59,6 +66,15 @@ struct StartGroupCallArgs {
bool scheduleNeeded = false;
};
struct ConferenceInviteMessages {
base::flat_set<MsgId> incoming;
base::flat_set<MsgId> outgoing;
};
struct ConferenceInvites {
base::flat_map<not_null<UserData*>, ConferenceInviteMessages> users;
};
class Instance final : public base::has_weak_ptr {
public:
Instance();
@ -69,6 +85,10 @@ public:
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
StartGroupCallArgs args);
void startOrJoinConferenceCall(StartConferenceInfo args);
void startedConferenceReady(
not_null<GroupCall*> call,
StartConferenceInfo args);
void showStartWithRtmp(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer);
@ -103,6 +123,28 @@ public:
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
void requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);
[[nodiscard]] const ConferenceInvites &conferenceInvites(
CallId conferenceId) const;
void registerConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming);
void unregisterConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
MsgId messageId,
bool incoming,
bool onlyStopCalling = false);
void showConferenceInvite(
not_null<UserData*> user,
MsgId conferenceInviteMsgId);
void declineIncomingConferenceInvites(CallId conferenceId);
void declineOutgoingConferenceInvite(
CallId conferenceId,
not_null<UserData*> user,
bool discard = false);
[[nodiscard]] FnMut<void()> addAsyncWaiter();
[[nodiscard]] bool isSharingScreen() const;
@ -117,6 +159,7 @@ private:
void createCall(not_null<UserData*> user, CallType type, bool isVideo);
void destroyCall(not_null<Call*> call);
void finishConferenceInvitations(const StartConferenceInfo &args);
void createGroupCall(
Group::JoinInfo info,
@ -136,7 +179,9 @@ private:
void refreshServerConfig(not_null<Main::Session*> session);
bytes::const_span updateDhConfig(const MTPmessages_DhConfig &data);
void destroyCurrentCall();
void destroyCurrentCall(
Data::GroupCall *migrateCall = nullptr,
const QString &migrateSlug = QString());
void handleCallUpdate(
not_null<Main::Session*> session,
const MTPPhoneCall &call);
@ -159,6 +204,7 @@ private:
std::unique_ptr<Panel> _currentCallPanel;
std::unique_ptr<GroupCall> _currentGroupCall;
std::unique_ptr<GroupCall> _startingGroupCall;
rpl::event_stream<GroupCall*> _currentGroupCallChanges;
std::unique_ptr<Group::Panel> _currentGroupCallPanel;
@ -167,6 +213,8 @@ private:
const std::unique_ptr<Group::ChooseJoinAsProcess> _chooseJoinAs;
const std::unique_ptr<Group::StartRtmpProcess> _startWithRtmp;
base::flat_map<CallId, ConferenceInvites> _conferenceInvites;
base::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;
};

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/calls_panel.h"
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -15,12 +16,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_cloud_file.h"
#include "data/data_changes.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_invite_controller.h"
#include "calls/ui/calls_device_menu.h"
#include "calls/calls_emoji_fingerprint.h"
#include "calls/calls_instance.h"
#include "calls/calls_signal_bars.h"
#include "calls/calls_userpic.h"
#include "calls/calls_video_bubble.h"
#include "calls/calls_video_incoming.h"
#include "calls/calls_window.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/call_button.h"
#include "ui/widgets/buttons.h"
@ -45,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/integration.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "main/session/session_show.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "platform/platform_specific.h"
@ -97,46 +102,56 @@ constexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000);
Panel::Panel(not_null<Call*> call)
: _call(call)
, _user(call->user())
, _layerBg(std::make_unique<Ui::LayerManager>(widget()))
#ifndef Q_OS_MAC
, _controls(Ui::Platform::SetupSeparateTitleControls(
window(),
st::callTitle,
[=](bool maximized) { toggleFullScreen(maximized); }))
#endif // !Q_OS_MAC
, _window(std::make_shared<Window>())
, _bodySt(&st::callBodyLayout)
, _answerHangupRedial(widget(), st::callAnswer, &st::callHangup)
, _decline(widget(), object_ptr<Ui::CallButton>(widget(), st::callHangup))
, _cancel(widget(), object_ptr<Ui::CallButton>(widget(), st::callCancel))
, _answerHangupRedial(
std::in_place,
widget(),
st::callAnswer,
&st::callHangup)
, _decline(
std::in_place,
widget(),
object_ptr<Ui::CallButton>(widget(), st::callHangup))
, _cancel(
std::in_place,
widget(),
object_ptr<Ui::CallButton>(widget(), st::callCancel))
, _screencast(
std::in_place,
widget(),
object_ptr<Ui::CallButton>(
widget(),
st::callScreencastOn,
&st::callScreencastOff))
, _camera(widget(), st::callCameraMute, &st::callCameraUnmute)
, _camera(std::in_place, widget(), st::callCameraMute, &st::callCameraUnmute)
, _mute(
std::in_place,
widget(),
object_ptr<Ui::CallButton>(
widget(),
st::callMicrophoneMute,
&st::callMicrophoneUnmute))
, _name(widget(), st::callName)
, _status(widget(), st::callStatus)
, _addPeople(
std::in_place,
widget(),
object_ptr<Ui::CallButton>(widget(), st::callAddPeople))
, _name(std::in_place, widget(), st::callName)
, _status(std::in_place, widget(), st::callStatus)
, _hideControlsTimer([=] { requestControlsHidden(true); })
, _controlsShownForceTimer([=] { controlsShownForce(false); }) {
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
_layerBg->setHideByBackgroundClick(true);
_decline->setDuration(st::callPanelDuration);
_decline->entity()->setText(tr::lng_call_decline());
_cancel->setDuration(st::callPanelDuration);
_cancel->entity()->setText(tr::lng_call_cancel());
_screencast->setDuration(st::callPanelDuration);
_addPeople->setDuration(st::callPanelDuration);
_addPeople->entity()->setText(tr::lng_call_add_people());
initWindow();
initWidget();
initControls();
initConferenceInvite();
initLayout();
initMediaDeviceToggles();
showAndActivate();
@ -153,6 +168,18 @@ bool Panel::isActive() const {
return window()->isActiveWindow() && isVisible();
}
ConferencePanelMigration Panel::migrationInfo() const {
return ConferencePanelMigration{ .window = _window };
}
std::shared_ptr<Main::SessionShow> Panel::sessionShow() {
return Main::MakeSessionShow(uiShow(), &_user->session());
}
std::shared_ptr<Ui::Show> Panel::uiShow() {
return _window->uiShow();
}
void Panel::showAndActivate() {
if (window()->isHidden()) {
window()->show();
@ -215,20 +242,16 @@ void Panel::initWindow() {
}
}
return base::EventFilterResult::Continue;
});
}, lifetime());
const auto guard = base::make_weak(this);
window()->setBodyTitleArea([=](QPoint widgetPoint) {
using Flag = Ui::WindowTitleHitTestFlag;
if (!widget()->rect().contains(widgetPoint)) {
if (!guard
|| !widget()->rect().contains(widgetPoint)
|| _window->controlsHasHitTest(widgetPoint)) {
return Flag::None | Flag(0);
}
#ifndef Q_OS_MAC
using Result = Ui::Platform::HitTestResult;
const auto windowPoint = widget()->mapTo(window(), widgetPoint);
if (_controls->controls.hitTest(windowPoint) != Result::None) {
return Flag::None | Flag(0);
}
#endif // !Q_OS_MAC
const auto buttonWidth = st::callCancel.button.width;
const auto buttonsWidth = buttonWidth * 4;
const auto inControls = (_fingerprint
@ -243,12 +266,15 @@ void Panel::initWindow() {
if (inControls) {
return Flag::None | Flag(0);
}
const auto shown = _layerBg->topShownLayer();
const auto shown = _window->topShownLayer();
return (!shown || !shown->geometry().contains(widgetPoint))
? (Flag::Move | Flag::Menu | Flag::FullScreen)
: Flag::None;
});
_window->maximizeRequests() | rpl::start_with_next([=](bool maximized) {
toggleFullScreen(maximized);
}, lifetime());
// Don't do that, it looks awful :(
//#ifdef Q_OS_WIN
// // On Windows we replace snap-to-top maximizing with fullscreen.
@ -282,12 +308,12 @@ void Panel::initWidget() {
widget()->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
paint(clip);
}, widget()->lifetime());
}, lifetime());
widget()->sizeValue(
) | rpl::skip(1) | rpl::start_with_next([=] {
updateControlsGeometry();
}, widget()->lifetime());
}, lifetime());
}
void Panel::initControls() {
@ -303,7 +329,7 @@ void Panel::initControls() {
return;
} else if (!env->desktopCaptureAllowed()) {
if (auto box = Group::ScreenSharingPrivacyRequestBox()) {
_layerBg->showBox(std::move(box));
uiShow()->showBox(std::move(box));
}
} else if (const auto source = env->uniqueDesktopCaptureSource()) {
if (!chooseSourceActiveDeviceId().isEmpty()) {
@ -318,9 +344,42 @@ void Panel::initControls() {
_camera->setClickedCallback([=] {
if (!_call) {
return;
} else {
_call->toggleCameraSharing(!_call->isSharingCamera());
}
_call->toggleCameraSharing(!_call->isSharingCamera());
});
_addPeople->entity()->setClickedCallback([=] {
if (!_call || _call->state() != Call::State::Established) {
uiShow()->showToast(tr::lng_call_error_add_not_started(tr::now));
return;
}
const auto call = _call;
const auto creating = std::make_shared<bool>();
const auto create = [=](std::vector<InviteRequest> users) {
if (*creating) {
return;
}
*creating = true;
const auto sharingLink = users.empty();
Core::App().calls().startOrJoinConferenceCall({
.show = sessionShow(),
.invite = std::move(users),
.sharingLink = sharingLink,
.migrating = true,
.muted = call->muted(),
.videoCapture = (call->isSharingVideo()
? call->peekVideoCapture()
: nullptr),
.videoCaptureScreenId = call->screenSharingDeviceId(),
});
};
const auto invite = crl::guard(call, [=](
std::vector<InviteRequest> users) {
create(std::move(users));
});
const auto share = crl::guard(call, [=] {
create({});
});
uiShow()->showBox(Group::PrepareInviteBox(call, invite, share));
});
_updateDurationTimer.setCallback([this] {
@ -367,6 +426,65 @@ void Panel::initControls() {
_screencast->finishAnimating();
}
void Panel::initConferenceInvite() {
const auto &participants = _call->conferenceParticipants();
const auto count = int(participants.size());
if (count < 2) {
return;
}
_conferenceParticipants = base::make_unique_q<Ui::RpWidget>(widget());
_conferenceParticipants->show();
const auto raw = _conferenceParticipants.get();
auto peers = std::vector<not_null<PeerData*>>();
for (const auto &peer : participants) {
if (peer == _user && count > 3) {
continue;
}
peers.push_back(peer);
if (peers.size() == 3) {
break;
}
}
const auto userpics = CreateUserpicsWithMoreBadge(
raw,
rpl::single(peers),
st::confcallInviteUserpics,
peers.size()).release();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
tr::lng_group_call_members(tr::now, lt_count, count),
st::confcallInviteParticipants);
const auto padding = st::confcallInviteParticipantsPadding;
const auto add = padding.bottom();
const auto width = add
+ userpics->width()
+ padding.left()
+ label->width()
+ padding.right();
const auto height = add + userpics->height() + add;
_status->geometryValue() | rpl::start_with_next([=] {
const auto top = _bodyTop + _bodySt->participantsTop;
const auto left = (widget()->width() - width) / 2;
raw->setGeometry(left, top, width, height);
userpics->move(add, add);
label->move(add + userpics->width() + padding.left(), padding.top());
}, raw->lifetime());
raw->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto radius = raw->height() / 2.;
p.setPen(Qt::NoPen);
p.setBrush(st::confcallInviteUserpicsBg);
p.drawRoundedRect(raw->rect(), radius, radius);
}, raw->lifetime());
}
void Panel::setIncomingSize(QSize size) {
if (_incomingFrameSize == size) {
return;
@ -444,15 +562,24 @@ void Panel::reinitWithCall(Call *call) {
updateControlsShown();
});
if (!_call) {
_fingerprint.destroy();
_fingerprint = nullptr;
_incoming = nullptr;
_outgoingVideoBubble = nullptr;
_powerSaveBlocker = nullptr;
return;
}
_user = _call->user();
_call->confereceSupportedValue(
) | rpl::start_with_next([=](bool supported) {
_conferenceSupported = supported;
_addPeople->toggle(_conferenceSupported
&& (_call->state() != State::WaitingUserConfirmation),
window()->isHidden() ? anim::type::instant : anim::type::normal);
updateHangupGeometry();
}, _callLifetime);
auto remoteMuted = _call->remoteAudioStateValue(
) | rpl::map(rpl::mappers::_1 == Call::RemoteAudioState::Muted);
rpl::duplicate(
@ -461,7 +588,7 @@ void Panel::reinitWithCall(Call *call) {
if (muted) {
createRemoteAudioMute();
} else {
_remoteAudioMute.destroy();
_remoteAudioMute = nullptr;
showRemoteLowBattery();
}
}, _callLifetime);
@ -470,7 +597,7 @@ void Panel::reinitWithCall(Call *call) {
if (state == Call::RemoteBatteryState::Low) {
createRemoteLowBattery();
} else {
_remoteLowBattery.destroy();
_remoteLowBattery = nullptr;
}
}, _callLifetime);
_userpic = std::make_unique<Userpic>(
@ -483,7 +610,7 @@ void Panel::reinitWithCall(Call *call) {
_incoming = std::make_unique<Incoming>(
widget(),
_call->videoIncoming(),
_window.backend());
_window->backend());
_incoming->widget()->hide();
_incoming->rp()->shownValue() | rpl::start_with_next([=] {
@ -605,6 +732,7 @@ void Panel::reinitWithCall(Call *call) {
&& state != State::EndedByOtherDevice
&& state != State::Failed
&& state != State::FailedHangingUp
&& state != State::MigrationHangingUp
&& state != State::HangingUp) {
refreshOutgoingPreviewInBody(state);
}
@ -630,10 +758,7 @@ void Panel::reinitWithCall(Call *call) {
}
Unexpected("Error type in _call->errors().");
}();
Ui::Toast::Show(widget(), Ui::Toast::Config{
.text = { text },
.st = &st::callErrorToast,
});
uiShow()->showToast(text);
}, _callLifetime);
_name->setText(_user->name());
@ -647,17 +772,13 @@ void Panel::reinitWithCall(Call *call) {
_startVideo->raise();
}
_mute->raise();
_powerSaveBlocker = std::make_unique<base::PowerSaveBlocker>(
base::PowerSaveBlockType::PreventDisplaySleep,
u"Video call is active"_q,
window()->windowHandle());
_addPeople->raise();
_incoming->widget()->lower();
}
void Panel::createRemoteAudioMute() {
_remoteAudioMute.create(
_remoteAudioMute = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(
widget(),
object_ptr<Ui::FlatLabel>(
widget(),
@ -694,7 +815,7 @@ void Panel::createRemoteAudioMute() {
}
void Panel::createRemoteLowBattery() {
_remoteLowBattery.create(
_remoteLowBattery = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(
widget(),
object_ptr<Ui::FlatLabel>(
widget(),
@ -710,7 +831,7 @@ void Panel::createRemoteLowBattery() {
style::PaletteChanged(
) | rpl::start_with_next([=] {
_remoteLowBattery.destroy();
_remoteLowBattery = nullptr;
createRemoteLowBattery();
}, _remoteLowBattery->lifetime());
@ -778,11 +899,9 @@ void Panel::initLayout() {
}) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
_name->setText(_call->user()->name());
updateControlsGeometry();
}, widget()->lifetime());
}, lifetime());
#ifndef Q_OS_MAC
_controls->wrap.raise();
#endif // !Q_OS_MAC
_window->raiseControls();
}
void Panel::showControls() {
@ -792,6 +911,7 @@ void Panel::showControls() {
_decline->setVisible(_decline->toggled());
_cancel->setVisible(_cancel->toggled());
_screencast->setVisible(_screencast->toggled());
_addPeople->setVisible(_addPeople->toggled());
const auto shown = !_incomingFrameSize.isEmpty();
_incoming->widget()->setVisible(shown);
@ -804,13 +924,16 @@ void Panel::showControls() {
showRemoteLowBattery();
}
void Panel::closeBeforeDestroy() {
void Panel::closeBeforeDestroy(bool windowIsReused) {
if (!windowIsReused) {
window()->close();
}
reinitWithCall(nullptr);
_lifetime.destroy();
}
rpl::lifetime &Panel::lifetime() {
return window()->lifetime();
return _lifetime;
}
void Panel::initGeometry() {
@ -948,7 +1071,7 @@ void Panel::updateControlsGeometry() {
_controlsShown ? 1. : 0.);
if (_fingerprint) {
#ifndef Q_OS_MAC
const auto controlsGeometry = _controls->controls.geometry();
const auto controlsGeometry = _window->controlsGeometry();
const auto halfWidth = widget()->width() / 2;
const auto minLeft = (controlsGeometry.center().x() < halfWidth)
? (controlsGeometry.width() + st::callFingerprintTop)
@ -994,7 +1117,11 @@ void Panel::updateControlsGeometry() {
std::min(
bodyPreviewSizeMax.height(),
st::callOutgoingPreviewMax.height()));
const auto contentHeight = _bodySt->height
const auto bodyContentHeight = _bodySt->height
+ (_conferenceParticipants
? (_bodySt->participantsTop - _bodySt->statusTop)
: 0);
const auto contentHeight = bodyContentHeight
+ (_outgoingPreviewInBody ? bodyPreviewSize.height() : 0);
const auto remainingHeight = available - contentHeight;
const auto skipHeight = remainingHeight
@ -1006,7 +1133,7 @@ void Panel::updateControlsGeometry() {
widget()->height(),
_buttonsTopShown,
shown);
const auto previewTop = _bodyTop + _bodySt->height + skipHeight;
const auto previewTop = _bodyTop + bodyContentHeight + skipHeight;
_userpic->setGeometry(
(widget()->width() - _bodySt->photoSize) / 2,
@ -1067,8 +1194,11 @@ void Panel::updateOutgoingVideoBubbleGeometry() {
}
void Panel::updateHangupGeometry() {
const auto isBusy = (_call
&& _call->state() == State::Busy);
const auto isWaitingUser = (_call
&& _call->state() == State::WaitingUserConfirmation);
const auto incomingWaiting = _call && _call->isIncomingWaiting();
const auto hangupProgress = isWaitingUser
? 0.
: _hangupShownProgress.value(_hangupShown ? 1. : 0.);
@ -1077,11 +1207,9 @@ void Panel::updateHangupGeometry() {
// Screencast - Camera - Cancel/Decline - Answer/Hangup/Redial - Mute.
const auto buttonWidth = st::callCancel.button.width;
const auto cancelWidth = buttonWidth * (1. - hangupProgress);
const auto cancelLeft = (isWaitingUser)
? ((widget()->width() - buttonWidth) / 2)
: (_mute->animating())
? ((widget()->width() - cancelWidth) / 2)
: ((widget()->width() / 2) - cancelWidth);
const auto cancelLeft = (widget()->width() - buttonWidth) / 2
- ((isBusy || incomingWaiting) ? buttonWidth : 0)
+ ((isWaitingUser || _conferenceSupported) ? 0 : (buttonWidth / 2));
_cancel->moveToLeft(cancelLeft, _buttonsTop);
_decline->moveToLeft(cancelLeft, _buttonsTop);
@ -1089,6 +1217,7 @@ void Panel::updateHangupGeometry() {
_screencast->moveToLeft(_camera->x() - buttonWidth, _buttonsTop);
_answerHangupRedial->moveToLeft(cancelLeft + cancelWidth, _buttonsTop);
_mute->moveToLeft(_answerHangupRedial->x() + buttonWidth, _buttonsTop);
_addPeople->moveToLeft(_mute->x() + buttonWidth, _buttonsTop);
if (_startVideo) {
_startVideo->moveToLeft(_camera->x(), _camera->y());
}
@ -1118,7 +1247,9 @@ void Panel::paint(QRect clip) {
bool Panel::handleClose() const {
if (_call) {
if (_call->state() == Call::State::WaitingUserConfirmation
|| _call->state() == Call::State::Busy) {
|| _call->state() == Call::State::Busy
|| _call->state() == Call::State::Starting
|| _call->state() == Call::State::WaitingIncoming) {
_call->hangup();
} else {
window()->hide();
@ -1129,11 +1260,15 @@ bool Panel::handleClose() const {
}
not_null<Ui::RpWindow*> Panel::window() const {
return _window.window();
return _window->window();
}
not_null<Ui::RpWidget*> Panel::widget() const {
return _window.widget();
return _window->widget();
}
not_null<UserData*> Panel::user() const {
return _user;
}
void Panel::stateChanged(State state) {
@ -1141,16 +1276,16 @@ void Panel::stateChanged(State state) {
updateStatusText(state);
const auto isBusy = (state == State::Busy);
const auto isWaitingUser = (state == State::WaitingUserConfirmation);
_window->togglePowerSaveBlocker(!isBusy && !isWaitingUser);
if ((state != State::HangingUp)
&& (state != State::MigrationHangingUp)
&& (state != State::Ended)
&& (state != State::EndedByOtherDevice)
&& (state != State::FailedHangingUp)
&& (state != State::Failed)) {
const auto isBusy = (state == State::Busy);
const auto isWaitingUser = (state == State::WaitingUserConfirmation);
if (isBusy) {
_powerSaveBlocker = nullptr;
}
if (_startVideo && !isWaitingUser) {
_startVideo = nullptr;
} else if (!_startVideo && isWaitingUser) {
@ -1165,12 +1300,11 @@ void Panel::stateChanged(State state) {
}
_camera->setVisible(!_startVideo);
const auto windowHidden = window()->isHidden();
const auto toggleButton = [&](auto &&button, bool visible) {
button->toggle(
visible,
window()->isHidden()
? anim::type::instant
: anim::type::normal);
(windowHidden ? anim::type::instant : anim::type::normal));
};
const auto incomingWaiting = _call->isIncomingWaiting();
if (incomingWaiting) {
@ -1182,6 +1316,7 @@ void Panel::stateChanged(State state) {
toggleButton(
_screencast,
!(isBusy || isWaitingUser || incomingWaiting));
toggleButton(_addPeople, !isWaitingUser && _conferenceSupported);
const auto hangupShown = !_decline->toggled()
&& !_cancel->toggled();
if (_hangupShown != hangupShown) {
@ -1205,7 +1340,7 @@ void Panel::stateChanged(State state) {
refreshAnswerHangupRedialLabel();
}
if (!_call->isKeyShaForFingerprintReady()) {
_fingerprint.destroy();
_fingerprint = nullptr;
} else if (!_fingerprint) {
_fingerprint = CreateFingerprintAndSignalBars(widget(), _call);
updateControlsGeometry();
@ -1232,7 +1367,8 @@ void Panel::updateStatusText(State state) {
switch (state) {
case State::Starting:
case State::WaitingInit:
case State::WaitingInitAck: return tr::lng_call_status_connecting(tr::now);
case State::WaitingInitAck:
case State::MigrationHangingUp: return tr::lng_call_status_connecting(tr::now);
case State::Established: {
if (_call) {
auto durationMs = _call->getDurationMs();
@ -1250,7 +1386,10 @@ void Panel::updateStatusText(State state) {
case State::ExchangingKeys: return tr::lng_call_status_exchanging(tr::now);
case State::Waiting: return tr::lng_call_status_waiting(tr::now);
case State::Requesting: return tr::lng_call_status_requesting(tr::now);
case State::WaitingIncoming: return tr::lng_call_status_incoming(tr::now);
case State::WaitingIncoming:
return (_call->conferenceInvite()
? tr::lng_call_status_group_invite(tr::now)
: tr::lng_call_status_incoming(tr::now));
case State::Ringing: return tr::lng_call_status_ringing(tr::now);
case State::Busy: return tr::lng_call_status_busy(tr::now);
case State::WaitingUserConfirmation: return tr::lng_call_status_sure(tr::now);

View file

@ -7,14 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "base/timer.h"
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
#include "calls/calls_call.h"
#include "calls/group/ui/desktop_capture_choose_source.h"
#include "ui/effects/animations.h"
#include "ui/gl/gl_window.h"
#include "ui/rp_widget.h"
class Image;
@ -27,7 +22,16 @@ namespace Data {
class PhotoMedia;
} // namespace Data
namespace Main {
class SessionShow;
} // namespace Main
namespace Ui {
class Show;
class BoxContent;
class LayerWidget;
enum class LayerOption;
using LayerOptions = base::flags<LayerOption>;
class IconButton;
class CallButton;
class LayerManager;
@ -38,14 +42,17 @@ template <typename Widget>
class PaddingWrap;
class RpWindow;
class PopupMenu;
namespace GL {
enum class Backend;
} // namespace GL
namespace Platform {
struct SeparateTitleControls;
} // namespace Platform
} // namespace Ui
namespace Ui::Toast {
class Instance;
struct Config;
} // namespace Ui::Toast
namespace Ui::Platform {
struct SeparateTitleControls;
} // namespace Ui::Platform
namespace style {
struct CallSignalBars;
struct CallBodyLayout;
@ -53,23 +60,32 @@ struct CallBodyLayout;
namespace Calls {
class Window;
class Userpic;
class SignalBars;
class VideoBubble;
struct DeviceSelection;
struct ConferencePanelMigration;
class Panel final : private Group::Ui::DesktopCapture::ChooseSourceDelegate {
class Panel final
: public base::has_weak_ptr
, private Group::Ui::DesktopCapture::ChooseSourceDelegate {
public:
Panel(not_null<Call*> call);
~Panel();
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
[[nodiscard]] not_null<UserData*> user() const;
[[nodiscard]] bool isVisible() const;
[[nodiscard]] bool isActive() const;
[[nodiscard]] ConferencePanelMigration migrationInfo() const;
void showAndActivate();
void minimize();
void toggleFullScreen();
void replaceCall(not_null<Call*> call);
void closeBeforeDestroy();
void closeBeforeDestroy(bool windowIsReused = false);
QWidget *chooseSourceParent() override;
QString chooseSourceActiveDeviceId() override;
@ -83,6 +99,11 @@ public:
[[nodiscard]] rpl::producer<bool> startOutgoingRequests() const;
[[nodiscard]] std::shared_ptr<Main::SessionShow> sessionShow();
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
@ -96,14 +117,12 @@ private:
StartCall,
};
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
void paint(QRect clip);
void initWindow();
void initWidget();
void initControls();
void initConferenceInvite();
void reinitWithCall(Call *call);
void initLayout();
void initMediaDeviceToggles();
@ -142,40 +161,35 @@ private:
Call *_call = nullptr;
not_null<UserData*> _user;
Ui::GL::Window _window;
const std::unique_ptr<Ui::LayerManager> _layerBg;
std::shared_ptr<Window> _window;
std::unique_ptr<Incoming> _incoming;
#ifndef Q_OS_MAC
std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
#endif // !Q_OS_MAC
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
QSize _incomingFrameSize;
rpl::lifetime _callLifetime;
not_null<const style::CallBodyLayout*> _bodySt;
object_ptr<Ui::CallButton> _answerHangupRedial;
object_ptr<Ui::FadeWrap<Ui::CallButton>> _decline;
object_ptr<Ui::FadeWrap<Ui::CallButton>> _cancel;
base::unique_qptr<Ui::CallButton> _answerHangupRedial;
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _decline;
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _cancel;
bool _hangupShown = false;
bool _conferenceSupported = false;
bool _outgoingPreviewInBody = false;
std::optional<AnswerHangupRedialState> _answerHangupRedialState;
Ui::Animations::Simple _hangupShownProgress;
object_ptr<Ui::FadeWrap<Ui::CallButton>> _screencast;
object_ptr<Ui::CallButton> _camera;
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _screencast;
base::unique_qptr<Ui::CallButton> _camera;
Ui::CallButton *_cameraDeviceToggle = nullptr;
base::unique_qptr<Ui::CallButton> _startVideo;
object_ptr<Ui::FadeWrap<Ui::CallButton>> _mute;
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _mute;
Ui::CallButton *_audioDeviceToggle = nullptr;
object_ptr<Ui::FlatLabel> _name;
object_ptr<Ui::FlatLabel> _status;
object_ptr<Ui::RpWidget> _fingerprint = { nullptr };
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute = { nullptr };
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteLowBattery
= { nullptr };
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _addPeople;
base::unique_qptr<Ui::FlatLabel> _name;
base::unique_qptr<Ui::FlatLabel> _status;
base::unique_qptr<Ui::RpWidget> _conferenceParticipants;
base::unique_qptr<Ui::RpWidget> _fingerprint;
base::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute;
base::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteLowBattery;
std::unique_ptr<Userpic> _userpic;
std::unique_ptr<VideoBubble> _outgoingVideoBubble;
QPixmap _bottomShadow;
@ -200,6 +214,8 @@ private:
rpl::event_stream<bool> _startOutgoingRequests;
rpl::lifetime _lifetime;
};
} // namespace Calls

View file

@ -228,14 +228,14 @@ private:
TopBar::TopBar(
QWidget *parent,
const base::weak_ptr<Call> &call,
Call *call,
std::shared_ptr<Ui::Show> show)
: TopBar(parent, show, call, nullptr) {
}
TopBar::TopBar(
QWidget *parent,
const base::weak_ptr<GroupCall> &call,
GroupCall *call,
std::shared_ptr<Ui::Show> show)
: TopBar(parent, show, nullptr, call) {
}
@ -243,8 +243,8 @@ TopBar::TopBar(
TopBar::TopBar(
QWidget *parent,
std::shared_ptr<Ui::Show> show,
const base::weak_ptr<Call> &call,
const base::weak_ptr<GroupCall> &groupCall)
Call *call,
GroupCall *groupCall)
: RpWidget(parent)
, _call(call)
, _groupCall(groupCall)
@ -424,7 +424,7 @@ void TopBar::initControls() {
if (const auto call = _call.get()) {
call->hangup();
} else if (const auto group = _groupCall.get()) {
if (!group->peer()->canManageGroupCall()) {
if (!group->canManage()) {
group->hangup();
} else {
_show->showBox(

View file

@ -42,11 +42,11 @@ class TopBar : public Ui::RpWidget {
public:
TopBar(
QWidget *parent,
const base::weak_ptr<Call> &call,
Call *call,
std::shared_ptr<Ui::Show> show);
TopBar(
QWidget *parent,
const base::weak_ptr<GroupCall> &call,
GroupCall *call,
std::shared_ptr<Ui::Show> show);
~TopBar();
@ -64,8 +64,8 @@ private:
TopBar(
QWidget *parent,
std::shared_ptr<Ui::Show> show,
const base::weak_ptr<Call> &call,
const base::weak_ptr<GroupCall> &groupCall);
Call *call,
GroupCall *groupCall);
void initControls();
void setupInitialBrush();

View file

@ -0,0 +1,250 @@
/*
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_window.h"
#include "base/power_save_blocker.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/rp_window.h"
#include "ui/layers/layer_manager.h"
#include "ui/layers/show.h"
#include "styles/style_calls.h"
namespace Calls {
namespace {
class Show final : public Ui::Show {
public:
explicit Show(not_null<Window*> window);
~Show();
void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const override;
[[nodiscard]] not_null<QWidget*> toastParent() const override;
[[nodiscard]] bool valid() const override;
operator bool() const override;
private:
const base::weak_ptr<Window> _window;
};
Show::Show(not_null<Window*> window)
: _window(base::make_weak(window)) {
}
Show::~Show() = default;
void Show::showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const {
using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
using ObjectBox = object_ptr<Ui::BoxContent>;
if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
if (const auto window = _window.get()) {
window->showLayer(std::move(*layerWidget), options, animated);
}
} else if (auto box = std::get_if<ObjectBox>(&layer)) {
if (const auto window = _window.get()) {
window->showBox(std::move(*box), options, animated);
}
} else if (const auto window = _window.get()) {
window->hideLayer(animated);
}
}
not_null<QWidget*> Show::toastParent() const {
const auto window = _window.get();
Assert(window != nullptr);
return window->widget();
}
bool Show::valid() const {
return !_window.empty();
}
Show::operator bool() const {
return valid();
}
} // namespace
Window::Window()
: _layerBg(std::make_unique<Ui::LayerManager>(widget()))
#ifndef Q_OS_MAC
, _controls(Ui::Platform::SetupSeparateTitleControls(
window(),
st::callTitle,
[=](bool maximized) { _maximizeRequests.fire_copy(maximized); },
_controlsTop.value()))
#endif // !Q_OS_MAC
{
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
_layerBg->setHideByBackgroundClick(true);
}
Window::~Window() = default;
Ui::GL::Backend Window::backend() const {
return _window.backend();
}
not_null<Ui::RpWindow*> Window::window() const {
return _window.window();
}
not_null<Ui::RpWidget*> Window::widget() const {
return _window.widget();
}
void Window::raiseControls() {
#ifndef Q_OS_MAC
_controls->wrap.raise();
#endif // !Q_OS_MAC
}
void Window::setControlsStyle(const style::WindowTitle &st) {
#ifndef Q_OS_MAC
_controls->controls.setStyle(st);
#endif // Q_OS_MAC
}
void Window::setControlsShown(float64 shown) {
#ifndef Q_OS_MAC
_controlsTop = anim::interpolate(-_controls->wrap.height(), 0, shown);
#endif // Q_OS_MAC
}
int Window::controlsWrapTop() const {
#ifndef Q_OS_MAC
return _controls->wrap.y();
#else // Q_OS_MAC
return 0;
#endif // Q_OS_MAC
}
QRect Window::controlsGeometry() const {
#ifndef Q_OS_MAC
return _controls->controls.geometry();
#else // Q_OS_MAC
return QRect();
#endif // Q_OS_MAC
}
auto Window::controlsLayoutChanges() const
-> rpl::producer<Ui::Platform::TitleLayout> {
#ifndef Q_OS_MAC
return _controls->controls.layout().changes();
#else // Q_OS_MAC
return rpl::never<Ui::Platform::TitleLayout>();
#endif // Q_OS_MAC
}
bool Window::controlsHasHitTest(QPoint widgetPoint) const {
#ifndef Q_OS_MAC
using Result = Ui::Platform::HitTestResult;
const auto windowPoint = widget()->mapTo(window(), widgetPoint);
return (_controls->controls.hitTest(windowPoint) != Result::None);
#else // Q_OS_MAC
return false;
#endif // Q_OS_MAC
}
rpl::producer<bool> Window::maximizeRequests() const {
return _maximizeRequests.events();
}
base::weak_ptr<Ui::Toast::Instance> Window::showToast(
const QString &text,
crl::time duration) {
return Show(this).showToast(text, duration);
}
base::weak_ptr<Ui::Toast::Instance> Window::showToast(
TextWithEntities &&text,
crl::time duration) {
return Show(this).showToast(std::move(text), duration);
}
base::weak_ptr<Ui::Toast::Instance> Window::showToast(
Ui::Toast::Config &&config) {
return Show(this).showToast(std::move(config));
}
void Window::raiseLayers() {
_layerBg->raise();
}
const Ui::LayerWidget *Window::topShownLayer() const {
return _layerBg->topShownLayer();
}
void Window::showBox(object_ptr<Ui::BoxContent> box) {
showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal);
}
void Window::showBox(
object_ptr<Ui::BoxContent> box,
Ui::LayerOptions options,
anim::type animated) {
_showingLayer.fire({});
if (window()->width() < st::groupCallWidth
|| window()->height() < st::groupCallWidth) {
window()->resize(
std::max(window()->width(), st::groupCallWidth),
std::max(window()->height(), st::groupCallWidth));
}
_layerBg->showBox(std::move(box), options, animated);
}
void Window::showLayer(
std::unique_ptr<Ui::LayerWidget> layer,
Ui::LayerOptions options,
anim::type animated) {
_showingLayer.fire({});
if (window()->width() < st::groupCallWidth
|| window()->height() < st::groupCallWidth) {
window()->resize(
std::max(window()->width(), st::groupCallWidth),
std::max(window()->height(), st::groupCallWidth));
}
_layerBg->showLayer(std::move(layer), options, animated);
}
void Window::hideLayer(anim::type animated) {
_layerBg->hideAll(animated);
}
bool Window::isLayerShown() const {
return _layerBg->topShownLayer() != nullptr;
}
std::shared_ptr<Ui::Show> Window::uiShow() {
return std::make_shared<Show>(this);
}
void Window::togglePowerSaveBlocker(bool enabled) {
if (!enabled) {
_powerSaveBlocker = nullptr;
} else if (!_powerSaveBlocker) {
_powerSaveBlocker = std::make_unique<base::PowerSaveBlocker>(
base::PowerSaveBlockType::PreventDisplaySleep,
u"Video call is active"_q,
window()->windowHandle());
}
}
} // namespace Calls

View file

@ -0,0 +1,112 @@
/*
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 "base/object_ptr.h"
#include "ui/effects/animations.h"
#include "ui/gl/gl_window.h"
namespace base {
class PowerSaveBlocker;
} // namespace base
namespace style {
struct WindowTitle;
} // namespace style
namespace Ui {
class BoxContent;
class RpWindow;
class RpWidget;
class LayerManager;
class LayerWidget;
enum class LayerOption;
using LayerOptions = base::flags<LayerOption>;
class Show;
} // namespace Ui
namespace Ui::Platform {
struct SeparateTitleControls;
struct TitleLayout;
} // namespace Ui::Platform
namespace Ui::Toast {
struct Config;
class Instance;
} // namespace Ui::Toast
namespace Calls {
class Window final : public base::has_weak_ptr {
public:
Window();
~Window();
[[nodiscard]] Ui::GL::Backend backend() const;
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
void raiseControls();
void setControlsStyle(const style::WindowTitle &st);
void setControlsShown(float64 shown);
[[nodiscard]] int controlsWrapTop() const;
[[nodiscard]] QRect controlsGeometry() const;
[[nodiscard]] auto controlsLayoutChanges() const
-> rpl::producer<Ui::Platform::TitleLayout>;
[[nodiscard]] bool controlsHasHitTest(QPoint widgetPoint) const;
[[nodiscard]] rpl::producer<bool> maximizeRequests() const;
void raiseLayers();
[[nodiscard]] const Ui::LayerWidget *topShownLayer() const;
base::weak_ptr<Ui::Toast::Instance> showToast(
const QString &text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
TextWithEntities &&text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
Ui::Toast::Config &&config);
void showBox(object_ptr<Ui::BoxContent> box);
void showBox(
object_ptr<Ui::BoxContent> box,
Ui::LayerOptions options,
anim::type animated = anim::type::normal);
void showLayer(
std::unique_ptr<Ui::LayerWidget> layer,
Ui::LayerOptions options,
anim::type animated = anim::type::normal);
void hideLayer(anim::type animated = anim::type::normal);
[[nodiscard]] bool isLayerShown() const;
[[nodiscard]] rpl::producer<> showingLayer() const {
return _showingLayer.events();
}
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
void togglePowerSaveBlocker(bool enabled);
private:
Ui::GL::Window _window;
const std::unique_ptr<Ui::LayerManager> _layerBg;
#ifndef Q_OS_MAC
rpl::variable<int> _controlsTop = 0;
const std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
#endif // !Q_OS_MAC
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
rpl::event_stream<bool> _maximizeRequests;
rpl::event_stream<> _showingLayer;
};
} // namespace Calls

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@ struct GroupLevelsUpdate;
struct GroupNetworkState;
struct GroupParticipantDescription;
class VideoCaptureInterface;
enum class VideoCodecName;
} // namespace tgcalls
namespace base {
@ -42,6 +43,11 @@ struct GroupCallParticipant;
class GroupCall;
} // namespace Data
namespace TdE2E {
class Call;
class EncryptDecrypt;
} // namespace TdE2E
namespace Calls {
namespace Group {
@ -49,12 +55,17 @@ struct MuteRequest;
struct VolumeRequest;
struct ParticipantState;
struct JoinInfo;
struct ConferenceInfo;
struct RejoinEvent;
struct RtmpInfo;
enum class VideoQuality;
enum class Error;
} // namespace Group
struct InviteRequest;
struct InviteResult;
struct StartConferenceInfo;
enum class MuteState {
Active,
PushToTalk,
@ -217,6 +228,7 @@ public:
not_null<Delegate*> delegate,
Group::JoinInfo info,
const MTPInputGroupCall &inputCall);
GroupCall(not_null<Delegate*> delegate, StartConferenceInfo info);
~GroupCall();
[[nodiscard]] CallId id() const {
@ -237,6 +249,7 @@ public:
}
[[nodiscard]] bool scheduleStartSubscribed() const;
[[nodiscard]] bool rtmp() const;
[[nodiscard]] bool conference() const;
[[nodiscard]] bool listenersHidden() const;
[[nodiscard]] bool emptyRtmp() const;
[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;
@ -247,14 +260,19 @@ public:
void setRtmpInfo(const Group::RtmpInfo &value);
[[nodiscard]] Data::GroupCall *lookupReal() const;
[[nodiscard]] std::shared_ptr<Data::GroupCall> conferenceCall() const;
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
void applyInputCall(const MTPInputGroupCall &inputCall);
void startConference();
void start(TimeId scheduleDate, bool rtmp);
void hangup();
void discard();
void rejoinAs(Group::JoinInfo info);
void rejoinWithHash(const QString &hash);
void join(const MTPInputGroupCall &inputCall);
void initialJoin();
void initialJoinRequested();
void handleUpdate(const MTPUpdate &update);
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
void handlePossibleCreateOrJoinResponse(
@ -271,10 +289,21 @@ public:
void startScheduledNow();
void toggleScheduleStartSubscribed(bool subscribed);
void setNoiseSuppression(bool enabled);
void removeConferenceParticipants(
const base::flat_set<UserId> userIds,
bool removingStale = false);
bool emitShareScreenError();
bool emitShareCameraError();
void joinDone(
int64 serverTimeMs,
const MTPUpdates &result,
MuteState wasMuteState,
bool wasVideoStopped,
bool justCreated = false);
void joinFail(const QString &error);
[[nodiscard]] rpl::producer<Group::Error> errors() const {
return _errors.events();
}
@ -404,8 +433,10 @@ public:
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);
void inviteUsers(
const std::vector<InviteRequest> &requests,
Fn<void(InviteResult)> done);
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
void applyGlobalShortcutChanges();
@ -426,6 +457,7 @@ private:
struct SinkPointer;
static constexpr uint32 kDisabledSsrc = uint32(-1);
static constexpr int kSubChainsCount = 2;
struct LoadingPart {
std::shared_ptr<LoadPartTask> task;
@ -454,9 +486,14 @@ private:
Joining,
Leaving,
};
struct JoinPayload {
uint32 ssrc = 0;
QByteArray json;
};
struct JoinState {
uint32 ssrc = 0;
JoinAction action = JoinAction::None;
JoinPayload payload;
bool nextActionPending = false;
void finish(uint32 updatedSsrc = 0) {
@ -464,11 +501,26 @@ private:
ssrc = updatedSsrc;
}
};
struct SubChainPending {
QVector<MTPbytes> blocks;
int next = 0;
};
struct SubChainState {
std::vector<SubChainPending> pending;
mtpRequestId requestId = 0;
bool inShortPoll = false;
};
friend inline constexpr bool is_flag_type(SendUpdateType) {
return true;
}
GroupCall(
not_null<Delegate*> delegate,
Group::JoinInfo join,
StartConferenceInfo conference,
const MTPInputGroupCall &inputCall);
void broadcastPartStart(std::shared_ptr<LoadPartTask> task);
void broadcastPartCancel(not_null<LoadPartTask*> task);
void mediaChannelDescriptionsStart(
@ -490,6 +542,13 @@ private:
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
void handleUpdate(const MTPDupdateGroupCall &data);
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
void handleUpdate(const MTPDupdateGroupCallChainBlocks &data);
void applySubChainUpdate(
int subchain,
const QVector<MTPbytes> &blocks,
int next);
[[nodiscard]] auto lookupVideoCodecPreferences() const
-> std::vector<tgcalls::VideoCodecName>;
bool tryCreateController();
void destroyController();
bool tryCreateScreencast();
@ -508,6 +567,7 @@ private:
const std::optional<Data::GroupCallParticipant> &was,
const Data::GroupCallParticipant &now);
void applyMeInCallLocally();
void startRejoin();
void rejoin();
void leave();
void rejoin(not_null<PeerData*> as);
@ -518,6 +578,10 @@ private:
void rejoinPresentation();
void leavePresentation();
void checkNextJoinAction();
void sendJoinRequest();
void refreshLastBlockAndJoin();
void requestSubchainBlocks(int subchain, int height);
void sendOutboundBlock(QByteArray block);
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
@ -558,6 +622,9 @@ private:
void setupMediaDevices();
void setupOutgoingVideo();
void initConferenceE2E();
void setupConferenceCall();
void trackParticipantsWithAccess();
void setScreenEndpoint(std::string endpoint);
void setCameraEndpoint(std::string endpoint);
void addVideoOutput(const std::string &endpoint, SinkPointer sink);
@ -570,11 +637,25 @@ private:
void markTrackPaused(const VideoEndpoint &endpoint, bool paused);
void markTrackShown(const VideoEndpoint &endpoint, bool shown);
void processConferenceStart(StartConferenceInfo conference);
void inviteToConference(
InviteRequest request,
Fn<not_null<InviteResult*>()> resultAddress,
Fn<void()> finishRequest);
[[nodiscard]] int activeVideoSendersCount() const;
[[nodiscard]] MTPInputGroupCall inputCall() const;
[[nodiscard]] MTPInputGroupCall inputCallSafe() const;
const not_null<Delegate*> _delegate;
std::shared_ptr<Data::GroupCall> _conferenceCall;
std::unique_ptr<TdE2E::Call> _e2e;
std::shared_ptr<TdE2E::EncryptDecrypt> _e2eEncryptDecrypt;
rpl::variable<QByteArray> _emojiHash;
QByteArray _pendingOutboundBlock;
std::shared_ptr<StartConferenceInfo> _startConferenceInfo;
not_null<PeerData*> _peer; // Can change in legacy group migration.
rpl::event_stream<PeerData*> _peerStream;
not_null<History*> _history; // Can change in legacy group migration.
@ -583,6 +664,7 @@ private:
rpl::variable<State> _state = State::Creating;
base::flat_set<uint32> _unresolvedSsrcs;
rpl::event_stream<Error> _errors;
std::vector<Fn<void()>> _rejoinedCallbacks;
bool _recordingStoppedByMe = false;
bool _requestedVideoChannelsUpdateScheduled = false;
@ -601,6 +683,8 @@ private:
rpl::variable<not_null<PeerData*>> _joinAs;
std::vector<not_null<PeerData*>> _possibleJoinAs;
QString _joinHash;
QString _conferenceLinkSlug;
MsgId _conferenceJoinMessageId;
int64 _serverTimeMs = 0;
crl::time _serverTimeMsGotAt = 0;
@ -688,8 +772,13 @@ private:
bool _reloadedStaleCall = false;
int _rtmpVolume = 0;
SubChainState _subchains[kSubChainsCount];
rpl::lifetime _lifetime;
};
[[nodiscard]] TextWithEntities ComposeInviteResultToast(
const InviteResult &result);
} // namespace Calls

View file

@ -7,13 +7,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_common.h"
#include "apiwrap.h"
#include "base/platform/base_platform_info.h"
#include "base/random.h"
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
#include "boxes/share_box.h"
#include "calls/calls_instance.h"
#include "core/application.h"
#include "core/local_url_handlers.h"
#include "data/data_group_call.h"
#include "data/data_session.h"
#include "info/bot/starref/info_bot_starref_common.h"
#include "tde2e/tde2e_api.h"
#include "tde2e/tde2e_integration.h"
#include "ui/boxes/boost_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_media_view.h"
#include "styles/style_menu_icons.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h"
#include <QtWidgets/QApplication>
#include <QtGui/QClipboard>
namespace Calls::Group {
@ -50,4 +77,422 @@ object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox() {
#endif // Q_OS_MAC
}
object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
const auto logoSize = st::confcallJoinLogo.size();
const auto logoOuter = logoSize.grownBy(st::confcallJoinLogoPadding);
auto result = object_ptr<Ui::RpWidget>(parent);
const auto logo = result.data();
logo->resize(logo->width(), logoOuter.height());
logo->paintRequest() | rpl::start_with_next([=] {
if (logo->width() < logoOuter.width()) {
return;
}
auto p = QPainter(logo);
auto hq = PainterHighQualityEnabler(p);
const auto x = (logo->width() - logoOuter.width()) / 2;
const auto outer = QRect(QPoint(x, 0), logoOuter);
p.setBrush(st::windowBgActive);
p.setPen(Qt::NoPen);
p.drawEllipse(outer);
st::confcallJoinLogo.paintInCenter(p, outer);
}, logo->lifetime());
return result;
}
void ConferenceCallJoinConfirm(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Data::GroupCall> call,
UserData *maybeInviter,
Fn<void(Fn<void()> close)> join) {
box->setStyle(st::confcallJoinBox);
box->setWidth(st::boxWideWidth);
box->setNoContentMargin(true);
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
box->addRow(
MakeJoinCallLogo(box),
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_confcall_join_title(),
st::boxTitle)),
st::boxRowPadding + st::confcallLinkTitlePadding);
const auto wrapName = [&](not_null<PeerData*> peer) {
return rpl::single(Ui::Text::Bold(peer->shortName()));
};
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
(maybeInviter
? tr::lng_confcall_join_text_inviter(
lt_user,
wrapName(maybeInviter),
Ui::Text::RichLangValue)
: tr::lng_confcall_join_text(Ui::Text::RichLangValue)),
st::confcallLinkCenteredText),
st::boxRowPadding
)->setTryMakeSimilarLines(true);
const auto &participants = call->participants();
const auto known = int(participants.size());
if (known) {
const auto sep = box->addRow(
object_ptr<Ui::RpWidget>(box),
st::boxRowPadding + st::confcallJoinSepPadding);
sep->resize(sep->width(), st::normalFont->height);
sep->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(sep);
const auto line = st::lineWidth;
const auto top = st::confcallLinkFooterOrLineTop;
const auto fg = st::windowSubTextFg->b;
p.setOpacity(0.2);
p.fillRect(0, top, sep->width(), line, fg);
}, sep->lifetime());
auto peers = std::vector<not_null<PeerData*>>();
for (const auto &participant : participants) {
peers.push_back(participant.peer);
if (peers.size() == 3) {
break;
}
}
box->addRow(
CreateUserpicsWithMoreBadge(
box,
rpl::single(peers),
st::confcallJoinUserpics,
known),
st::boxRowPadding + st::confcallJoinUserpicsPadding);
const auto wrapByIndex = [&](int index) {
Expects(index >= 0 && index < known);
return wrapName(participants[index].peer);
};
auto text = (known == 1)
? tr::lng_confcall_already_joined_one(
lt_user,
wrapByIndex(0),
Ui::Text::RichLangValue)
: (known == 2)
? tr::lng_confcall_already_joined_two(
lt_user,
wrapByIndex(0),
lt_other,
wrapByIndex(1),
Ui::Text::RichLangValue)
: (known == 3)
? tr::lng_confcall_already_joined_three(
lt_user,
wrapByIndex(0),
lt_other,
wrapByIndex(1),
lt_third,
wrapByIndex(2),
Ui::Text::RichLangValue)
: tr::lng_confcall_already_joined_many(
lt_count,
rpl::single(1. * (std::max(known, call->fullCount()) - 2)),
lt_user,
wrapByIndex(0),
lt_other,
wrapByIndex(1),
Ui::Text::RichLangValue);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
st::confcallLinkCenteredText),
st::boxRowPadding
)->setTryMakeSimilarLines(true);
}
const auto joinAndClose = [=] {
join([weak = Ui::MakeWeak(box)] {
if (const auto strong = weak.data()) {
strong->closeBox();
}
});
};
Info::BotStarRef::AddFullWidthButton(
box,
tr::lng_confcall_join_button(),
joinAndClose,
&st::confcallLinkButton);
}
ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle() {
return {
.box = &st::groupCallLinkBox,
.menuToggle = &st::groupCallLinkMenu,
.menu = &st::groupCallPopupMenuWithIcons,
.close = &st::storiesStealthBoxClose,
.centerLabel = &st::groupCallLinkCenteredText,
.linkPreview = &st::groupCallLinkPreview,
.contextRevoke = &st::mediaMenuIconRemove,
.shareBox = std::make_shared<ShareBoxStyleOverrides>(
DarkShareBoxStyle()),
};
}
void ShowConferenceCallLinkBox(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<Data::GroupCall> call,
const ConferenceCallLinkArgs &args) {
const auto st = args.st;
const auto initial = args.initial;
const auto link = call->conferenceInviteLink();
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
base::unique_qptr<Ui::PopupMenu> menu;
bool resetting = false;
};
const auto state = box->lifetime().make_state<State>();
box->setStyle(st.box
? *st.box
: initial
? st::confcallLinkBoxInitial
: st::confcallLinkBox);
box->setWidth(st::boxWideWidth);
box->setNoContentMargin(true);
const auto close = box->addTopButton(
st.close ? *st.close : st::boxTitleClose,
[=] { box->closeBox(); });
if (!args.initial && call->canManage()) {
const auto toggle = Ui::CreateChild<Ui::IconButton>(
close->parentWidget(),
st.menuToggle ? *st.menuToggle : st::confcallLinkMenu);
const auto handler = [=] {
if (state->resetting) {
return;
}
state->resetting = true;
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
const auto weak = Ui::MakeWeak(box);
call->session().api().request(
MTPphone_ToggleGroupCallSettings(
MTP_flags(Flag::f_reset_invite_hash),
call->input(),
MTPbool()) // join_muted
).done([=](const MTPUpdates &result) {
call->session().api().applyUpdates(result);
ShowConferenceCallLinkBox(show, call, args);
if (const auto strong = weak.data()) {
strong->closeBox();
}
show->showToast({
.title = tr::lng_confcall_link_revoked_title(
tr::now),
.text = {
tr::lng_confcall_link_revoked_text(tr::now),
},
});
}).send();
};
toggle->setClickedCallback([=] {
state->menu = base::make_unique_q<Ui::PopupMenu>(
toggle,
st.menu ? *st.menu : st::popupMenuWithIcons);
state->menu->addAction(
tr::lng_confcall_link_revoke(tr::now),
handler,
(st.contextRevoke
? st.contextRevoke
: &st::menuIconRemove));
state->menu->popup(QCursor::pos());
});
close->geometryValue(
) | rpl::start_with_next([=](QRect geometry) {
toggle->moveToLeft(
geometry.x() - toggle->width(),
geometry.y());
}, close->lifetime());
}
box->addRow(
Info::BotStarRef::CreateLinkHeaderIcon(box, &call->session()),
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
box->addRow(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_confcall_link_title(),
st.box ? st.box->title : st::boxTitle)),
st::boxRowPadding + st::confcallLinkTitlePadding);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_confcall_link_about(),
(st.centerLabel
? *st.centerLabel
: st::confcallLinkCenteredText)),
st::boxRowPadding
)->setTryMakeSimilarLines(true);
Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);
const auto preview = box->addRow(
Info::BotStarRef::MakeLinkLabel(box, link, st.linkPreview));
Ui::AddSkip(box->verticalLayout());
const auto copyCallback = [=] {
QApplication::clipboard()->setText(link);
show->showToast(tr::lng_username_copied(tr::now));
};
const auto shareCallback = [=] {
FastShareLink(
show,
link,
st.shareBox ? *st.shareBox : ShareBoxStyleOverrides());
};
preview->setClickedCallback(copyCallback);
const auto share = box->addButton(
tr::lng_group_invite_share(),
shareCallback,
st::confcallLinkShareButton);
const auto copy = box->addButton(
tr::lng_group_invite_copy(),
copyCallback,
st::confcallLinkCopyButton);
rpl::combine(
box->widthValue(),
copy->widthValue(),
share->widthValue()
) | rpl::start_with_next([=] {
const auto width = st::boxWideWidth;
const auto padding = st::confcallLinkBox.buttonPadding;
const auto available = width - 2 * padding.right();
const auto buttonWidth = (available - padding.left()) / 2;
copy->resizeToWidth(buttonWidth);
share->resizeToWidth(buttonWidth);
copy->moveToLeft(padding.right(), copy->y(), width);
share->moveToRight(padding.right(), share->y(), width);
}, box->lifetime());
if (!initial) {
return;
}
const auto sep = Ui::CreateChild<Ui::FlatLabel>(
copy->parentWidget(),
tr::lng_confcall_link_or(),
st::confcallLinkFooterOr);
sep->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(sep);
const auto text = sep->textMaxWidth();
const auto white = (sep->width() - 2 * text) / 2;
const auto line = st::lineWidth;
const auto top = st::confcallLinkFooterOrLineTop;
const auto fg = st::windowSubTextFg->b;
p.setOpacity(0.4);
p.fillRect(0, top, white, line, fg);
p.fillRect(sep->width() - white, top, white, line, fg);
}, sep->lifetime());
const auto footer = Ui::CreateChild<Ui::FlatLabel>(
copy->parentWidget(),
tr::lng_confcall_link_join(
lt_link,
tr::lng_confcall_link_join_link(
lt_arrow,
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
[](QString v) { return Ui::Text::Link(v); }),
Ui::Text::WithEntities),
(st.centerLabel
? *st.centerLabel
: st::confcallLinkCenteredText));
footer->setTryMakeSimilarLines(true);
footer->setClickHandlerFilter([=](const auto &...) {
if (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) {
Core::App().calls().startOrJoinConferenceCall({
.call = call,
.linkSlug = std::move(slug),
});
}
return false;
});
copy->geometryValue() | rpl::start_with_next([=](QRect geometry) {
const auto width = st::boxWideWidth
- st::boxRowPadding.left()
- st::boxRowPadding.right();
footer->resizeToWidth(width);
const auto top = geometry.y()
+ geometry.height()
+ st::confcallLinkFooterOrTop;
sep->resizeToWidth(width / 2);
sep->move(
st::boxRowPadding.left() + (width - sep->width()) / 2,
top);
footer->moveToLeft(
st::boxRowPadding.left(),
top + sep->height() + st::confcallLinkFooterOrSkip);
}, footer->lifetime());
}));
}
void MakeConferenceCall(ConferenceFactoryArgs &&args) {
const auto show = std::move(args.show);
const auto finished = std::move(args.finished);
const auto session = &show->session();
const auto fail = [=](QString error) {
show->showToast(error);
if (const auto onstack = finished) {
onstack(false);
}
};
session->api().request(MTPphone_CreateConferenceCall(
MTP_flags(0),
MTP_int(base::RandomValue<int32>()),
MTPint256(), // public_key
MTPbytes(), // block
MTPDataJSON() // params
)).done([=](const MTPUpdates &result) {
auto call = session->data().sharedConferenceCallFind(result);
if (!call) {
fail(u"Call not found!"_q);
return;
}
session->api().applyUpdates(result);
const auto link = call ? call->conferenceInviteLink() : QString();
if (link.isEmpty()) {
fail(u"Call link not found!"_q);
return;
}
Calls::Group::ShowConferenceCallLinkBox(
show,
call,
{ .initial = true });
if (const auto onstack = finished) {
finished(true);
}
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
QString ExtractConferenceSlug(const QString &link) {
const auto local = Core::TryConvertUrlToLocal(link);
const auto parts1 = QStringView(local).split('#');
if (!parts1.isEmpty()) {
const auto parts2 = parts1.front().split('&');
if (!parts2.isEmpty()) {
const auto parts3 = parts2.front().split(u"slug="_q);
if (parts3.size() > 1) {
return parts3.back().toString();
}
}
}
return QString();
}
} // namespace Calls::Group

View file

@ -8,13 +8,81 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/object_ptr.h"
#include "base/weak_ptr.h"
class UserData;
struct ShareBoxStyleOverrides;
namespace style {
struct Box;
struct FlatLabel;
struct IconButton;
struct InputField;
struct PopupMenu;
} // namespace style
namespace Data {
class GroupCall;
} // namespace Data
namespace Main {
class SessionShow;
} // namespace Main
namespace Ui {
class Show;
class RpWidget;
class GenericBox;
} // namespace Ui
namespace TdE2E {
class Call;
} // namespace TdE2E
namespace tgcalls {
class VideoCaptureInterface;
} // namespace tgcalls
namespace Window {
class SessionController;
} // namespace Window
namespace Calls {
class Window;
struct InviteRequest {
not_null<UserData*> user;
bool video = false;
};
struct InviteResult {
std::vector<not_null<UserData*>> invited;
std::vector<not_null<UserData*>> alreadyIn;
std::vector<not_null<UserData*>> privacyRestricted;
std::vector<not_null<UserData*>> kicked;
std::vector<not_null<UserData*>> failed;
};
struct StartConferenceInfo {
std::shared_ptr<Main::SessionShow> show;
std::shared_ptr<Data::GroupCall> call;
QString linkSlug;
MsgId joinMessageId;
std::vector<InviteRequest> invite;
bool sharingLink = false;
bool migrating = false;
bool muted = false;
std::shared_ptr<tgcalls::VideoCaptureInterface> videoCapture;
QString videoCaptureScreenId;
};
struct ConferencePanelMigration {
std::shared_ptr<Window> window;
};
} // namespace Calls
namespace Calls::Group {
constexpr auto kDefaultVolume = 10000;
@ -93,4 +161,44 @@ using StickedTooltips = base::flags<StickedTooltip>;
[[nodiscard]] object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox();
[[nodiscard]] object_ptr<Ui::RpWidget> MakeJoinCallLogo(
not_null<QWidget*> parent);
void ConferenceCallJoinConfirm(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Data::GroupCall> call,
UserData *maybeInviter,
Fn<void(Fn<void()> close)> join);
struct ConferenceCallLinkStyleOverrides {
const style::Box *box = nullptr;
const style::IconButton *menuToggle = nullptr;
const style::PopupMenu *menu = nullptr;
const style::IconButton *close = nullptr;
const style::FlatLabel *centerLabel = nullptr;
const style::InputField *linkPreview = nullptr;
const style::icon *contextRevoke = nullptr;
std::shared_ptr<ShareBoxStyleOverrides> shareBox;
};
[[nodiscard]] ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle();
struct ConferenceCallLinkArgs {
ConferenceCallLinkStyleOverrides st;
bool initial = false;
};
void ShowConferenceCallLinkBox(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<Data::GroupCall> call,
const ConferenceCallLinkArgs &args);
struct ConferenceFactoryArgs {
std::shared_ptr<Main::SessionShow> show;
Fn<void(bool)> finished;
bool joining = false;
StartConferenceInfo info;
};
void MakeConferenceCall(ConferenceFactoryArgs &&args);
[[nodiscard]] QString ExtractConferenceSlug(const QString &link);
} // namespace Calls::Group

View file

@ -9,20 +9,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_participants.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_menu.h"
#include "calls/calls_call.h"
#include "calls/calls_instance.h"
#include "core/application.h"
#include "boxes/peer_lists_box.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_group_call.h"
#include "info/profile/info_profile_icon.h"
#include "main/session/session_show.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/scroll_area.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h" // membersMarginTop
#include "styles/style_calls.h"
#include "styles/style_dialogs.h" // searchedBarHeight
#include "styles/style_layers.h" // boxWideWidth
namespace Calls::Group {
namespace {
@ -56,6 +71,627 @@ namespace {
return result;
}
struct ConfInviteStyles {
const style::IconButton *video = nullptr;
const style::icon *videoActive = nullptr;
const style::IconButton *audio = nullptr;
const style::icon *audioActive = nullptr;
const style::SettingsButton *inviteViaLink = nullptr;
const style::icon *inviteViaLinkIcon = nullptr;
};
class ConfInviteRow final : public PeerListRow {
public:
ConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st);
void setAlreadyIn(bool alreadyIn);
void setVideo(bool video);
int elementsCount() const override;
QRect elementGeometry(int element, int outerWidth) const override;
bool elementDisabled(int element) const override;
bool elementOnlySelect(int element) const override;
void elementAddRipple(
int element,
QPoint point,
Fn<void()> updateCallback) override;
void elementsStopLastRipple() override;
void elementsPaint(
Painter &p,
int outerWidth,
bool selected,
int selectedElement) override;
private:
[[nodiscard]] const style::IconButton &buttonSt(int element) const;
const ConfInviteStyles &_st;
std::unique_ptr<Ui::RippleAnimation> _videoRipple;
std::unique_ptr<Ui::RippleAnimation> _audioRipple;
bool _alreadyIn = false;
bool _video = false;
};
struct PrioritizedSelector {
object_ptr<Ui::RpWidget> content = { nullptr };
Fn<void()> init;
Fn<bool(int, int, int)> overrideKey;
Fn<void(PeerListRowId)> deselect;
Fn<void()> activate;
rpl::producer<Ui::ScrollToRequest> scrollToRequests;
};
class ConfInviteController final : public ContactsBoxController {
public:
ConfInviteController(
not_null<Main::Session*> session,
ConfInviteStyles st,
base::flat_set<not_null<UserData*>> alreadyIn,
Fn<void()> shareLink,
std::vector<not_null<UserData*>> prioritize);
[[nodiscard]] rpl::producer<bool> hasSelectedValue() const;
[[nodiscard]] std::vector<InviteRequest> requests(
const std::vector<not_null<PeerData*>> &peers) const;
void noSearchSubmit();
[[nodiscard]] auto prioritizeScrollRequests() const
-> rpl::producer<Ui::ScrollToRequest>;
protected:
void prepareViewHook() override;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowElementClicked(not_null<PeerListRow*> row, int element) override;
bool handleDeselectForeignRow(PeerListRowId itemId) override;
bool overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) override;
private:
[[nodiscard]] int fullCount() const;
void toggleRowSelected(not_null<PeerListRow*> row, bool video);
[[nodiscard]] bool toggleRowGetChecked(
not_null<PeerListRow*> row,
bool video);
void addShareLinkButton();
void addPriorityInvites();
const ConfInviteStyles _st;
const base::flat_set<not_null<UserData*>> _alreadyIn;
const std::vector<not_null<UserData*>> _prioritize;
const Fn<void()> _shareLink;
PrioritizedSelector _prioritizeRows;
rpl::event_stream<Ui::ScrollToRequest> _prioritizeScrollRequests;
base::flat_set<not_null<UserData*>> _skip;
rpl::variable<bool> _hasSelected;
base::flat_set<not_null<UserData*>> _withVideo;
bool _lastSelectWithVideo = false;
};
[[nodiscard]] ConfInviteStyles ConfInviteDarkStyles() {
return {
.video = &st::confcallInviteVideo,
.videoActive = &st::confcallInviteVideoActive,
.audio = &st::confcallInviteAudio,
.audioActive = &st::confcallInviteAudioActive,
.inviteViaLink = &st::groupCallInviteLink,
.inviteViaLinkIcon = &st::groupCallInviteLinkIcon,
};
}
[[nodiscard]] ConfInviteStyles ConfInviteDefaultStyles() {
return {
.video = &st::createCallVideo,
.videoActive = &st::createCallVideoActive,
.audio = &st::createCallAudio,
.audioActive = &st::createCallAudioActive,
.inviteViaLink = &st::createCallInviteLink,
.inviteViaLinkIcon = &st::createCallInviteLinkIcon,
};
}
ConfInviteRow::ConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st)
: PeerListRow(user)
, _st(st) {
}
void ConfInviteRow::setAlreadyIn(bool alreadyIn) {
_alreadyIn = alreadyIn;
setDisabledState(alreadyIn ? State::DisabledChecked : State::Active);
}
void ConfInviteRow::setVideo(bool video) {
_video = video;
}
const style::IconButton &ConfInviteRow::buttonSt(int element) const {
return (element == 1)
? (_st.video ? *_st.video : st::createCallVideo)
: (_st.audio ? *_st.audio : st::createCallAudio);
}
int ConfInviteRow::elementsCount() const {
return _alreadyIn ? 0 : 2;
}
QRect ConfInviteRow::elementGeometry(int element, int outerWidth) const {
if (_alreadyIn || (element != 1 && element != 2)) {
return QRect();
}
const auto &st = buttonSt(element);
const auto size = QSize(st.width, st.height);
const auto margins = (element == 1)
? st::createCallVideoMargins
: st::createCallAudioMargins;
const auto right = margins.right();
const auto top = margins.top();
const auto side = (element == 1)
? outerWidth
: elementGeometry(1, outerWidth).x();
const auto left = side - right - size.width();
return QRect(QPoint(left, top), size);
}
bool ConfInviteRow::elementDisabled(int element) const {
return _alreadyIn
|| (checked()
&& ((_video && element == 1) || (!_video && element == 2)));
}
bool ConfInviteRow::elementOnlySelect(int element) const {
return false;
}
void ConfInviteRow::elementAddRipple(
int element,
QPoint point,
Fn<void()> updateCallback) {
if (_alreadyIn || (element != 1 && element != 2)) {
return;
}
auto &ripple = (element == 1) ? _videoRipple : _audioRipple;
const auto &st = buttonSt(element);
if (!ripple) {
auto mask = Ui::RippleAnimation::EllipseMask(QSize(
st.rippleAreaSize,
st.rippleAreaSize));
ripple = std::make_unique<Ui::RippleAnimation>(
st.ripple,
std::move(mask),
std::move(updateCallback));
}
ripple->add(point - st.rippleAreaPosition);
}
void ConfInviteRow::elementsStopLastRipple() {
if (_videoRipple) {
_videoRipple->lastStop();
}
if (_audioRipple) {
_audioRipple->lastStop();
}
}
void ConfInviteRow::elementsPaint(
Painter &p,
int outerWidth,
bool selected,
int selectedElement) {
if (_alreadyIn) {
return;
}
const auto paintElement = [&](int element) {
const auto &st = buttonSt(element);
auto &ripple = (element == 1) ? _videoRipple : _audioRipple;
const auto active = checked() && ((element == 1) ? _video : !_video);
const auto geometry = elementGeometry(element, outerWidth);
if (ripple) {
ripple->paint(
p,
geometry.x() + st.rippleAreaPosition.x(),
geometry.y() + st.rippleAreaPosition.y(),
outerWidth);
if (ripple->empty()) {
ripple.reset();
}
}
const auto selected = (element == selectedElement);
const auto &icon = active
? (element == 1
? (_st.videoActive
? *_st.videoActive
: st::createCallVideoActive)
: (_st.audioActive
? *_st.audioActive
: st::createCallAudioActive))
: (selected ? st.iconOver : st.icon);
icon.paintInCenter(p, geometry);
};
paintElement(1);
paintElement(2);
}
[[nodiscard]] PrioritizedSelector PrioritizedInviteSelector(
const ConfInviteStyles &st,
std::vector<not_null<UserData*>> users,
Fn<bool(not_null<PeerListRow*>, bool, anim::type)> toggleGetChecked,
Fn<bool()> lastSelectWithVideo,
Fn<void(bool)> setLastSelectWithVideo) {
class PrioritizedController final : public PeerListController {
public:
PrioritizedController(
const ConfInviteStyles &st,
std::vector<not_null<UserData*>> users,
Fn<bool(
not_null<PeerListRow*>,
bool,
anim::type)> toggleGetChecked,
Fn<bool()> lastSelectWithVideo,
Fn<void(bool)> setLastSelectWithVideo)
: _st(st)
, _users(std::move(users))
, _toggleGetChecked(std::move(toggleGetChecked))
, _lastSelectWithVideo(std::move(lastSelectWithVideo))
, _setLastSelectWithVideo(std::move(setLastSelectWithVideo)) {
Expects(!_users.empty());
}
void prepare() override {
for (const auto user : _users) {
delegate()->peerListAppendRow(
std::make_unique<ConfInviteRow>(user, _st));
}
delegate()->peerListRefreshRows();
}
void loadMoreRows() override {
}
void rowClicked(not_null<PeerListRow*> row) override {
toggleRowSelected(row, _lastSelectWithVideo());
}
void rowElementClicked(
not_null<PeerListRow*> row,
int element) override {
if (row->checked()) {
static_cast<ConfInviteRow*>(row.get())->setVideo(
element == 1);
_setLastSelectWithVideo(element == 1);
} else if (element == 1) {
toggleRowSelected(row, true);
} else if (element == 2) {
toggleRowSelected(row, false);
}
}
void toggleRowSelected(not_null<PeerListRow*> row, bool video) {
delegate()->peerListSetRowChecked(
row,
_toggleGetChecked(row, video, anim::type::normal));
}
Main::Session &session() const override {
return _users.front()->session();
}
void toggleFirst() {
rowClicked(delegate()->peerListRowAt(0));
}
private:
const ConfInviteStyles &_st;
std::vector<not_null<UserData*>> _users;
Fn<bool(not_null<PeerListRow*>, bool, anim::type)> _toggleGetChecked;
Fn<bool()> _lastSelectWithVideo;
Fn<void(bool)> _setLastSelectWithVideo;
};
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
const auto delegate = container->lifetime().make_state<
PeerListContentDelegateSimple
>();
const auto controller = container->lifetime(
).make_state<PrioritizedController>(
st,
users,
toggleGetChecked,
lastSelectWithVideo,
setLastSelectWithVideo);
controller->setStyleOverrides(&st::createCallList);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
const auto activate = [=] {
content->submitted();
};
content->noSearchSubmits() | rpl::start_with_next([=] {
controller->toggleFirst();
}, content->lifetime());
delegate->setContent(content);
controller->setDelegate(delegate);
Ui::AddDivider(container);
const auto overrideKey = [=](int direction, int from, int to) {
if (!content->isVisible()) {
return false;
} else if (direction > 0 && from < 0 && to >= 0) {
if (content->hasSelection()) {
const auto was = content->selectedIndex();
const auto now = content->selectSkip(1).reallyMovedTo;
if (was != now) {
return true;
}
content->clearSelection();
} else {
content->selectSkip(1);
return true;
}
} else if (direction < 0 && to < 0) {
if (!content->hasSelection()) {
content->selectLast();
} else if (from >= 0 || content->hasSelection()) {
content->selectSkip(-1);
}
}
return false;
};
const auto deselect = [=](PeerListRowId rowId) {
if (const auto row = delegate->peerListFindRow(rowId)) {
delegate->peerListSetRowChecked(row, false);
}
};
const auto init = [=] {
for (const auto &user : users) {
if (const auto row = delegate->peerListFindRow(user->id.value)) {
delegate->peerListSetRowChecked(
row,
toggleGetChecked(row, false, anim::type::instant));
}
}
};
return {
.content = std::move(result),
.init = init,
.overrideKey = overrideKey,
.deselect = deselect,
.activate = activate,
.scrollToRequests = content->scrollToRequests(),
};
}
ConfInviteController::ConfInviteController(
not_null<Main::Session*> session,
ConfInviteStyles st,
base::flat_set<not_null<UserData*>> alreadyIn,
Fn<void()> shareLink,
std::vector<not_null<UserData*>> prioritize)
: ContactsBoxController(session)
, _st(st)
, _alreadyIn(std::move(alreadyIn))
, _prioritize(std::move(prioritize))
, _shareLink(std::move(shareLink)) {
if (!_shareLink) {
_skip.reserve(_prioritize.size());
for (const auto user : _prioritize) {
_skip.emplace(user);
}
}
}
rpl::producer<bool> ConfInviteController::hasSelectedValue() const {
return _hasSelected.value();
}
std::vector<InviteRequest> ConfInviteController::requests(
const std::vector<not_null<PeerData*>> &peers) const {
auto result = std::vector<InviteRequest>();
result.reserve(peers.size());
for (const auto &peer : peers) {
if (const auto user = peer->asUser()) {
result.push_back({ user, _withVideo.contains(user) });
}
}
return result;
}
std::unique_ptr<PeerListRow> ConfInviteController::createRow(
not_null<UserData*> user) {
if (user->isSelf()
|| user->isBot()
|| user->isServiceUser()
|| user->isInaccessible()
|| _skip.contains(user)) {
return nullptr;
}
auto result = std::make_unique<ConfInviteRow>(user, _st);
if (_alreadyIn.contains(user)) {
result->setAlreadyIn(true);
}
if (_withVideo.contains(user)) {
result->setVideo(true);
}
return result;
}
int ConfInviteController::fullCount() const {
return _alreadyIn.size()
+ delegate()->peerListSelectedRowsCount()
+ (_alreadyIn.contains(session().user()) ? 1 : 0);
}
void ConfInviteController::rowClicked(not_null<PeerListRow*> row) {
toggleRowSelected(row, _lastSelectWithVideo);
}
void ConfInviteController::rowElementClicked(
not_null<PeerListRow*> row,
int element) {
if (row->checked()) {
static_cast<ConfInviteRow*>(row.get())->setVideo(element == 1);
_lastSelectWithVideo = (element == 1);
} else if (element == 1) {
toggleRowSelected(row, true);
} else if (element == 2) {
toggleRowSelected(row, false);
}
}
bool ConfInviteController::handleDeselectForeignRow(PeerListRowId itemId) {
if (_prioritizeRows.deselect) {
const auto userId = peerToUser(PeerId(itemId));
if (ranges::contains(_prioritize, session().data().user(userId))) {
_prioritizeRows.deselect(itemId);
return true;
}
}
return false;
}
bool ConfInviteController::overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) {
return _prioritizeRows.overrideKey
&& _prioritizeRows.overrideKey(direction, fromIndex, toIndex);
}
void ConfInviteController::toggleRowSelected(
not_null<PeerListRow*> row,
bool video) {
delegate()->peerListSetRowChecked(row, toggleRowGetChecked(row, video));
// row may have been destroyed here, from search.
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
}
bool ConfInviteController::toggleRowGetChecked(
not_null<PeerListRow*> row,
bool video) {
auto count = fullCount();
const auto conferenceLimit = session().appConfig().confcallSizeLimit();
if (!row->checked() && count >= conferenceLimit) {
delegate()->peerListUiShow()->showToast(
tr::lng_group_call_invite_limit(tr::now));
return false;
}
const auto real = static_cast<ConfInviteRow*>(row.get());
if (!row->checked()) {
real->setVideo(video);
_lastSelectWithVideo = video;
}
const auto user = row->peer()->asUser();
if (!row->checked() && video) {
_withVideo.emplace(user);
} else {
_withVideo.remove(user);
}
return !row->checked();
}
void ConfInviteController::noSearchSubmit() {
if (const auto onstack = _prioritizeRows.activate) {
onstack();
} else if (delegate()->peerListFullRowsCount() > 0) {
rowClicked(delegate()->peerListRowAt(0));
}
}
auto ConfInviteController::prioritizeScrollRequests() const
-> rpl::producer<Ui::ScrollToRequest> {
return _prioritizeScrollRequests.events();
}
void ConfInviteController::prepareViewHook() {
if (_shareLink) {
addShareLinkButton();
} else if (!_prioritize.empty()) {
addPriorityInvites();
}
}
void ConfInviteController::addPriorityInvites() {
const auto toggleGetChecked = [=](
not_null<PeerListRow*> row,
bool video,
anim::type animated) {
const auto result = toggleRowGetChecked(row, video);
delegate()->peerListSetForeignRowChecked(
row,
result,
animated);
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
return result;
};
_prioritizeRows = PrioritizedInviteSelector(
_st,
_prioritize,
toggleGetChecked,
[=] { return _lastSelectWithVideo; },
[=](bool video) { _lastSelectWithVideo = video; });
if (auto &scrollTo = _prioritizeRows.scrollToRequests) {
std::move(
scrollTo
) | rpl::start_to_stream(_prioritizeScrollRequests, lifetime());
}
if (const auto onstack = _prioritizeRows.init) {
onstack();
// Force finishing in instant adding checked rows bunch.
delegate()->peerListAddSelectedPeers(
std::vector<not_null<PeerData*>>());
}
delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content));
}
void ConfInviteController::addShareLinkButton() {
auto button = object_ptr<Ui::PaddingWrap<Ui::SettingsButton>>(
nullptr,
object_ptr<Ui::SettingsButton>(
nullptr,
tr::lng_profile_add_via_link(),
(_st.inviteViaLink
? *_st.inviteViaLink
: st::createCallInviteLink)),
style::margins(0, st::membersMarginTop, 0, 0));
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
button->entity(),
(_st.inviteViaLinkIcon
? *_st.inviteViaLinkIcon
: st::createCallInviteLinkIcon),
QPoint());
button->entity()->heightValue(
) | rpl::start_with_next([=](int height) {
icon->moveToLeft(
st::createCallInviteLinkIconPosition.x(),
(height - st::groupCallInviteLinkIcon.height()) / 2);
}, icon->lifetime());
button->entity()->setClickedCallback(_shareLink);
button->entity()->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Enter);
}) | rpl::start_with_next([=] {
delegate()->peerListMouseLeftGeometry();
}, button->lifetime());
delegate()->peerListSetAboveWidget(std::move(button));
}
} // namespace
InviteController::InviteController(
@ -167,19 +803,73 @@ std::unique_ptr<PeerListRow> InviteContactsController::createRow(
object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<GroupCall*> call,
Fn<void(TextWithEntities&&)> showToast) {
Fn<void(TextWithEntities&&)> showToast,
Fn<void()> shareConferenceLink) {
const auto real = call->lookupReal();
if (!real) {
return nullptr;
}
const auto peer = call->peer();
auto alreadyIn = peer->owner().invitedToCallUsers(real->id());
const auto conference = call->conference();
const auto weak = base::make_weak(call);
const auto &invited = peer->owner().invitedToCallUsers(real->id());
auto alreadyIn = base::flat_set<not_null<UserData*>>();
alreadyIn.reserve(invited.size() + real->participants().size() + 1);
alreadyIn.emplace(peer->session().user());
for (const auto &participant : real->participants()) {
if (const auto user = participant.peer->asUser()) {
alreadyIn.emplace(user);
}
}
alreadyIn.emplace(peer->session().user());
for (const auto &[user, calling] : invited) {
if (!conference || calling) {
alreadyIn.emplace(user);
}
}
if (conference) {
const auto close = std::make_shared<Fn<void()>>();
const auto shareLink = [=] {
Expects(shareConferenceLink != nullptr);
shareConferenceLink();
(*close)();
};
auto controller = std::make_unique<ConfInviteController>(
&real->session(),
ConfInviteDarkStyles(),
alreadyIn,
shareLink,
std::vector<not_null<UserData*>>());
const auto raw = controller.get();
raw->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
auto initBox = [=](not_null<PeerListBox*> box) {
box->setTitle(tr::lng_group_call_invite_conf());
raw->hasSelectedValue() | rpl::start_with_next([=](bool has) {
box->clearButtons();
if (has) {
box->addButton(tr::lng_group_call_confcall_add(), [=] {
const auto call = weak.get();
if (!call) {
return;
}
const auto done = [=](InviteResult result) {
(*close)();
showToast({ ComposeInviteResultToast(result) });
};
call->inviteUsers(
raw->requests(box->collectSelectedRows()),
done);
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}, box->lifetime());
*close = crl::guard(box, [=] { box->closeBox(); });
};
return Box<PeerListBox>(std::move(controller), initBox);
}
auto controller = std::make_unique<InviteController>(peer, alreadyIn);
controller->setStyleOverrides(
&st::groupCallInviteMembersList,
@ -194,30 +884,31 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
const auto weak = base::make_weak(call);
const auto invite = [=](const std::vector<not_null<UserData*>> &users) {
const auto call = weak.get();
if (!call) {
return;
}
const auto result = call->inviteUsers(users);
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
auto requests = ranges::views::all(
users
) | ranges::views::transform([](not_null<UserData*> user) {
return InviteRequest{ user };
}) | ranges::to_vector;
call->inviteUsers(std::move(requests), [=](InviteResult result) {
if (result.invited.size() == 1) {
showToast(tr::lng_group_call_invite_done_user(
tr::now,
lt_user,
Ui::Text::Bold((*user)->firstName),
Ui::Text::Bold(result.invited.front()->firstName),
Ui::Text::WithEntities));
} else if (const auto count = std::get_if<int>(&result)) {
if (*count > 0) {
} else if (result.invited.size() > 1) {
showToast(tr::lng_group_call_invite_done_many(
tr::now,
lt_count,
*count,
result.invited.size(),
Ui::Text::RichLangValue));
}
} else {
Unexpected("Result in GroupCall::inviteUsers.");
}
});
};
const auto inviteWithAdd = [=](
std::shared_ptr<Ui::Show> show,
@ -308,4 +999,215 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
return Box<PeerListsBox>(std::move(controllers), initBox);
}
object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<InviteRequest>)> inviteUsers,
Fn<void()> shareLink) {
const auto user = call->user();
const auto weak = base::make_weak(call);
auto alreadyIn = base::flat_set<not_null<UserData*>>{ user };
auto controller = std::make_unique<ConfInviteController>(
&user->session(),
ConfInviteDarkStyles(),
alreadyIn,
shareLink,
std::vector<not_null<UserData*>>());
const auto raw = controller.get();
raw->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
auto initBox = [=](not_null<PeerListBox*> box) {
box->setTitle(tr::lng_group_call_invite_conf());
raw->hasSelectedValue() | rpl::start_with_next([=](bool has) {
box->clearButtons();
if (has) {
box->addButton(tr::lng_group_call_invite_button(), [=] {
const auto call = weak.get();
if (!call) {
return;
}
inviteUsers(raw->requests(box->collectSelectedRows()));
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}, box->lifetime());
};
return Box<PeerListBox>(std::move(controller), initBox);
}
not_null<Ui::RpWidget*> CreateReActivateHeader(not_null<QWidget*> parent) {
const auto result = Ui::CreateChild<Ui::VerticalLayout>(parent);
result->add(
MakeJoinCallLogo(result),
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
result->add(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
result,
object_ptr<Ui::FlatLabel>(
result,
tr::lng_confcall_inactive_title(),
st::boxTitle)),
st::boxRowPadding + st::confcallLinkTitlePadding);
result->add(
object_ptr<Ui::FlatLabel>(
result,
tr::lng_confcall_inactive_about(),
st::confcallLinkCenteredText),
st::boxRowPadding + st::confcallLinkTitlePadding
)->setTryMakeSimilarLines(true);
Ui::AddDivider(result);
return result;
}
void InitReActivate(not_null<PeerListBox*> box) {
box->setTitle(rpl::producer<TextWithEntities>(nullptr));
box->setNoContentMargin(true);
const auto header = CreateReActivateHeader(box);
header->resizeToWidth(st::boxWideWidth);
header->heightValue() | rpl::start_with_next([=](int height) {
box->setAddedTopScrollSkip(height, true);
}, header->lifetime());
header->moveToLeft(0, 0);
}
object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
std::shared_ptr<Data::GroupCall> call,
MsgId inviteMsgId,
std::vector<not_null<UserData*>> prioritize) {
auto controller = std::make_unique<ConfInviteController>(
&call->session(),
ConfInviteDefaultStyles(),
base::flat_set<not_null<UserData*>>(),
nullptr,
std::move(prioritize));
const auto raw = controller.get();
raw->setStyleOverrides(&st::createCallList);
const auto initBox = [=](not_null<PeerListBox*> box) {
InitReActivate(box);
box->noSearchSubmits() | rpl::start_with_next([=] {
raw->noSearchSubmit();
}, box->lifetime());
raw->prioritizeScrollRequests(
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
box->scrollTo(request);
}, box->lifetime());
const auto join = [=] {
const auto weak = Ui::MakeWeak(box);
auto selected = raw->requests(box->collectSelectedRows());
Core::App().calls().startOrJoinConferenceCall({
.call = call,
.joinMessageId = inviteMsgId,
.invite = std::move(selected),
});
if (const auto strong = weak.data()) {
strong->closeBox();
}
};
box->addButton(
rpl::conditional(
raw->hasSelectedValue(),
tr::lng_group_call_confcall_add(),
tr::lng_create_group_create()),
join);
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
};
return Box<PeerListBox>(std::move(controller), initBox);
}
object_ptr<Ui::BoxContent> PrepareCreateCallBox(
not_null<::Window::SessionController*> window,
Fn<void()> created,
MsgId discardedInviteMsgId,
std::vector<not_null<UserData*>> prioritize) {
struct State {
bool creatingLink = false;
QPointer<PeerListBox> box;
};
const auto state = std::make_shared<State>();
const auto finished = [=](bool ok) {
if (!ok) {
state->creatingLink = false;
} else {
if (const auto strong = state->box.data()) {
strong->closeBox();
}
if (const auto onstack = created) {
onstack();
}
}
};
const auto shareLink = [=] {
if (state->creatingLink) {
return;
}
state->creatingLink = true;
MakeConferenceCall({
.show = window->uiShow(),
.finished = finished,
});
};
auto controller = std::make_unique<ConfInviteController>(
&window->session(),
ConfInviteDefaultStyles(),
base::flat_set<not_null<UserData*>>(),
discardedInviteMsgId ? Fn<void()>() : shareLink,
std::move(prioritize));
const auto raw = controller.get();
if (discardedInviteMsgId) {
raw->setStyleOverrides(&st::createCallList);
}
const auto initBox = [=](not_null<PeerListBox*> box) {
if (discardedInviteMsgId) {
InitReActivate(box);
} else {
box->setTitle(tr::lng_confcall_create_title());
}
box->noSearchSubmits() | rpl::start_with_next([=] {
raw->noSearchSubmit();
}, box->lifetime());
raw->prioritizeScrollRequests(
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
box->scrollTo(request);
}, box->lifetime());
const auto create = [=] {
auto selected = raw->requests(box->collectSelectedRows());
if (selected.size() != 1 || discardedInviteMsgId) {
Core::App().calls().startOrJoinConferenceCall({
.show = window->uiShow(),
.invite = std::move(selected),
});
} else {
const auto &invite = selected.front();
Core::App().calls().startOutgoingCall(
invite.user,
invite.video);
}
finished(true);
};
box->addButton(
rpl::conditional(
raw->hasSelectedValue(),
tr::lng_group_call_confcall_add(),
tr::lng_create_group_create()),
create);
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
};
auto result = Box<PeerListBox>(std::move(controller), initBox);
state->box = result.data();
return result;
}
} // namespace Calls::Group

View file

@ -11,9 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/add_participants_box.h"
namespace Calls {
class Call;
class GroupCall;
struct InviteRequest;
} // namespace Calls
namespace Data {
class GroupCall;
} // namespace Data
namespace Calls::Group {
class InviteController final : public ParticipantsBoxController {
@ -77,6 +83,23 @@ private:
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<GroupCall*> call,
Fn<void(TextWithEntities&&)> showToast);
Fn<void(TextWithEntities&&)> showToast,
Fn<void()> shareConferenceLink = nullptr);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<InviteRequest>)> inviteUsers,
Fn<void()> shareLink);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
std::shared_ptr<Data::GroupCall> call,
MsgId inviteMsgId,
std::vector<not_null<UserData*>> prioritize);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareCreateCallBox(
not_null<::Window::SessionController*> window,
Fn<void()> created = nullptr,
MsgId discardedInviteMsgId = 0,
std::vector<not_null<UserData*>> prioritize = {});
} // namespace Calls::Group

View file

@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_volume_item.h"
#include "calls/group/calls_group_members_row.h"
#include "calls/group/calls_group_viewport.h"
#include "calls/calls_emoji_fingerprint.h"
#include "calls/calls_instance.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
@ -107,6 +109,9 @@ private:
[[nodiscard]] std::unique_ptr<Row> createRow(
const Data::GroupCallParticipant &participant);
[[nodiscard]] std::unique_ptr<Row> createInvitedRow(
not_null<PeerData*> participantPeer,
bool calling);
[[nodiscard]] std::unique_ptr<Row> createWithAccessRow(
not_null<PeerData*> participantPeer);
[[nodiscard]] bool isMe(not_null<PeerData*> participantPeer) const;
@ -128,7 +133,8 @@ private:
void updateRow(
not_null<Row*> row,
const std::optional<Data::GroupCallParticipant> &was,
const Data::GroupCallParticipant *participant);
const Data::GroupCallParticipant *participant,
Row::State noParticipantState = Row::State::Invited);
void updateRowInSoundingMap(
not_null<Row*> row,
bool wasSounding,
@ -162,8 +168,13 @@ private:
const VideoEndpoint &endpoint,
bool active);
void appendInvitedUsers();
void partitionRows();
void setupInvitedUsers();
[[nodiscard]] bool appendInvitedUsers();
void setupWithAccessUsers();
[[nodiscard]] bool appendWithAccessUsers();
void scheduleRaisedHandStatusRemove();
void refreshWithAccessRows(base::flat_set<UserId> &&nowIds);
void hideRowsWithVideoExcept(const VideoEndpoint &large);
void showAllHiddenRows();
@ -205,6 +216,8 @@ private:
Ui::RoundRect _narrowRoundRect;
QImage _narrowShadow;
base::flat_set<UserId> _withAccess;
rpl::lifetime _lifetime;
};
@ -414,6 +427,9 @@ void Members::Controller::subscribeToChanges(not_null<Data::GroupCall*> real) {
if (const auto row = findRow(participantPeer)) {
if (isMe(participantPeer)) {
updateRow(row, update.was, nullptr);
} else if (_withAccess.contains(peerToUser(participantPeer->id))) {
updateRow(row, update.was, nullptr, Row::State::WithAccess);
partitionRows();
} else {
removeRow(row);
delegate()->peerListRefreshRows();
@ -431,10 +447,6 @@ void Members::Controller::subscribeToChanges(not_null<Data::GroupCall*> real) {
) | rpl::start_with_next([=](const VideoStateToggle &update) {
toggleVideoEndpointActive(update.endpoint, update.value);
}, _lifetime);
if (_prepared) {
appendInvitedUsers();
}
}
void Members::Controller::toggleVideoEndpointActive(
@ -481,13 +493,22 @@ void Members::Controller::toggleVideoEndpointActive(
}
void Members::Controller::appendInvitedUsers() {
bool Members::Controller::appendInvitedUsers() {
auto changed = false;
if (const auto id = _call->id()) {
for (const auto &user : _peer->owner().invitedToCallUsers(id)) {
if (auto row = createInvitedRow(user)) {
const auto &invited = _peer->owner().invitedToCallUsers(id);
for (const auto &[user, calling] : invited) {
if (auto row = createInvitedRow(user, calling)) {
delegate()->peerListAppendRow(std::move(row));
changed = true;
}
}
}
return changed;
}
void Members::Controller::setupInvitedUsers() {
if (appendInvitedUsers()) {
delegate()->peerListRefreshRows();
}
@ -496,22 +517,98 @@ void Members::Controller::appendInvitedUsers() {
) | rpl::filter([=](const Invite &invite) {
return (invite.id == _call->id());
}) | rpl::start_with_next([=](const Invite &invite) {
if (auto row = createInvitedRow(invite.user)) {
const auto user = invite.user;
if (invite.removed) {
if (const auto row = findRow(user)) {
if (row->state() == Row::State::Invited
|| row->state() == Row::State::Calling) {
delegate()->peerListRemoveRow(row);
delegate()->peerListRefreshRows();
}
}
} else if (auto row = createInvitedRow(user, invite.calling)) {
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListRefreshRows();
}
}, _lifetime);
}
bool Members::Controller::appendWithAccessUsers() {
auto changed = false;
for (const auto id : _withAccess) {
if (auto row = createWithAccessRow(_peer->owner().user(id))) {
changed = true;
delegate()->peerListAppendRow(std::move(row));
}
}
return changed;
}
void Members::Controller::setupWithAccessUsers() {
const auto conference = _call->conferenceCall().get();
if (!conference) {
return;
}
conference->participantsWithAccessValue(
) | rpl::start_with_next([=](base::flat_set<UserId> &&nowIds) {
for (auto i = begin(_withAccess); i != end(_withAccess);) {
const auto oldId = *i;
if (nowIds.remove(oldId)) {
++i;
continue;
}
const auto user = _peer->owner().user(oldId);
if (const auto row = findRow(user)) {
if (row->state() == Row::State::WithAccess) {
removeRow(row);
}
}
i = _withAccess.erase(i);
}
auto partition = false;
auto partitionChecked = false;
for (const auto nowId : nowIds) {
const auto user = _peer->owner().user(nowId);
if (!findRow(user)) {
if (auto row = createWithAccessRow(user)) {
if (!partitionChecked) {
partitionChecked = true;
if (const auto count = delegate()->peerListFullRowsCount()) {
const auto last = delegate()->peerListRowAt(count - 1);
const auto state = static_cast<Row*>(last.get())->state();
if (state == Row::State::Invited
|| state == Row::State::Calling) {
partition = true;
}
}
}
delegate()->peerListAppendRow(std::move(row));
}
}
_withAccess.emplace(nowId);
}
if (partition) {
delegate()->peerListPartitionRows([](const PeerListRow &row) {
const auto state = static_cast<const Row&>(row).state();
return (state != Row::State::Invited)
&& (state != Row::State::Calling);
});
}
delegate()->peerListRefreshRows();
}, _lifetime);
}
void Members::Controller::updateRow(
const std::optional<Data::GroupCallParticipant> &was,
const Data::GroupCallParticipant &now) {
auto reorderIfInvitedBefore = 0;
auto reorderIfNonRealBefore = 0;
auto checkPosition = (Row*)nullptr;
auto addedToBottom = (Row*)nullptr;
if (const auto row = findRow(now.peer)) {
if (row->state() == Row::State::Invited) {
reorderIfInvitedBefore = row->absoluteIndex();
if (row->state() == Row::State::Invited
|| row->state() == Row::State::Calling
|| row->state() == Row::State::WithAccess) {
reorderIfNonRealBefore = row->absoluteIndex();
}
updateRow(row, was, &now);
if ((now.speaking && (!was || !was->speaking))
@ -523,7 +620,7 @@ void Members::Controller::updateRow(
if (row->speaking()) {
delegate()->peerListPrependRow(std::move(row));
} else {
reorderIfInvitedBefore = delegate()->peerListFullRowsCount();
reorderIfNonRealBefore = delegate()->peerListFullRowsCount();
if (now.raisedHandRating != 0) {
checkPosition = row.get();
} else {
@ -533,20 +630,21 @@ void Members::Controller::updateRow(
}
delegate()->peerListRefreshRows();
}
static constexpr auto kInvited = Row::State::Invited;
const auto reorder = [&] {
const auto count = reorderIfInvitedBefore;
const auto count = reorderIfNonRealBefore;
if (count <= 0) {
return false;
}
const auto row = delegate()->peerListRowAt(
reorderIfInvitedBefore - 1).get();
return (static_cast<Row*>(row)->state() == kInvited);
reorderIfNonRealBefore - 1).get();
using State = Row::State;
const auto state = static_cast<Row*>(row)->state();
return (state == State::Invited)
|| (state == State::Calling)
|| (state == State::WithAccess);
}();
if (reorder) {
delegate()->peerListPartitionRows([](const PeerListRow &row) {
return static_cast<const Row&>(row).state() != kInvited;
});
partitionRows();
}
if (checkPosition) {
checkRowPosition(checkPosition);
@ -570,6 +668,27 @@ void Members::Controller::updateRow(
}
}
void Members::Controller::partitionRows() {
auto hadWithAccess = false;
delegate()->peerListPartitionRows([&](const PeerListRow &row) {
using State = Row::State;
const auto state = static_cast<const Row&>(row).state();
if (state == State::WithAccess) {
hadWithAccess = true;
}
return (state != State::Invited)
&& (state != State::Calling)
&& (state != State::WithAccess);
});
if (hadWithAccess) {
delegate()->peerListPartitionRows([](const PeerListRow &row) {
const auto state = static_cast<const Row&>(row).state();
return (state != Row::State::Invited)
&& (state != Row::State::Calling);
});
}
}
bool Members::Controller::allRowsAboveAreSpeaking(not_null<Row*> row) const {
const auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count; ++i) {
@ -615,7 +734,7 @@ bool Members::Controller::needToReorder(not_null<Row*> row) const {
if (row->speaking()) {
return !allRowsAboveAreSpeaking(row);
} else if (!_peer->canManageGroupCall()) {
} else if (!_call->canManage()) {
// Raising hands reorder participants only for voice chat admins.
return false;
}
@ -684,7 +803,7 @@ void Members::Controller::checkRowPosition(not_null<Row*> row) {
return proj(a) > proj(b);
};
};
delegate()->peerListSortRows(_peer->canManageGroupCall()
delegate()->peerListSortRows(_call->canManage()
? makeComparator(projForAdmin)
: makeComparator(projForOther));
}
@ -692,14 +811,21 @@ void Members::Controller::checkRowPosition(not_null<Row*> row) {
void Members::Controller::updateRow(
not_null<Row*> row,
const std::optional<Data::GroupCallParticipant> &was,
const Data::GroupCallParticipant *participant) {
const Data::GroupCallParticipant *participant,
Row::State noParticipantState) {
const auto wasSounding = row->sounding();
const auto wasSsrc = was ? was->ssrc : 0;
const auto wasAdditionalSsrc = was
? GetAdditionalAudioSsrc(was->videoParams)
: 0;
row->setSkipLevelUpdate(_skipRowLevelUpdate);
row->updateState(participant);
if (participant) {
row->updateState(*participant);
} else if (noParticipantState == Row::State::WithAccess) {
row->updateStateWithAccess();
} else {
row->updateStateInvited(noParticipantState == Row::State::Calling);
}
const auto wasNoSounding = _soundingRowBySsrc.empty();
updateRowInSoundingMap(
@ -842,7 +968,8 @@ void Members::Controller::prepare() {
}
loadMoreRows();
appendInvitedUsers();
setupWithAccessUsers();
setupInvitedUsers();
_prepared = true;
setupListChangeViewers();
@ -893,6 +1020,12 @@ void Members::Controller::prepareRows(not_null<Data::GroupCall*> real) {
delegate()->peerListAppendRow(std::move(row));
}
}
if (appendWithAccessUsers()) {
changed = true;
}
if (appendInvitedUsers()) {
changed = true;
}
if (changed) {
delegate()->peerListRefreshRows();
}
@ -919,7 +1052,7 @@ bool Members::Controller::rowIsMe(not_null<PeerData*> participantPeer) {
}
bool Members::Controller::rowCanMuteMembers() {
return _peer->canManageGroupCall();
return _call->canManage();
}
void Members::Controller::rowUpdateRow(not_null<Row*> row) {
@ -973,15 +1106,21 @@ void Members::Controller::rowPaintIcon(
return;
}
const auto narrow = (state.style == MembersRowStyle::Narrow);
if (state.invited) {
if (state.invited || state.calling) {
if (narrow) {
st::groupCallNarrowInvitedIcon.paintInCenter(p, rect);
(state.invited
? st::groupCallNarrowInvitedIcon
: st::groupCallNarrowCallingIcon).paintInCenter(p, rect);
} else {
st::groupCallMemberInvited.paintInCenter(
const auto &icon = state.invited
? st::groupCallMemberInvited
: st::groupCallMemberCalling;
const auto shift = state.invited
? st::groupCallMemberInvitedPosition
: st::groupCallMemberCallingPosition;
icon.paintInCenter(
p,
QRect(
rect.topLeft() + st::groupCallMemberInvitedPosition,
st::groupCallMemberInvited.size()));
QRect(rect.topLeft() + shift, icon.size()));
}
return;
}
@ -1189,6 +1328,9 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
const auto participantPeer = row->peer();
const auto real = static_cast<Row*>(row.get());
const auto muteState = real->state();
if (muteState == Row::State::WithAccess) {
return nullptr;
}
const auto muted = (muteState == Row::State::Muted)
|| (muteState == Row::State::RaisedHand);
const auto addCover = !_call->rtmp();
@ -1218,22 +1360,22 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
window->invokeForSessionController(
account,
participantPeer,
[&](not_null<Window::SessionController*> newController) {
[&](not_null<::Window::SessionController*> newController) {
callback(newController);
newController->widget()->activate();
});
}
};
const auto showProfile = [=] {
withActiveWindow([=](not_null<Window::SessionController*> window) {
withActiveWindow([=](not_null<::Window::SessionController*> window) {
window->showPeerInfo(participantPeer);
});
};
const auto showHistory = [=] {
withActiveWindow([=](not_null<Window::SessionController*> window) {
withActiveWindow([=](not_null<::Window::SessionController*> window) {
window->showPeerHistory(
participantPeer,
Window::SectionShow::Way::Forward);
::Window::SectionShow::Way::Forward);
});
};
const auto removeFromVoiceChat = crl::guard(this, [=] {
@ -1317,7 +1459,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
false,
static_cast<Row*>(row.get()));
} else if (participant
&& (!isMe(participantPeer) || _peer->canManageGroupCall())
&& (!isMe(participantPeer) || _call->canManage())
&& (participant->ssrc != 0
|| GetAdditionalAudioSsrc(participant->videoParams) != 0)) {
addMuteActionsToContextMenu(
@ -1340,6 +1482,29 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
removeHand);
}
} else {
const auto invited = (muteState == Row::State::Invited)
|| (muteState == Row::State::Calling);
const auto conference = _call->conferenceCall().get();
if (conference
&& participantPeer->isUser()
&& invited) {
const auto id = conference->id();
const auto cancelInvite = [=](bool discard) {
Core::App().calls().declineOutgoingConferenceInvite(
id,
participantPeer->asUser(),
discard);
};
if (muteState == Row::State::Calling) {
result->addAction(
tr::lng_group_call_context_stop_ringing(tr::now),
[=] { cancelInvite(false); });
}
result->addAction(
tr::lng_group_call_context_cancel_invite(tr::now),
[=] { cancelInvite(true); });
result->addSeparator();
}
result->addAction(
(participantPeer->isUser()
? tr::lng_context_view_profile(tr::now)
@ -1354,9 +1519,12 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
}
const auto canKick = [&] {
const auto user = participantPeer->asUser();
if (static_cast<Row*>(row.get())->state()
== Row::State::Invited) {
if (muteState == Row::State::Invited
|| muteState == Row::State::Calling
|| muteState == Row::State::WithAccess) {
return false;
} else if (conference && _call->canManage()) {
return true;
} else if (const auto chat = _peer->asChat()) {
return chat->amCreator()
|| (user
@ -1387,11 +1555,11 @@ void Members::Controller::addMuteActionsToContextMenu(
bool participantIsCallAdmin,
not_null<Row*> row) {
const auto muteUnmuteString = [=](bool muted, bool mutedByMe) {
return (muted && _peer->canManageGroupCall())
return (muted && _call->canManage())
? tr::lng_group_call_context_unmute(tr::now)
: mutedByMe
? tr::lng_group_call_context_unmute_for_me(tr::now)
: _peer->canManageGroupCall()
: _call->canManage()
? tr::lng_group_call_context_mute(tr::now)
: tr::lng_group_call_context_mute_for_me(tr::now);
};
@ -1484,11 +1652,13 @@ void Members::Controller::addMuteActionsToContextMenu(
const auto muteAction = [&]() -> QAction* {
if (muteState == Row::State::Invited
|| muteState == Row::State::Calling
|| muteState == Row::State::WithAccess
|| _call->rtmp()
|| isMe(participantPeer)
|| (muteState == Row::State::Inactive
&& participantIsCallAdmin
&& _peer->canManageGroupCall())) {
&& _call->canManage())) {
return nullptr;
}
auto callback = [=] {
@ -1538,12 +1708,29 @@ std::unique_ptr<Row> Members::Controller::createRow(
}
std::unique_ptr<Row> Members::Controller::createInvitedRow(
not_null<PeerData*> participantPeer,
bool calling) {
if (const auto row = findRow(participantPeer)) {
if (row->state() == Row::State::Invited
|| row->state() == Row::State::Calling) {
row->updateStateInvited(calling);
delegate()->peerListUpdateRow(row);
}
return nullptr;
}
const auto state = calling ? Row::State::Calling : Row::State::Invited;
auto result = std::make_unique<Row>(this, participantPeer);
updateRow(result.get(), std::nullopt, nullptr, state);
return result;
}
std::unique_ptr<Row> Members::Controller::createWithAccessRow(
not_null<PeerData*> participantPeer) {
if (findRow(participantPeer)) {
return nullptr;
}
auto result = std::make_unique<Row>(this, participantPeer);
updateRow(result.get(), std::nullopt, nullptr);
updateRow(result.get(), std::nullopt, nullptr, Row::State::WithAccess);
return result;
}
@ -1559,6 +1746,9 @@ Members::Members(
, _listController(std::make_unique<Controller>(call, parent, mode))
, _layout(_scroll->setOwnedWidget(
object_ptr<Ui::VerticalLayout>(_scroll.data())))
, _fingerprint(call->conference()
? _layout->add(object_ptr<Ui::RpWidget>(_layout.get()))
: nullptr)
, _videoWrap(_layout->add(object_ptr<Ui::RpWidget>(_layout.get())))
, _viewport(
std::make_unique<Viewport>(
@ -1567,6 +1757,7 @@ Members::Members(
backend)) {
setupList();
setupAddMember(call);
setupFingerprint();
setContent(_list);
setupFakeRoundCorners();
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
@ -1615,6 +1806,7 @@ rpl::producer<int> Members::desiredHeightValue() const {
return rpl::combine(
heightValue(),
_addMemberButton.value(),
_shareLinkButton.value(),
_listController->fullCountValue(),
_mode.value()
) | rpl::map([=] {
@ -1626,8 +1818,11 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
using namespace rpl::mappers;
const auto peer = call->peer();
const auto conference = call->conference();
const auto canAddByPeer = [=](not_null<PeerData*> peer) {
if (peer->isBroadcast()) {
if (conference) {
return rpl::single(true) | rpl::type_erased();
} else if (peer->isBroadcast()) {
return rpl::single(false) | rpl::type_erased();
}
return rpl::combine(
@ -1638,6 +1833,9 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
}) | rpl::type_erased();
};
const auto canInviteByLinkByPeer = [=](not_null<PeerData*> peer) {
if (conference) {
return rpl::single(true) | rpl::type_erased();
}
const auto channel = peer->asChannel();
if (!channel) {
return rpl::single(false) | rpl::type_erased();
@ -1661,6 +1859,8 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
_canInviteByLink = canInviteByLinkByPeer(channel);
});
const auto baseIndex = _layout->count() - 2;
rpl::combine(
_canAddMembers.value(),
_canInviteByLink.value(),
@ -1672,11 +1872,18 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
_addMemberButton = nullptr;
updateControlsGeometry();
}
if (const auto old = _shareLinkButton.current()) {
delete old;
_shareLinkButton = nullptr;
updateControlsGeometry();
}
return;
}
auto addMember = Settings::CreateButtonWithIcon(
_layout.get(),
tr::lng_group_call_invite(),
(conference
? tr::lng_group_call_invite_conf()
: tr::lng_group_call_invite()),
st::groupCallAddMember,
{ .icon = &st::groupCallAddMemberIcon });
addMember->clicks(
@ -1687,7 +1894,22 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
addMember->resizeToWidth(_layout->width());
delete _addMemberButton.current();
_addMemberButton = addMember.data();
_layout->insert(3, std::move(addMember));
_layout->insert(baseIndex, std::move(addMember));
if (conference) {
auto shareLink = Settings::CreateButtonWithIcon(
_layout.get(),
tr::lng_group_invite_share(),
st::groupCallAddMember,
{ .icon = &st::groupCallShareLinkIcon });
shareLink->clicks() | rpl::to_empty | rpl::start_to_stream(
_shareLinkRequests,
shareLink->lifetime());
shareLink->show();
shareLink->resizeToWidth(_layout->width());
delete _shareLinkButton.current();
_shareLinkButton = shareLink.data();
_layout->insert(baseIndex + 1, std::move(shareLink));
}
}, lifetime());
updateControlsGeometry();
@ -1713,13 +1935,15 @@ void Members::setMode(PanelMode mode) {
}
QRect Members::getInnerGeometry() const {
const auto shareLink = _shareLinkButton.current();
const auto addMembers = _addMemberButton.current();
const auto share = shareLink ? shareLink->height() : 0;
const auto add = addMembers ? addMembers->height() : 0;
return QRect(
0,
-_scroll->scrollTop(),
width(),
_list->y() + _list->height() + _bottomSkip->height() + add);
_list->y() + _list->height() + _bottomSkip->height() + add + share);
}
rpl::producer<int> Members::fullCountValue() const {
@ -1783,6 +2007,23 @@ void Members::setupList() {
}, _scroll->lifetime());
}
void Members::setupFingerprint() {
if (const auto raw = _fingerprint) {
auto badge = SetupFingerprintBadge(
raw->lifetime(),
_call->emojiHashValue());
std::move(badge.repaints) | rpl::start_to_stream(
_fingerprintRepaints,
raw->lifetime());
_fingerprintState = badge.state;
SetupFingerprintBadgeWidget(
raw,
_fingerprintState,
_fingerprintRepaints.events());
}
}
void Members::trackViewportGeometry() {
_call->videoEndpointLargeValue(
) | rpl::start_with_next([=](const VideoEndpoint &large) {
@ -1894,16 +2135,22 @@ void Members::setupFakeRoundCorners() {
const auto bottomleft = create({ 0, shift });
const auto bottomright = create({ shift, shift });
rpl::combine(
_list->geometryValue(),
_addMemberButton.value() | rpl::map([=](Ui::RpWidget *widget) {
const auto heightValue = [=](Ui::RpWidget *widget) {
topleft->raise();
topright->raise();
bottomleft->raise();
bottomright->raise();
return widget ? widget->heightValue() : rpl::single(0);
}) | rpl::flatten_latest()
) | rpl::start_with_next([=](QRect list, int addMembers) {
};
rpl::combine(
_list->geometryValue(),
_addMemberButton.value() | rpl::map(
heightValue
) | rpl::flatten_latest(),
_shareLinkButton.value() | rpl::map(
heightValue
) | rpl::flatten_latest()
) | rpl::start_with_next([=](QRect list, int addMembers, int shareLink) {
const auto left = list.x();
const auto top = list.y() - _topSkip->height();
const auto right = left + list.width() - topright->width();
@ -1912,6 +2159,7 @@ void Members::setupFakeRoundCorners() {
+ list.height()
+ _bottomSkip->height()
+ addMembers
+ shareLink
- bottomleft->height();
topleft->move(left, top);
topright->move(right, top);

View file

@ -25,6 +25,7 @@ class GroupCall;
namespace Calls {
class GroupCall;
struct FingerprintBadgeState;
} // namespace Calls
namespace Calls::Group {
@ -59,6 +60,9 @@ public:
[[nodiscard]] rpl::producer<> addMembersRequests() const {
return _addMemberRequests.events();
}
[[nodiscard]] rpl::producer<> shareLinkRequests() const {
return _shareLinkRequests.events();
}
[[nodiscard]] MembersRow *lookupRow(not_null<PeerData*> peer) const;
[[nodiscard]] not_null<MembersRow*> rtmpFakeRow(
@ -93,6 +97,7 @@ private:
void setupAddMember(not_null<GroupCall*> call);
void resizeToList();
void setupList();
void setupFingerprint();
void setupFakeRoundCorners();
void trackViewportGeometry();
@ -103,13 +108,18 @@ private:
object_ptr<Ui::ScrollArea> _scroll;
std::unique_ptr<Controller> _listController;
not_null<Ui::VerticalLayout*> _layout;
Ui::RpWidget *_fingerprint = nullptr;
rpl::event_stream<> _fingerprintRepaints;
const FingerprintBadgeState *_fingerprintState = nullptr;
const not_null<Ui::RpWidget*> _videoWrap;
std::unique_ptr<Viewport> _viewport;
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;
rpl::variable<Ui::RpWidget*> _shareLinkButton = nullptr;
RpWidget *_topSkip = nullptr;
RpWidget *_bottomSkip = nullptr;
ListWidget *_list = nullptr;
rpl::event_stream<> _addMemberRequests;
rpl::event_stream<> _shareLinkRequests;
mutable std::unique_ptr<MembersRow> _rtmpFakeRow;

View file

@ -138,41 +138,52 @@ void MembersRow::setSkipLevelUpdate(bool value) {
_skipLevelUpdate = value;
}
void MembersRow::updateState(
const Data::GroupCallParticipant *participant) {
setVolume(participant
? participant->volume
: Group::kDefaultVolume);
if (!participant) {
setState(State::Invited);
void MembersRow::updateStateInvited(bool calling) {
setVolume(Group::kDefaultVolume);
setState(calling ? State::Calling : State::Invited);
setSounding(false);
setSpeaking(false);
_mutedByMe = false;
_raisedHandRating = 0;
} else if (!participant->muted
|| (participant->sounding && participant->ssrc != 0)
|| (participant->additionalSounding
&& GetAdditionalAudioSsrc(participant->videoParams) != 0)) {
setState(State::Active);
setSounding((participant->sounding && participant->ssrc != 0)
|| (participant->additionalSounding
&& GetAdditionalAudioSsrc(participant->videoParams) != 0));
setSpeaking((participant->speaking && participant->ssrc != 0)
|| (participant->additionalSpeaking
&& GetAdditionalAudioSsrc(participant->videoParams) != 0));
_mutedByMe = participant->mutedByMe;
refreshStatus();
}
void MembersRow::updateStateWithAccess() {
setVolume(Group::kDefaultVolume);
setState(State::WithAccess);
setSounding(false);
setSpeaking(false);
_mutedByMe = false;
_raisedHandRating = 0;
} else if (participant->canSelfUnmute) {
refreshStatus();
}
void MembersRow::updateState(const Data::GroupCallParticipant &participant) {
setVolume(participant.volume);
if (!participant.muted
|| (participant.sounding && participant.ssrc != 0)
|| (participant.additionalSounding
&& GetAdditionalAudioSsrc(participant.videoParams) != 0)) {
setState(State::Active);
setSounding((participant.sounding && participant.ssrc != 0)
|| (participant.additionalSounding
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
setSpeaking((participant.speaking && participant.ssrc != 0)
|| (participant.additionalSpeaking
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
_mutedByMe = participant.mutedByMe;
_raisedHandRating = 0;
} else if (participant.canSelfUnmute) {
setState(State::Inactive);
setSounding(false);
setSpeaking(false);
_mutedByMe = participant->mutedByMe;
_mutedByMe = participant.mutedByMe;
_raisedHandRating = 0;
} else {
setSounding(false);
setSpeaking(false);
_mutedByMe = participant->mutedByMe;
_raisedHandRating = participant->raisedHandRating;
_mutedByMe = participant.mutedByMe;
_raisedHandRating = participant.raisedHandRating;
setState(_raisedHandRating ? State::RaisedHand : State::Muted);
}
refreshStatus();
@ -450,6 +461,20 @@ void MembersRow::paintMuteIcon(
_delegate->rowPaintIcon(p, iconRect, computeIconState(style));
}
QString MembersRow::generateName() {
const auto result = peer()->name();
return result.isEmpty()
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
: result;
}
QString MembersRow::generateShortName() {
const auto result = peer()->shortName();
return result.isEmpty()
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
: result;
}
auto MembersRow::generatePaintUserpicCallback(bool forceRound)
-> PaintRoundImageCallback {
return [=](Painter &p, int x, int y, int outerWidth, int size) {
@ -613,11 +638,16 @@ void MembersRow::paintComplexStatusText(
availableWidth -= skip;
const auto &font = st::normalFont;
const auto useAbout = !_about.isEmpty()
&& (_state != State::WithAccess)
&& (_state != State::Invited)
&& (_state != State::Calling)
&& (style != MembersRowStyle::Video)
&& ((_state == State::RaisedHand && !_raisedHandStatus)
|| (_state != State::RaisedHand && !_speaking));
if (!useAbout
&& _state != State::Invited
&& _state != State::Calling
&& _state != State::WithAccess
&& !_mutedByMe) {
paintStatusIcon(p, x, y, st, font, selected, narrowMode);
@ -663,6 +693,10 @@ void MembersRow::paintComplexStatusText(
? tr::lng_group_call_muted_by_me_status(tr::now)
: _delegate->rowIsMe(peer())
? tr::lng_status_connecting(tr::now)
: (_state == State::WithAccess)
? tr::lng_group_call_blockchain_only_status(tr::now)
: (_state == State::Calling)
? tr::lng_group_call_calling_status(tr::now)
: tr::lng_group_call_invited_status(tr::now)));
}
}
@ -676,6 +710,7 @@ QSize MembersRow::rightActionSize() const {
bool MembersRow::rightActionDisabled() const {
return _delegate->rowIsMe(peer())
|| (_state == State::Invited)
|| (_state == State::Calling)
|| !_delegate->rowCanMuteMembers();
}
@ -701,7 +736,9 @@ void MembersRow::rightActionPaint(
size.width(),
size.height(),
outerWidth);
if (_state == State::Invited) {
if (_state == State::Invited
|| _state == State::Calling
|| _state == State::WithAccess) {
_actionRipple = nullptr;
}
if (_actionRipple) {
@ -731,6 +768,7 @@ MembersRowDelegate::IconState MembersRow::computeIconState(
.mutedByMe = _mutedByMe,
.raisedHand = (_state == State::RaisedHand),
.invited = (_state == State::Invited),
.calling = (_state == State::Calling),
.style = style,
};
}

View file

@ -24,7 +24,7 @@ struct PeerUserpicView;
namespace Calls::Group {
enum class MembersRowStyle {
enum class MembersRowStyle : uchar {
Default,
Narrow,
Video,
@ -40,6 +40,7 @@ public:
bool mutedByMe = false;
bool raisedHand = false;
bool invited = false;
bool calling = false;
MembersRowStyle style = MembersRowStyle::Default;
};
virtual bool rowIsMe(not_null<PeerData*> participantPeer) = 0;
@ -75,11 +76,15 @@ public:
Muted,
RaisedHand,
Invited,
Calling,
WithAccess,
};
void setAbout(const QString &about);
void setSkipLevelUpdate(bool value);
void updateState(const Data::GroupCallParticipant *participant);
void updateState(const Data::GroupCallParticipant &participant);
void updateStateInvited(bool calling);
void updateStateWithAccess();
void updateLevel(float level);
void updateBlobAnimation(crl::time now);
void clearRaisedHandStatus();
@ -122,6 +127,8 @@ public:
bool selected,
bool actionSelected) override;
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
void paintComplexUserpic(

View file

@ -416,10 +416,13 @@ void LeaveBox(
not_null<GroupCall*> call,
bool discardChecked,
BoxContext context) {
const auto conference = call->conference();
const auto livestream = call->peer()->isBroadcast();
const auto scheduled = (call->scheduleDate() != 0);
if (!scheduled) {
box->setTitle(livestream
box->setTitle(conference
? tr::lng_group_call_leave_title_call()
: livestream
? tr::lng_group_call_leave_title_channel()
: tr::lng_group_call_leave_title());
}
@ -431,12 +434,14 @@ void LeaveBox(
? (livestream
? tr::lng_group_call_close_sure_channel()
: tr::lng_group_call_close_sure())
: (livestream
: (conference
? tr::lng_group_call_leave_sure_call()
: livestream
? tr::lng_group_call_leave_sure_channel()
: tr::lng_group_call_leave_sure())),
(inCall ? st::groupCallBoxLabel : st::boxLabel)),
scheduled ? st::boxPadding : st::boxRowPadding);
const auto discard = call->peer()->canManageGroupCall()
const auto discard = call->canManage()
? box->addRow(object_ptr<Ui::Checkbox>(
box.get(),
(scheduled
@ -490,20 +495,24 @@ void FillMenu(
Fn<void(object_ptr<Ui::BoxContent>)> showBox) {
const auto weak = base::make_weak(call);
const auto resolveReal = [=] {
const auto real = peer->groupCall();
const auto strong = weak.get();
return (real && strong && (real->id() == strong->id()))
? real
: nullptr;
if (const auto strong = weak.get()) {
if (const auto real = strong->lookupReal()) {
return real;
}
}
return (Data::GroupCall*)nullptr;
};
const auto real = resolveReal();
if (!real) {
return;
}
const auto conference = call->conference();
const auto addEditJoinAs = call->showChooseJoinAs();
const auto addEditTitle = call->canManage();
const auto addEditRecording = call->canManage() && !real->scheduleDate();
const auto addEditTitle = !conference && call->canManage();
const auto addEditRecording = !conference
&& call->canManage()
&& !real->scheduleDate();
const auto addScreenCast = !wide
&& call->videoIsWorking()
&& !real->scheduleDate();

View file

@ -16,7 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_group_invite_controller.h"
#include "calls/group/ui/calls_group_scheduled_labels.h"
#include "calls/group/ui/desktop_capture_choose_source.h"
#include "ui/platform/ui_platform_window_title.h"
#include "calls/calls_emoji_fingerprint.h"
#include "calls/calls_window.h"
#include "ui/platform/ui_platform_window_title.h" // TitleLayout
#include "ui/platform/ui_platform_utility.h"
#include "ui/controls/call_mute_button.h"
#include "ui/widgets/buttons.h"
@ -28,7 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/rp_window.h"
#include "ui/chat/group_call_bar.h"
#include "ui/controls/userpic_button.h"
#include "ui/layers/layer_manager.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
@ -47,12 +48,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_changes.h"
#include "main/session/session_show.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "base/event_filter.h"
#include "base/unixtime.h"
#include "base/qt_signal_producer.h"
#include "base/timer_rpl.h"
#include "base/power_save_blocker.h"
#include "apiwrap.h" // api().kick.
#include "api/api_chat_participants.h" // api().kick.
#include "webrtc/webrtc_environment.h"
@ -76,77 +77,6 @@ constexpr auto kControlsBackgroundOpacity = 0.8;
constexpr auto kOverrideActiveColorBgAlpha = 172;
constexpr auto kHideControlsTimeout = 5 * crl::time(1000);
class Show final : public Main::SessionShow {
public:
explicit Show(not_null<Panel*> panel);
~Show();
void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const override;
[[nodiscard]] not_null<QWidget*> toastParent() const override;
[[nodiscard]] bool valid() const override;
operator bool() const override;
[[nodiscard]] Main::Session &session() const override;
private:
const base::weak_ptr<Panel> _panel;
};
Show::Show(not_null<Panel*> panel)
: _panel(base::make_weak(panel)) {
}
Show::~Show() = default;
void Show::showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const {
using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
using ObjectBox = object_ptr<Ui::BoxContent>;
if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
if (const auto panel = _panel.get()) {
panel->showLayer(std::move(*layerWidget), options, animated);
}
} else if (auto box = std::get_if<ObjectBox>(&layer)) {
if (const auto panel = _panel.get()) {
panel->showBox(std::move(*box), options, animated);
}
} else if (const auto panel = _panel.get()) {
panel->hideLayer(animated);
}
}
not_null<QWidget*> Show::toastParent() const {
const auto panel = _panel.get();
Assert(panel != nullptr);
return panel->widget();
}
bool Show::valid() const {
return !_panel.empty();
}
Show::operator bool() const {
return valid();
}
Main::Session &Show::session() const {
const auto panel = _panel.get();
Assert(panel != nullptr);
return panel->call()->peer()->session();
}
#ifdef Q_OS_WIN
void UnpinMaximized(not_null<QWidget*> widget) {
SetWindowPos(
@ -177,22 +107,18 @@ struct Panel::ControlsBackgroundNarrow {
};
Panel::Panel(not_null<GroupCall*> call)
: Panel(call, ConferencePanelMigration()) {
}
Panel::Panel(not_null<GroupCall*> call, ConferencePanelMigration info)
: _call(call)
, _peer(call->peer())
, _layerBg(std::make_unique<Ui::LayerManager>(widget()))
#ifndef Q_OS_MAC
, _controls(Ui::Platform::SetupSeparateTitleControls(
window(),
st::groupCallTitle,
nullptr,
_controlsTop.value()))
#endif // !Q_OS_MAC
, _powerSaveBlocker(std::make_unique<base::PowerSaveBlocker>(
base::PowerSaveBlockType::PreventDisplaySleep,
u"Video chat is active"_q,
window()->windowHandle()))
, _window(info.window ? info.window : std::make_shared<Window>())
, _viewport(
std::make_unique<Viewport>(widget(), PanelMode::Wide, _window.backend()))
std::make_unique<Viewport>(
widget(),
PanelMode::Wide,
_window->backend()))
, _mute(std::make_unique<Ui::CallMuteButton>(
widget(),
st::callMuteButton,
@ -222,9 +148,6 @@ Panel::Panel(not_null<GroupCall*> call)
return result;
})
, _hideControlsTimer([=] { toggleWideControls(false); }) {
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
_layerBg->setHideByBackgroundClick(true);
_viewport->widget()->hide();
if (!_viewport->requireARGB32()) {
_call->setNotRequireARGB32();
@ -239,7 +162,7 @@ Panel::Panel(not_null<GroupCall*> call)
initWindow();
initWidget();
initControls();
initLayout();
initLayout(info);
showAndActivate();
}
@ -268,25 +191,12 @@ bool Panel::isActive() const {
return window()->isActiveWindow() && isVisible();
}
base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
const QString &text,
crl::time duration) {
return Show(this).showToast(text, duration);
std::shared_ptr<Main::SessionShow> Panel::sessionShow() {
return Main::MakeSessionShow(uiShow(), &_peer->session());
}
base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
TextWithEntities &&text,
crl::time duration) {
return Show(this).showToast(std::move(text), duration);
}
base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
Ui::Toast::Config &&config) {
return Show(this).showToast(std::move(config));
}
std::shared_ptr<Main::SessionShow> Panel::uiShow() {
return std::make_shared<Show>(this);
std::shared_ptr<Ui::Show> Panel::uiShow() {
return _window->uiShow();
}
void Panel::minimize() {
@ -367,14 +277,28 @@ void Panel::initWindow() {
window()->setAttribute(Qt::WA_NoSystemBackground);
window()->setTitleStyle(st::groupCallTitle);
if (_call->conference()) {
titleText() | rpl::start_with_next([=](const QString &text) {
window()->setTitle(text);
}, lifetime());
} else {
subscribeToPeerChanges();
}
const auto updateFullScreen = [=] {
const auto state = window()->windowState();
const auto full = (state & Qt::WindowFullScreen)
|| (state & Qt::WindowMaximized);
_rtmpFull = _call->rtmp() && full;
_fullScreenOrMaximized = full;
};
base::install_event_filter(window().get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Close && handleClose()) {
const auto type = e->type();
if (type == QEvent::Close && handleClose()) {
e->ignore();
return base::EventFilterResult::Cancel;
} else if (e->type() == QEvent::KeyPress
|| e->type() == QEvent::KeyRelease) {
} else if (_call->rtmp()
&& (type == QEvent::KeyPress || type == QEvent::KeyRelease)) {
const auto key = static_cast<QKeyEvent*>(e.get())->key();
if (key == Qt::Key_Space) {
_call->pushToTalk(
@ -384,16 +308,19 @@ void Panel::initWindow() {
&& _fullScreenOrMaximized.current()) {
toggleFullScreen();
}
} else if (e->type() == QEvent::WindowStateChange && _call->rtmp()) {
const auto state = window()->windowState();
_fullScreenOrMaximized = (state & Qt::WindowFullScreen)
|| (state & Qt::WindowMaximized);
} else if (type == QEvent::WindowStateChange) {
updateFullScreen();
}
return base::EventFilterResult::Continue;
});
}, lifetime());
updateFullScreen();
const auto guard = base::make_weak(this);
window()->setBodyTitleArea([=](QPoint widgetPoint) {
using Flag = Ui::WindowTitleHitTestFlag;
if (!guard) {
return (Flag::None | Flag(0));
}
const auto titleRect = QRect(
0,
0,
@ -409,7 +336,7 @@ void Panel::initWindow() {
if (!moveable) {
return (Flag::None | Flag(0));
}
const auto shown = _layerBg->topShownLayer();
const auto shown = _window->topShownLayer();
return (!shown || !shown->geometry().contains(widgetPoint))
? (Flag::Move | Flag::Menu | Flag::Maximize)
: Flag::None;
@ -419,6 +346,25 @@ void Panel::initWindow() {
) | rpl::start_with_next([=] {
updateMode();
}, lifetime());
_window->maximizeRequests() | rpl::start_with_next([=](bool maximized) {
if (_call->rtmp()) {
toggleFullScreen(maximized);
} else {
window()->setWindowState(maximized
? Qt::WindowMaximized
: Qt::WindowNoState);
}
}, lifetime());
_window->showingLayer() | rpl::start_with_next([=] {
hideStickedTooltip(StickedTooltipHide::Unavailable);
}, lifetime());
_window->setControlsStyle(st::groupCallTitle);
_window->togglePowerSaveBlocker(true);
uiShow()->hideLayer(anim::type::instant);
}
void Panel::initWidget() {
@ -437,7 +383,7 @@ void Panel::initWidget() {
// some geometries depends on _controls->controls.geometry,
// which is not updated here yet.
crl::on_main(widget(), [=] { updateControlsGeometry(); });
crl::on_main(this, [=] { updateControlsGeometry(); });
}, lifetime());
}
@ -446,7 +392,7 @@ void Panel::endCall() {
_call->hangup();
return;
}
showBox(Box(
uiShow()->showBox(Box(
LeaveBox,
_call,
false,
@ -476,7 +422,7 @@ void Panel::startScheduledNow() {
.confirmText = tr::lng_group_call_start_now(),
});
*box = owned.data();
showBox(std::move(owned));
uiShow()->showBox(std::move(owned));
}
}
@ -500,7 +446,9 @@ void Panel::initControls() {
const auto oldState = _call->muted();
const auto newState = (oldState == MuteState::ForceMuted)
? MuteState::RaisedHand
? (_call->conference()
? MuteState::ForceMuted
: MuteState::RaisedHand)
: (oldState == MuteState::RaisedHand)
? MuteState::RaisedHand
: (oldState == MuteState::Muted)
@ -583,10 +531,15 @@ void Panel::initControls() {
}
void Panel::toggleFullScreen() {
if (_fullScreenOrMaximized.current() || window()->isFullScreen()) {
window()->showNormal();
} else {
toggleFullScreen(
!_fullScreenOrMaximized.current() && !window()->isFullScreen());
}
void Panel::toggleFullScreen(bool fullscreen) {
if (fullscreen) {
window()->showFullScreen();
} else {
window()->showNormal();
}
}
@ -605,7 +558,7 @@ void Panel::refreshLeftButton() {
_callShare.destroy();
_settings.create(widget(), st::groupCallSettings);
_settings->setClickedCallback([=] {
showBox(Box(SettingsBox, _call));
uiShow()->showBox(Box(SettingsBox, _call));
});
trackControls(_trackControls, true);
}
@ -795,7 +748,9 @@ void Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {
: state == GroupCall::InstanceState::Disconnected
? Type::Connecting
: mute == MuteState::ForceMuted
? Type::ForceMuted
? (_call->conference()
? Type::ConferenceForceMuted
: Type::ForceMuted)
: mute == MuteState::RaisedHand
? Type::RaisedHand
: mute == MuteState::Muted
@ -890,13 +845,13 @@ void Panel::setupMembers() {
_countdown.destroy();
_startsWhen.destroy();
_members.create(widget(), _call, mode(), _window.backend());
_members.create(widget(), _call, mode(), _window->backend());
setupVideo(_viewport.get());
setupVideo(_members->viewport());
_viewport->mouseInsideValue(
) | rpl::filter([=] {
return !_fullScreenOrMaximized.current();
return !_rtmpFull;
}) | rpl::start_with_next([=](bool inside) {
toggleWideControls(inside);
}, _viewport->lifetime());
@ -914,16 +869,12 @@ void Panel::setupMembers() {
_members->toggleMuteRequests(
) | rpl::start_with_next([=](MuteRequest request) {
if (_call) {
_call->toggleMute(request);
}
}, _callLifetime);
_members->changeVolumeRequests(
) | rpl::start_with_next([=](VolumeRequest request) {
if (_call) {
_call->changeVolume(request);
}
}, _callLifetime);
_members->kickParticipantRequests(
@ -933,7 +884,9 @@ void Panel::setupMembers() {
_members->addMembersRequests(
) | rpl::start_with_next([=] {
if (!_peer->isBroadcast()
if (_call->conference()) {
addMembers();
} else if (!_peer->isBroadcast()
&& Data::CanSend(_peer, ChatRestriction::SendOther, false)
&& _call->joinAs()->isSelf()) {
addMembers();
@ -944,6 +897,9 @@ void Panel::setupMembers() {
}
}, _callLifetime);
_members->shareLinkRequests(
) | rpl::start_with_next(shareConferenceLinkCallback(), _callLifetime);
_call->videoEndpointLargeValue(
) | rpl::start_with_next([=](const VideoEndpoint &large) {
if (large && mode() != PanelMode::Wide) {
@ -953,6 +909,30 @@ void Panel::setupMembers() {
}, _callLifetime);
}
Fn<void()> Panel::shareConferenceLinkCallback() {
return [=] {
Expects(_call->conference());
ShowConferenceCallLinkBox(sessionShow(), _call->conferenceCall(), {
.st = DarkConferenceCallLinkStyle(),
});
};
}
void Panel::migrationShowShareLink() {
ShowConferenceCallLinkBox(
sessionShow(),
_call->conferenceCall(),
{ .st = DarkConferenceCallLinkStyle() });
}
void Panel::migrationInviteUsers(std::vector<InviteRequest> users) {
const auto done = [=](InviteResult result) {
uiShow()->showToast({ ComposeInviteResultToast(result) });
};
_call->inviteUsers(std::move(users), crl::guard(this, done));
}
void Panel::enlargeVideo() {
_lastSmallGeometry = window()->geometry();
@ -1035,7 +1015,7 @@ void Panel::raiseControls() {
if (_pinOnTop) {
_pinOnTop->raise();
}
_layerBg->raise();
_window->raiseLayers();
if (_niceTooltip) {
_niceTooltip->raise();
}
@ -1113,7 +1093,7 @@ void Panel::toggleWideControls(bool shown) {
return;
}
_showWideControls = shown;
crl::on_main(widget(), [=] {
crl::on_main(this, [=] {
updateWideControlsVisibility();
});
}
@ -1124,7 +1104,7 @@ void Panel::updateWideControlsVisibility() {
if (_wideControlsShown == shown) {
return;
}
_viewport->setCursorShown(!_fullScreenOrMaximized.current() || shown);
_viewport->setCursorShown(!_rtmpFull || shown);
_wideControlsShown = shown;
_wideControlsAnimation.start(
[=] { updateButtonsGeometry(); },
@ -1151,7 +1131,7 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
const auto skip = st::groupCallRecordingMarkSkip;
_recordingMark->resize(size + 2 * skip, size + 2 * skip);
_recordingMark->setClickedCallback([=] {
showToast({ (livestream
uiShow()->showToast({ (livestream
? tr::lng_group_call_is_recorded_channel
: real->recordVideo()
? tr::lng_group_call_is_recorded_video
@ -1197,7 +1177,7 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
*startedAsVideo = isVideo;
}
validateRecordingMark(recorded);
showToast((recorded
uiShow()->showToast((recorded
? (livestream
? tr::lng_group_call_recording_started_channel
: isVideo
@ -1258,7 +1238,7 @@ void Panel::createPinOnTop() {
pin ? &st::groupCallPinnedOnTop : nullptr,
pin ? &st::groupCallPinnedOnTop : nullptr);
if (!_pinOnTop->isHidden()) {
showToast({ pin
uiShow()->showToast({ pin
? tr::lng_group_call_pinned_on_top(tr::now)
: tr::lng_group_call_unpinned_on_top(tr::now) });
}
@ -1266,11 +1246,9 @@ void Panel::createPinOnTop() {
};
_fullScreenOrMaximized.value(
) | rpl::start_with_next([=](bool fullScreenOrMaximized) {
#ifndef Q_OS_MAC
_controls->controls.setStyle(fullScreenOrMaximized
_window->setControlsStyle(fullScreenOrMaximized
? st::callTitle
: st::groupCallTitle);
#endif // Q_OS_MAC
_pinOnTop->setVisible(!fullScreenOrMaximized);
if (fullScreenOrMaximized) {
@ -1360,7 +1338,7 @@ void Panel::refreshTopButton() {
void Panel::screenSharingPrivacyRequest() {
if (auto box = ScreenSharingPrivacyRequestBox()) {
showBox(std::move(box));
uiShow()->showBox(std::move(box));
}
}
@ -1411,7 +1389,7 @@ void Panel::chooseShareScreenSource() {
.confirmText = tr::lng_continue(),
});
*shared = box.data();
showBox(std::move(box));
uiShow()->showBox(std::move(box));
}
void Panel::chooseJoinAs() {
@ -1422,7 +1400,7 @@ void Panel::chooseJoinAs() {
_joinAsProcess.start(
_peer,
context,
std::make_shared<Show>(this),
uiShow(),
callback,
_call->joinAs());
}
@ -1443,7 +1421,7 @@ void Panel::showMainMenu() {
wide,
[=] { chooseJoinAs(); },
[=] { chooseShareScreenSource(); },
[=](auto box) { showBox(std::move(box)); });
[=](auto box) { uiShow()->showBox(std::move(box)); });
if (_menu->empty()) {
_wideMenuShown = false;
_menu.destroy();
@ -1505,16 +1483,25 @@ void Panel::showMainMenu() {
}
void Panel::addMembers() {
const auto &appConfig = _call->peer()->session().appConfig();
const auto conferenceLimit = appConfig.confcallSizeLimit();
if (_call->conference()
&& _call->conferenceCall()->fullCount() >= conferenceLimit) {
uiShow()->showToast({ tr::lng_group_call_invite_limit(tr::now) });
}
const auto showToastCallback = [=](TextWithEntities &&text) {
showToast(std::move(text));
uiShow()->showToast(std::move(text));
};
if (auto box = PrepareInviteBox(_call, showToastCallback)) {
showBox(std::move(box));
const auto link = _call->conference()
? shareConferenceLinkCallback()
: nullptr;
if (auto box = PrepareInviteBox(_call, showToastCallback, link)) {
uiShow()->showBox(std::move(box));
}
}
void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
showBox(Box([=](not_null<Ui::GenericBox*> box) {
uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
@ -1525,7 +1512,9 @@ void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
tr::now,
lt_channel,
participantPeer->name())
: (_peer->isBroadcast()
: (_call->conference()
? tr::lng_confcall_sure_remove
: _peer->isBroadcast()
? tr::lng_profile_sure_kick_channel
: tr::lng_profile_sure_kick)(
tr::now,
@ -1545,48 +1534,12 @@ void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
}));
}
void Panel::showBox(object_ptr<Ui::BoxContent> box) {
showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal);
}
void Panel::showBox(
object_ptr<Ui::BoxContent> box,
Ui::LayerOptions options,
anim::type animated) {
hideStickedTooltip(StickedTooltipHide::Unavailable);
if (window()->width() < st::groupCallWidth
|| window()->height() < st::groupCallWidth) {
window()->resize(
std::max(window()->width(), st::groupCallWidth),
std::max(window()->height(), st::groupCallWidth));
}
_layerBg->showBox(std::move(box), options, animated);
}
void Panel::showLayer(
std::unique_ptr<Ui::LayerWidget> layer,
Ui::LayerOptions options,
anim::type animated) {
hideStickedTooltip(StickedTooltipHide::Unavailable);
if (window()->width() < st::groupCallWidth
|| window()->height() < st::groupCallWidth) {
window()->resize(
std::max(window()->width(), st::groupCallWidth),
std::max(window()->height(), st::groupCallWidth));
}
_layerBg->showLayer(std::move(layer), options, animated);
}
void Panel::hideLayer(anim::type animated) {
_layerBg->hideAll(animated);
}
bool Panel::isLayerShown() const {
return _layerBg->topShownLayer() != nullptr;
}
void Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {
if (const auto chat = _peer->asChat()) {
if (_call->conference()) {
if (const auto user = participantPeer->asUser()) {
_call->removeConferenceParticipants({ peerToUser(user->id) });
}
} else if (const auto chat = _peer->asChat()) {
chat->session().api().chatParticipants().kick(chat, participantPeer);
} else if (const auto channel = _peer->asChannel()) {
const auto currentRestrictedRights = [&] {
@ -1606,20 +1559,19 @@ void Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {
}
}
void Panel::initLayout() {
initGeometry();
void Panel::initLayout(ConferencePanelMigration info) {
initGeometry(info);
#ifndef Q_OS_MAC
_controls->wrap.raise();
_window->raiseControls();
_controls->controls.layout().changes(
_window->controlsLayoutChanges(
) | rpl::start_with_next([=] {
// _menuToggle geometry depends on _controls arrangement.
crl::on_main(widget(), [=] { updateControlsGeometry(); });
crl::on_main(this, [=] { updateControlsGeometry(); });
}, lifetime());
raiseControls();
#endif // !Q_OS_MAC
updateControlsGeometry();
}
void Panel::showControls() {
@ -1634,10 +1586,17 @@ void Panel::closeBeforeDestroy() {
}
rpl::lifetime &Panel::lifetime() {
return window()->lifetime();
return _lifetime;
}
void Panel::initGeometry() {
void Panel::initGeometry(ConferencePanelMigration info) {
const auto minWidth = _call->rtmp()
? st::groupCallWidthRtmpMin
: st::groupCallWidth;
const auto minHeight = _call->rtmp()
? st::groupCallHeightRtmpMin
: st::groupCallHeight;
if (!info.window) {
const auto center = Core::App().getPointForCallPanelCenter();
const auto width = _call->rtmp()
? st::groupCallWidthRtmp
@ -1645,14 +1604,9 @@ void Panel::initGeometry() {
const auto height = _call->rtmp()
? st::groupCallHeightRtmp
: st::groupCallHeight;
const auto minWidth = _call->rtmp()
? st::groupCallWidthRtmpMin
: st::groupCallWidth;
const auto minHeight = _call->rtmp()
? st::groupCallHeightRtmpMin
: st::groupCallHeight;
const auto rect = QRect(0, 0, width, height);
window()->setGeometry(rect.translated(center - rect.center()));
}
window()->setMinimumSize({ minWidth, minHeight });
window()->show();
}
@ -1673,7 +1627,7 @@ QRect Panel::computeTitleRect() const {
#ifdef Q_OS_MAC
return QRect(70, 0, width - remove - 70, 28);
#else // Q_OS_MAC
const auto controls = _controls->controls.geometry();
const auto controls = _window->controlsGeometry();
const auto right = controls.x() + controls.width() + skip;
return (controls.center().x() < width / 2)
? QRect(right, 0, width - right - remove, controls.height())
@ -1835,7 +1789,7 @@ void Panel::refreshControlsBackground() {
}
void Panel::refreshTitleBackground() {
if (!_fullScreenOrMaximized.current()) {
if (!_rtmpFull) {
_titleBackground.destroy();
return;
} else if (_titleBackground) {
@ -1980,7 +1934,7 @@ void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) {
}
void Panel::trackControlOver(not_null<Ui::RpWidget*> control, bool over) {
if (_fullScreenOrMaximized.current()) {
if (_rtmpFull) {
return;
} else if (_stickedTooltipClose) {
if (!over) {
@ -2021,7 +1975,7 @@ void Panel::showStickedTooltip() {
&& callReady
&& _mute
&& !_call->mutedByAdmin()
&& !_layerBg->topShownLayer()) {
&& !_window->topShownLayer()) {
if (_stickedTooltipClose) {
// Showing already.
return;
@ -2224,10 +2178,10 @@ void Panel::updateControlsGeometry() {
const auto controlsOnTheLeft = true;
const auto controlsPadding = 0;
#else // Q_OS_MAC
const auto center = _controls->controls.geometry().center();
const auto center = _window->controlsGeometry().center();
const auto controlsOnTheLeft = center.x()
< widget()->width() / 2;
const auto controlsPadding = _controls->wrap.y();
const auto controlsPadding = _window->controlsWrapTop();
#endif // Q_OS_MAC
const auto menux = st::groupCallMenuTogglePosition.x();
const auto menuy = st::groupCallMenuTogglePosition.y();
@ -2335,7 +2289,7 @@ void Panel::updateButtonsGeometry() {
_controlsBackgroundWide->setGeometry(
rect.marginsAdded(st::groupCallControlsBackMargin));
}
if (_fullScreenOrMaximized.current()) {
if (_rtmpFull) {
refreshTitleGeometry();
}
} else {
@ -2403,10 +2357,9 @@ void Panel::updateMembersGeometry() {
_members->setVisible(!_call->rtmp());
const auto desiredHeight = _members->desiredHeight();
if (mode() == PanelMode::Wide) {
const auto full = _fullScreenOrMaximized.current();
const auto skip = full ? 0 : st::groupCallNarrowSkip;
const auto skip = _rtmpFull ? 0 : st::groupCallNarrowSkip;
const auto membersWidth = st::groupCallNarrowMembersWidth;
const auto top = full ? 0 : st::groupCallWideVideoTop;
const auto top = _rtmpFull ? 0 : st::groupCallWideVideoTop;
_members->setGeometry(
widget()->width() - skip - membersWidth,
top,
@ -2415,7 +2368,7 @@ void Panel::updateMembersGeometry() {
const auto viewportSkip = _call->rtmp()
? 0
: (skip + membersWidth);
_viewport->setGeometry(full, {
_viewport->setGeometry(_rtmpFull, {
skip,
top,
widget()->width() - viewportSkip - 2 * skip,
@ -2445,9 +2398,11 @@ void Panel::updateMembersGeometry() {
}
}
void Panel::refreshTitle() {
if (!_title) {
auto text = rpl::combine(
rpl::producer<QString> Panel::titleText() {
if (_call->conference()) {
return tr::lng_confcall_join_title();
}
return rpl::combine(
Info::Profile::NameValue(_peer),
rpl::single(
QString()
@ -2457,7 +2412,12 @@ void Panel::refreshTitle() {
}) | rpl::flatten_latest())
) | rpl::map([=](const QString &name, const QString &title) {
return title.isEmpty() ? name : title;
}) | rpl::after_next([=] {
});
}
void Panel::refreshTitle() {
if (!_title) {
auto text = titleText() | rpl::after_next([=] {
refreshTitleGeometry();
});
_title.create(
@ -2557,9 +2517,8 @@ void Panel::refreshTitleGeometry() {
? st::groupCallTitleTop
: (st::groupCallWideVideoTop
- st::groupCallTitleLabel.style.font->height) / 2;
const auto shown = _fullScreenOrMaximized.current()
? _wideControlsAnimation.value(
_wideControlsShown ? 1. : 0.)
const auto shown = _rtmpFull
? _wideControlsAnimation.value(_wideControlsShown ? 1. : 0.)
: 1.;
const auto top = anim::interpolate(
-_title->height() - st::boxRadius,
@ -2623,10 +2582,7 @@ void Panel::refreshTitleGeometry() {
} else {
layout(left + titleRect.width() - best);
}
#ifndef Q_OS_MAC
_controlsTop = anim::interpolate(-_controls->wrap.height(), 0, shown);
#endif // Q_OS_MAC
_window->setControlsShown(shown);
}
void Panel::refreshTitleColors() {
@ -2663,11 +2619,11 @@ bool Panel::handleClose() {
}
not_null<Ui::RpWindow*> Panel::window() const {
return _window.window();
return _window->window();
}
not_null<Ui::RpWidget*> Panel::widget() const {
return _window.widget();
return _window->widget();
}
} // namespace Calls::Group

View file

@ -7,32 +7,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "base/timer.h"
#include "base/flags.h"
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_choose_join_as.h"
#include "calls/group/ui/desktop_capture_choose_source.h"
#include "ui/effects/animations.h"
#include "ui/gl/gl_window.h"
#include "ui/layers/show.h"
#include "ui/rp_widget.h"
class Image;
namespace base {
class PowerSaveBlocker;
} // namespace base
namespace Data {
class PhotoMedia;
class GroupCall;
} // namespace Data
namespace Main {
class SessionShow;
} // namespace Main
namespace Ui {
class Show;
class BoxContent;
class LayerWidget;
enum class LayerOption;
@ -45,13 +39,13 @@ class CallMuteButton;
class IconButton;
class FlatLabel;
class RpWidget;
class RpWindow;
template <typename Widget>
class FadeWrap;
template <typename Widget>
class PaddingWrap;
class ScrollArea;
class GenericBox;
class LayerManager;
class GroupCallScheduledLeft;
} // namespace Ui
@ -60,19 +54,17 @@ class Instance;
struct Config;
} // namespace Ui::Toast
namespace Ui::Platform {
struct SeparateTitleControls;
} // namespace Ui::Platform
namespace Main {
class SessionShow;
} // namespace Main
namespace style {
struct CallSignalBars;
struct CallBodyLayout;
} // namespace style
namespace Calls {
struct InviteRequest;
struct ConferencePanelMigration;
class Window;
} // namespace Calls
namespace Calls::Group {
class Toasts;
@ -86,7 +78,8 @@ class Panel final
: public base::has_weak_ptr
, private Ui::DesktopCapture::ChooseSourceDelegate {
public:
Panel(not_null<GroupCall*> call);
explicit Panel(not_null<GroupCall*> call);
Panel(not_null<GroupCall*> call, ConferencePanelMigration info);
~Panel();
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
@ -94,34 +87,20 @@ public:
[[nodiscard]] bool isVisible() const;
[[nodiscard]] bool isActive() const;
base::weak_ptr<Ui::Toast::Instance> showToast(
const QString &text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
TextWithEntities &&text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
Ui::Toast::Config &&config);
void showBox(object_ptr<Ui::BoxContent> box);
void showBox(
object_ptr<Ui::BoxContent> box,
Ui::LayerOptions options,
anim::type animated = anim::type::normal);
void showLayer(
std::unique_ptr<Ui::LayerWidget> layer,
Ui::LayerOptions options,
anim::type animated = anim::type::normal);
void hideLayer(anim::type animated = anim::type::normal);
[[nodiscard]] bool isLayerShown() const;
void migrationShowShareLink();
void migrationInviteUsers(std::vector<InviteRequest> users);
void minimize();
void toggleFullScreen();
void toggleFullScreen(bool fullscreen);
void close();
void showAndActivate();
void closeBeforeDestroy();
[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();
[[nodiscard]] std::shared_ptr<Main::SessionShow> sessionShow();
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
rpl::lifetime &lifetime();
@ -139,8 +118,6 @@ private:
Discarded,
};
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
[[nodiscard]] PanelMode mode() const;
void paint(QRect clip);
@ -149,12 +126,13 @@ private:
void initWidget();
void initControls();
void initShareAction();
void initLayout();
void initGeometry();
void initLayout(ConferencePanelMigration info);
void initGeometry(ConferencePanelMigration info);
void setupScheduledLabels(rpl::producer<TimeId> date);
void setupMembers();
void setupVideo(not_null<Viewport*> viewport);
void setupRealMuteButtonState(not_null<Data::GroupCall*> real);
[[nodiscard]] rpl::producer<QString> titleText();
bool handleClose();
void startScheduledNow();
@ -192,6 +170,7 @@ private:
void toggleWideControls(bool shown);
void updateWideControlsVisibility();
[[nodiscard]] bool videoButtonInNarrowMode() const;
[[nodiscard]] Fn<void()> shareConferenceLinkCallback();
void endCall();
@ -225,18 +204,11 @@ private:
const not_null<GroupCall*> _call;
not_null<PeerData*> _peer;
Ui::GL::Window _window;
const std::unique_ptr<Ui::LayerManager> _layerBg;
std::shared_ptr<Window> _window;
rpl::variable<PanelMode> _mode;
rpl::variable<bool> _fullScreenOrMaximized = false;
bool _unpinnedMaximized = false;
#ifndef Q_OS_MAC
rpl::variable<int> _controlsTop = 0;
const std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
#endif // !Q_OS_MAC
const std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
bool _rtmpFull = false;
rpl::lifetime _callLifetime;
@ -293,6 +265,7 @@ private:
rpl::lifetime _hideControlsTimerLifetime;
rpl::lifetime _peerLifetime;
rpl::lifetime _lifetime;
};

View file

@ -766,7 +766,7 @@ void SettingsBox(
}, volumeItem->lifetime());
}
if (peer->canManageGroupCall()) {
if (call->canManage()) {
layout->add(object_ptr<Ui::SettingsButton>(
layout,
(peer->isBroadcast()

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/group/calls_group_panel.h"
#include "data/data_peer.h"
#include "data/data_group_call.h"
#include "ui/layers/show.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "lang/lang_keys.h"
@ -49,7 +50,7 @@ void Toasts::setupJoinAsChanged() {
return (state == State::Joined);
}) | rpl::take(1);
}) | rpl::flatten_latest() | rpl::start_with_next([=] {
_panel->showToast((_call->peer()->isBroadcast()
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
? tr::lng_group_call_join_as_changed_channel
: tr::lng_group_call_join_as_changed)(
tr::now,
@ -69,7 +70,7 @@ void Toasts::setupTitleChanged() {
? peer->name()
: peer->groupCall()->title();
}) | rpl::start_with_next([=](const QString &title) {
_panel->showToast((_call->peer()->isBroadcast()
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
? tr::lng_group_call_title_changed_channel
: tr::lng_group_call_title_changed)(
tr::now,
@ -83,7 +84,8 @@ void Toasts::setupAllowedToSpeak() {
_call->allowedToSpeakNotifications(
) | rpl::start_with_next([=] {
if (_panel->isActive()) {
_panel->showToast(tr::lng_group_call_can_speak_here(tr::now));
_panel->uiShow()->showToast(
tr::lng_group_call_can_speak_here(tr::now));
} else {
const auto real = _call->lookupReal();
const auto name = (real && !real->title().isEmpty())
@ -137,7 +139,7 @@ void Toasts::setupPinnedVideo() {
: tr::lng_group_call_unpinned_screen);
return key(tr::now, lt_user, peer->shortName());
}();
_panel->showToast(text);
_panel->uiShow()->showToast(text);
}, _lifetime);
}
@ -146,7 +148,7 @@ void Toasts::setupRequestedToSpeak() {
) | rpl::combine_previous(
) | rpl::start_with_next([=](MuteState was, MuteState now) {
if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {
_panel->showToast(
_panel->uiShow()->showToast(
tr::lng_group_call_tooltip_raised_hand(tr::now));
}
}, _lifetime);
@ -173,7 +175,7 @@ void Toasts::setupError() {
}
Unexpected("Error in Calls::Group::Toasts::setupErrorToasts.");
}();
_panel->showToast({ key(tr::now) }, kErrorDuration);
_panel->uiShow()->showToast({ key(tr::now) }, kErrorDuration);
}, _lifetime);
}

View file

@ -2108,7 +2108,7 @@ void EmojiListWidget::colorChosen(EmojiChosen data) {
const auto emoji = data.emoji;
auto &settings = Core::App().settings();
if (const auto button = std::get_if<OverButton>(&_pickerSelected)) {
if (v::is<OverButton>(_pickerSelected)) {
settings.saveAllEmojiVariants(emoji);
for (auto section = int(Section::People)
; section < _staticCount
@ -2433,7 +2433,7 @@ Ui::Text::CustomEmoji *EmojiListWidget::resolveCustomRecent(
const auto &data = customId.data;
if (const auto document = std::get_if<RecentEmojiDocument>(&data)) {
return resolveCustomRecent(document->id);
} else if (const auto emoji = std::get_if<EmojiPtr>(&data)) {
} else if (v::is<EmojiPtr>(data)) {
return nullptr;
}
Unexpected("Custom recent emoji id.");

View file

@ -765,7 +765,7 @@ InlineBotQuery ParseInlineBotQuery(
result.username = username.toString();
if (const auto peer = session->data().peerByUsername(result.username)) {
if (const auto user = peer->asUser()) {
result.bot = peer->asUser();
result.bot = user;
} else {
result.bot = nullptr;
}

View file

@ -85,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qthelp_url.h"
#include "boxes/premium_limits_box.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/location_picker.h"
#include "styles/style_window.h"
#include <QtCore/QStandardPaths>
@ -329,6 +330,8 @@ void Application::run() {
// Check now to avoid re-entrance later.
[[maybe_unused]] const auto ivSupported = Iv::ShowButton();
[[maybe_unused]] const auto lpAvailable = Ui::LocationPicker::Available(
{});
_windows.emplace(nullptr, std::make_unique<Window::Controller>());
setLastActiveWindow(_windows.front().second.get());

View file

@ -0,0 +1,251 @@
/*
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 "core/bank_card_click_handler.h"
#include "core/click_handler_types.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mtproto/sender.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/menu/menu_multiline_action.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h" // popupMenuExpandedSeparator.
#include "styles/style_menu_icons.h"
namespace {
struct State final {
State(not_null<Main::Session*> session) : sender(&session->mtp()) {
}
MTP::Sender sender;
};
struct BankCardData final {
QString title;
std::vector<EntityLinkData> links;
};
enum class Status {
Loading,
Resolved,
Failed,
};
void RequestResolveBankCard(
not_null<State*> state,
const QString &bankCard,
Fn<void(BankCardData)> done,
Fn<void(QString)> fail) {
state->sender.request(MTPpayments_GetBankCardData(
MTP_string(bankCard)
)).done([=](const MTPpayments_BankCardData &result) {
auto bankCardData = BankCardData{
.title = qs(result.data().vtitle()),
};
for (const auto &tl : result.data().vopen_urls().v) {
const auto url = qs(tl.data().vurl());
const auto name = qs(tl.data().vname());
bankCardData.links.emplace_back(EntityLinkData{
.text = name,
.data = url,
});
}
done(std::move(bankCardData));
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
class ResolveBankCardAction final : public Ui::Menu::ItemBase {
public:
ResolveBankCardAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st);
void setStatus(Status status);
bool isEnabled() const override;
not_null<QAction*> action() const override;
protected:
int contentHeight() const override;
void paintEvent(QPaintEvent *e) override;
private:
void paint(Painter &p);
const not_null<QAction*> _dummyAction;
const style::Menu &_st;
const int _height = 0;
Status _status = Status::Loading;
Ui::Text::String _text;
};
ResolveBankCardAction::ResolveBankCardAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st)
: ItemBase(parent, st)
, _dummyAction(Ui::CreateChild<QAction>(parent))
, _st(st)
, _height(st::groupCallJoinAsPhotoSize) {
setAcceptBoth(true);
initResizeHook(parent->sizeValue());
setStatus(Status::Loading);
}
void ResolveBankCardAction::setStatus(Status status) {
_status = status;
if (status == Status::Resolved) {
resize(width(), 0);
} else if (status == Status::Failed) {
_text.setText(_st.itemStyle, tr::lng_attach_failed(tr::now));
} else if (status == Status::Loading) {
_text.setText(_st.itemStyle, tr::lng_contacts_loading(tr::now));
}
update();
}
void ResolveBankCardAction::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto selected = false;
const auto height = contentHeight();
if (selected && _st.itemBgOver->c.alpha() < 255) {
p.fillRect(0, 0, width(), height, _st.itemBg);
}
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
const auto &padding = st::groupCallJoinAsPadding;
{
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
const auto w = width() - padding.left() - padding.right();
_text.draw(p, Ui::Text::PaintContext{
.position = QPoint(
(width() - w) / 2,
(height - _text.countHeight(w)) / 2),
.outerWidth = w,
.availableWidth = w,
.align = style::al_center,
.elisionLines = 2,
});
}
}
bool ResolveBankCardAction::isEnabled() const {
return false;
}
not_null<QAction*> ResolveBankCardAction::action() const {
return _dummyAction;
}
int ResolveBankCardAction::contentHeight() const {
if (_status == Status::Resolved) {
return 0;
}
return _height;
}
} // namespace
BankCardClickHandler::BankCardClickHandler(
not_null<Main::Session*> session,
QString text)
: _session(session)
, _text(text) {
}
void BankCardClickHandler::onClick(ClickContext context) const {
if (context.button != Qt::LeftButton) {
return;
}
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
const auto pos = QCursor::pos();
if (!controller) {
return;
}
const auto menu = Ui::CreateChild<Ui::PopupMenu>(
controller->content(),
st::popupMenuWithIcons);
const auto bankCard = _text;
const auto copy = [bankCard, show = controller->uiShow()] {
TextUtilities::SetClipboardText(
TextForMimeData::Simple(bankCard));
show->showToast(tr::lng_context_bank_card_copied(tr::now));
};
menu->addAction(
tr::lng_context_bank_card_copy(tr::now),
copy,
&st::menuIconCopy);
auto resolveBankCardAction = base::make_unique_q<ResolveBankCardAction>(
menu,
menu->st().menu);
const auto resolveBankCardRaw = resolveBankCardAction.get();
menu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator);
menu->addAction(std::move(resolveBankCardAction));
const auto addTitle = [=](const QString &name) {
auto button = base::make_unique_q<Ui::Menu::MultilineAction>(
menu,
menu->st().menu,
st::historyHasCustomEmoji,
st::historyBankCardMenuMultilinePosition,
TextWithEntities{ name });
button->setClickedCallback(copy);
menu->addAction(std::move(button));
};
const auto state = menu->lifetime().make_state<State>(
&controller->session());
RequestResolveBankCard(
state,
bankCard,
[=](BankCardData data) {
resolveBankCardRaw->setStatus(Status::Resolved);
for (auto &link : data.links) {
menu->addAction(
base::take(link.text),
[u = base::take(link.data)] { UrlClickHandler::Open(u); },
&st::menuIconPayment);
}
if (!data.title.isEmpty()) {
addTitle(base::take(data.title));
}
},
[=](const QString &) {
resolveBankCardRaw->setStatus(Status::Failed);
});
menu->popup(pos);
}
auto BankCardClickHandler::getTextEntity() const -> TextEntity {
return { EntityType::BankCard };
}
QString BankCardClickHandler::tooltip() const {
return _text;
}

View file

@ -0,0 +1,30 @@
/*
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/basic_click_handlers.h"
namespace Main {
class Session;
} // namespace Main
class BankCardClickHandler : public ClickHandler {
public:
BankCardClickHandler(not_null<Main::Session*> session, QString text);
void onClick(ClickContext context) const override;
TextEntity getTextEntity() const override;
QString tooltip() const override;
private:
const not_null<Main::Session*> _session;
QString _text;
};

View file

@ -315,6 +315,8 @@ CloudPasswordState ParseCloudPasswordState(
ParseSecureSecretAlgo(data.vnew_secure_algo()));
result.unconfirmedPattern = qs(
data.vemail_unconfirmed_pattern().value_or_empty());
result.loginEmailPattern = qs(
data.vlogin_email_pattern().value_or_empty());
result.pendingResetDate = data.vpending_reset_date().value_or_empty();
result.outdatedClient = [&] {

View file

@ -135,6 +135,7 @@ struct CloudPasswordState {
bool outdatedClient = false;
QString hint;
QString unconfirmedPattern;
QString loginEmailPattern;
TimeId pendingResetDate = 0;
};

View file

@ -332,7 +332,7 @@ QString ImagesOrAllFilter() {
}
QString PhotoVideoFilesFilter() {
return u"Image and Video Files (*.png *.jpg *.jpeg *.mp4 *.mov *.m4v);;"_q
return u"Image and Video Files (*"_q + Ui::ImageExtensions().join(u" *"_q) + u" *.m4v);;"_q
+ AllFilesFilter();
}

View file

@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_form_controller.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "data/components/credits.h"
#include "data/data_birthday.h"
#include "data/data_channel.h"
@ -940,10 +941,41 @@ bool ShowEditBirthday(
: (u"Error: "_q + error.type()));
})).handleFloodErrors().send();
};
controller->show(Box(
Ui::EditBirthdayBox,
user->birthday(),
save));
if (match->captured(1).isEmpty()) {
controller->show(Box(Ui::EditBirthdayBox, user->birthday(), save));
} else {
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
Ui::EditBirthdayBox(box, user->birthday(), save);
const auto container = box->verticalLayout();
const auto session = &user->session();
const auto key = Api::UserPrivacy::Key::Birthday;
session->api().userPrivacy().reload(key);
auto isExactlyContacts = session->api().userPrivacy().value(
key
) | rpl::map([=](const Api::UserPrivacy::Rule &value) {
return (value.option == Api::UserPrivacy::Option::Contacts)
&& value.always.peers.empty()
&& !value.always.premiums
&& value.never.peers.empty();
}) | rpl::distinct_until_changed();
Ui::AddSkip(container);
const auto link = u"internal:edit_privacy_birthday:from_box"_q;
Ui::AddDividerText(container, rpl::conditional(
std::move(isExactlyContacts),
tr::lng_settings_birthday_contacts(
lt_link,
tr::lng_settings_birthday_contacts_link(
) | Ui::Text::ToLink(link),
Ui::Text::WithEntities),
tr::lng_settings_birthday_about(
lt_link,
tr::lng_settings_birthday_about_link(
) | Ui::Text::ToLink(link),
Ui::Text::WithEntities)));
}));
}
return true;
}
@ -954,11 +986,29 @@ bool ShowEditBirthdayPrivacy(
if (!controller) {
return false;
}
const auto isFromBox = !match->captured(1).isEmpty();
auto syncLifetime = controller->session().api().userPrivacy().value(
Api::UserPrivacy::Key::Birthday
) | rpl::take(
1
) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) {
if (isFromBox) {
using namespace ::Settings;
class Controller final : public BirthdayPrivacyController {
object_ptr<Ui::RpWidget> setupAboveWidget(
not_null<Window::SessionController*> controller,
not_null<QWidget*> parent,
rpl::producer<Option> optionValue,
not_null<QWidget*> outerContainer) override {
return { nullptr };
}
};
controller->show(Box<EditPrivacyBox>(
controller,
std::make_unique<Controller>(),
value));
return;
}
controller->show(Box<EditPrivacyBox>(
controller,
std::make_unique<::Settings::BirthdayPrivacyController>(),
@ -1452,6 +1502,35 @@ bool ResolveUniqueGift(
return true;
}
bool ResolveConferenceCall(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto slug = match->captured(1);
if (slug.isEmpty()) {
return false;
}
const auto myContext = context.value<ClickHandlerContext>();
controller->window().activate();
controller->resolveConferenceCall(match->captured(1), myContext.itemId);
return true;
}
bool ResolveStarsSettings(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
controller->showSettings(::Settings::CreditsId());
controller->window().activate();
return true;
}
} // namespace
const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
@ -1548,6 +1627,14 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
u"^nft/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
ResolveUniqueGift
},
{
u"^call/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
ResolveConferenceCall
},
{
u"^stars/?(^\\?.*)?(#|$)"_q,
ResolveStarsSettings
},
{
u"^user\\?(.+)(#|$)"_q,
AyuUrlHandlers::ResolveUser
@ -1587,11 +1674,11 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
ShowSearchTagsPromo
},
{
u"^edit_birthday$"_q,
u"^edit_birthday(.*)$"_q,
ShowEditBirthday,
},
{
u"^edit_privacy_birthday$"_q,
u"^edit_privacy_birthday(.*)$"_q,
ShowEditBirthdayPrivacy,
},
{
@ -1716,6 +1803,9 @@ QString TryConvertUrlToLocal(QString url) {
} else if (const auto nftMatch = regex_match(u"^nft/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
const auto slug = nftMatch->captured(1);
return u"tg://nft?slug="_q + slug;
} else if (const auto callMatch = regex_match(u"^call/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
const auto slug = callMatch->captured(1);
return u"tg://call?slug="_q + slug;
} else if (const auto privateMatch = regex_match(u"^"
"c/(\\-?\\d+)"
"("

View file

@ -326,7 +326,7 @@ bool NameTypeAllowsThumbnail(NameType type) {
bool IsIpRevealingPath(const QString &filepath) {
static const auto kExtensions = [] {
const auto joined = u"htm html svg m4v m3u8 xhtml"_q;
const auto joined = u"htm html svg m4v m3u m3u8 xhtml xml"_q;
const auto list = joined.split(' ');
return base::flat_set<QString>(list.begin(), list.end());
}();

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/local_url_handlers.h"
#include "core/file_utilities.h"
#include "core/application.h"
#include "core/bank_card_click_handler.h"
#include "core/sandbox.h"
#include "core/click_handler_types.h"
#include "data/stickers/data_custom_emoji.h"
@ -260,6 +261,10 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return my->session
? std::make_shared<PhoneClickHandler>(my->session, data.text)
: nullptr;
case EntityType::BankCard:
return my->session
? std::make_shared<BankCardClickHandler>(my->session, data.text)
: nullptr;
}
return Integration::createLinkHandler(data, context);
}

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5013001;
constexpr auto AppVersionStr = "5.13.1";
constexpr auto AppVersion = 5014001;
constexpr auto AppVersionStr = "5.14.1";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -25,7 +25,7 @@ Ui::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) {
for (auto i = begin(_pickers); i != end(_pickers);) {
if (const auto strong = i->picker.get()) {
if (i->action == action) {
return i->picker.get();
return strong;
}
++i;
} else {

View file

@ -956,7 +956,7 @@ QString ChannelData::invitePeekHash() const {
}
void ChannelData::privateErrorReceived() {
if (const auto expires = invitePeekExpires()) {
if (invitePeekExpires()) {
const auto hash = invitePeekHash();
for (const auto &window : session().windows()) {
clearInvitePeek();
@ -1001,10 +1001,13 @@ void ChannelData::setGroupCall(
data.vid().v,
data.vaccess_hash().v,
scheduleDate,
rtmp);
rtmp,
false); // conference
owner().registerGroupCall(_call.get());
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
addFlags(Flag::CallActive);
}, [&](const auto &) {
clearGroupCall();
});
}

View file

@ -234,10 +234,13 @@ void ChatData::setGroupCall(
data.vid().v,
data.vaccess_hash().v,
scheduleDate,
rtmp);
rtmp,
false); // conference
owner().registerGroupCall(_call.get());
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
addFlags(Flag::CallActive);
}, [&](const auto &) {
clearGroupCall();
});
}

View file

@ -351,7 +351,7 @@ bool ChatFilter::contains(
: user->isContact()
? Flag::Contacts
: Flag::NonContacts;
} else if (const auto chat = peer->asChat()) {
} else if (peer->isChat()) {
return Flag::Groups;
} else if (const auto channel = peer->asChannel()) {
if (channel->isBroadcast()) {

View file

@ -238,7 +238,7 @@ void CloudThemes::showPreview(
void CloudThemes::showPreview(
not_null<Window::Controller*> controller,
const CloudTheme &cloud) {
if (const auto documentId = cloud.documentId) {
if (cloud.documentId) {
previewFromDocument(controller, cloud);
} else if (cloud.createdBy == _session->userId()) {
controller->show(Box(

View file

@ -60,9 +60,10 @@ bool GroupCallParticipant::screenPaused() const {
GroupCall::GroupCall(
not_null<PeerData*> peer,
CallId id,
CallId accessHash,
uint64 accessHash,
TimeId scheduleDate,
bool rtmp)
bool rtmp,
bool conference)
: _id(id)
, _accessHash(accessHash)
, _peer(peer)
@ -70,15 +71,50 @@ GroupCall::GroupCall(
, _speakingByActiveFinishTimer([=] { checkFinishSpeakingByActive(); })
, _scheduleDate(scheduleDate)
, _rtmp(rtmp)
, _conference(conference)
, _listenersHidden(rtmp) {
if (_conference) {
session().data().registerGroupCall(this);
_participantUpdates.events(
) | rpl::filter([=](const ParticipantUpdate &update) {
return !update.now
&& !update.was->peer->isSelf()
&& !_participantsWithAccess.current().empty();
}) | rpl::start_with_next([=](const ParticipantUpdate &update) {
if (const auto id = peerToUser(update.was->peer->id)) {
if (_participantsWithAccess.current().contains(id)) {
_staleParticipantIds.fire({ id });
}
}
}, _checkStaleLifetime);
_participantsWithAccess.changes(
) | rpl::filter([=](const base::flat_set<UserId> &list) {
return !list.empty();
}) | rpl::start_with_next([=] {
if (_allParticipantsLoaded) {
checkStaleParticipants();
} else {
requestParticipants();
}
}, _checkStaleLifetime);
}
}
GroupCall::~GroupCall() {
if (_conference) {
session().data().unregisterGroupCall(this);
}
api().request(_unknownParticipantPeersRequestId).cancel();
api().request(_participantsRequestId).cancel();
api().request(_reloadRequestId).cancel();
}
Main::Session &GroupCall::session() const {
return _peer->session();
}
CallId GroupCall::id() const {
return _id;
}
@ -91,10 +127,18 @@ bool GroupCall::rtmp() const {
return _rtmp;
}
bool GroupCall::canManage() const {
return _conference ? _creator : _peer->canManageGroupCall();
}
bool GroupCall::listenersHidden() const {
return _listenersHidden;
}
bool GroupCall::blockchainMayBeEmpty() const {
return _version < 2;
}
not_null<PeerData*> GroupCall::peer() const {
return _peer;
}
@ -146,7 +190,7 @@ void GroupCall::requestParticipants() {
: ApplySliceSource::SliceLoaded));
setServerParticipantsCount(data.vcount().v);
if (data.vparticipants().v.isEmpty()) {
_allParticipantsLoaded = true;
setParticipantsLoaded();
}
finishParticipantsSliceRequest();
if (reloaded) {
@ -157,7 +201,7 @@ void GroupCall::requestParticipants() {
_participantsRequestId = 0;
const auto reloaded = processSavedFullCall();
setServerParticipantsCount(_participants.size());
_allParticipantsLoaded = true;
setParticipantsLoaded();
finishParticipantsSliceRequest();
if (reloaded) {
_participantsReloaded.fire({});
@ -165,6 +209,36 @@ void GroupCall::requestParticipants() {
}).send();
}
void GroupCall::setParticipantsLoaded() {
_allParticipantsLoaded = true;
checkStaleParticipants();
}
void GroupCall::checkStaleParticipants() {
const auto &list = _participantsWithAccess.current();
if (list.empty()) {
return;
}
auto existing = base::flat_set<UserId>();
existing.reserve(_participants.size() + 1);
existing.emplace(session().userId());
for (const auto &participant : _participants) {
if (const auto id = peerToUser(participant.peer->id)) {
existing.emplace(id);
}
}
auto stale = base::flat_set<UserId>();
for (const auto &id : list) {
if (!existing.contains(id)) {
stale.reserve(list.size());
stale.emplace(id);
}
}
if (!stale.empty()) {
_staleParticipantIds.fire(std::move(stale));
}
}
bool GroupCall::processSavedFullCall() {
if (!_savedFull) {
return false;
@ -225,6 +299,10 @@ rpl::producer<int> GroupCall::fullCountValue() const {
return _fullCount.value();
}
QString GroupCall::conferenceInviteLink() const {
return _conferenceInviteLink;
}
bool GroupCall::participantsLoaded() const {
return _allParticipantsLoaded;
}
@ -275,7 +353,32 @@ auto GroupCall::participantSpeaking() const
return _participantSpeaking.events();
}
void GroupCall::setParticipantsWithAccess(base::flat_set<UserId> list) {
_participantsWithAccess = std::move(list);
if (_allParticipantsLoaded) {
checkStaleParticipants();
} else {
requestParticipants();
}
}
auto GroupCall::participantsWithAccessCurrent() const
-> const base::flat_set<UserId> & {
return _participantsWithAccess.current();
}
auto GroupCall::participantsWithAccessValue() const
-> rpl::producer<base::flat_set<UserId>> {
return _participantsWithAccess.value();
}
auto GroupCall::staleParticipantIds() const
-> rpl::producer<base::flat_set<UserId>> {
return _staleParticipantIds.events();
}
void GroupCall::enqueueUpdate(const MTPUpdate &update) {
const auto initial = !_version;
update.match([&](const MTPDupdateGroupCall &updateData) {
updateData.vcall().match([&](const MTPDgroupCall &data) {
const auto version = data.vversion().v;
@ -329,7 +432,7 @@ void GroupCall::enqueueUpdate(const MTPUpdate &update) {
}, [](const auto &) {
Unexpected("Type in GroupCall::enqueueUpdate.");
});
processQueuedUpdates();
processQueuedUpdates(initial);
}
void GroupCall::discard(const MTPDgroupCallDiscarded &data) {
@ -404,6 +507,7 @@ void GroupCall::applyCallFields(const MTPDgroupCall &data) {
_version = 1;
}
_rtmp = data.is_rtmp_stream();
_creator = data.is_creator();
_listenersHidden = data.is_listeners_hidden();
_joinMuted = data.is_join_muted();
_canChangeJoinMuted = data.is_can_change_join_muted();
@ -420,6 +524,7 @@ void GroupCall::applyCallFields(const MTPDgroupCall &data) {
_unmutedVideoLimit = data.vunmuted_video_limit().v;
_allParticipantsLoaded
= (_serverParticipantsCount == _participants.size());
_conferenceInviteLink = qs(data.vinvite_link().value_or_empty());
}
void GroupCall::applyLocalUpdate(
@ -459,12 +564,10 @@ void GroupCall::applyEnqueuedUpdate(const MTPUpdate &update) {
}, [](const auto &) {
Unexpected("Type in GroupCall::applyEnqueuedUpdate.");
});
Core::App().calls().applyGroupCallUpdateChecked(
&_peer->session(),
update);
Core::App().calls().applyGroupCallUpdateChecked(&session(), update);
}
void GroupCall::processQueuedUpdates() {
void GroupCall::processQueuedUpdates(bool initial) {
if (!_version || _applyingQueuedUpdates) {
return;
}
@ -476,7 +579,13 @@ void GroupCall::processQueuedUpdates() {
const auto type = entry.first.second;
const auto incremented = (type == QueuedType::VersionedParticipant);
if ((version < _version)
|| (version == _version && incremented)) {
|| (version == _version && incremented && !initial)) {
// There is a case for a new conference call we receive:
// - updateGroupCall, version = 2
// - updateGroupCallParticipants, version = 2, versioned
// In case we were joining together with creation,
// in that case we don't want to skip the participants update,
// so we pass the `initial` flag specifically for that case.
_queuedUpdates.erase(_queuedUpdates.begin());
} else if (version == _version
|| (version == _version + 1 && incremented)) {
@ -651,7 +760,8 @@ void GroupCall::applyParticipantsSlice(
.videoJoined = videoJoined,
.applyVolumeFromMin = applyVolumeFromMin,
};
if (i == end(_participants)) {
const auto adding = (i == end(_participants));
if (adding) {
if (value.ssrc) {
_participantPeerByAudioSsrc.emplace(
value.ssrc,
@ -664,9 +774,6 @@ void GroupCall::applyParticipantsSlice(
participantPeer);
}
_participants.push_back(value);
if (const auto user = participantPeer->asUser()) {
_peer->owner().unregisterInvitedToCallUser(_id, user);
}
} else {
if (i->ssrc != value.ssrc) {
_participantPeerByAudioSsrc.erase(i->ssrc);
@ -698,6 +805,14 @@ void GroupCall::applyParticipantsSlice(
.now = value,
});
}
if (adding) {
if (const auto user = participantPeer->asUser()) {
_peer->owner().unregisterInvitedToCallUser(
_id,
user,
false);
}
}
});
}
if (sliceSource == ApplySliceSource::UpdateReceived) {
@ -984,7 +1099,7 @@ bool GroupCall::joinedToTop() const {
}
ApiWrap &GroupCall::api() const {
return _peer->session().api();
return session().api();
}
} // namespace Data

View file

@ -17,6 +17,15 @@ namespace Calls {
struct ParticipantVideoParams;
} // namespace Calls
namespace Main {
class Session;
} // namespace Main
namespace TdE2E {
struct ParticipantState;
struct UserId;
} // namespace TdE2E
namespace Data {
[[nodiscard]] const std::string &RtmpEndpointId();
@ -56,15 +65,20 @@ public:
GroupCall(
not_null<PeerData*> peer,
CallId id,
CallId accessHash,
uint64 accessHash,
TimeId scheduleDate,
bool rtmp);
bool rtmp,
bool conference);
~GroupCall();
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] CallId id() const;
[[nodiscard]] bool loaded() const;
[[nodiscard]] bool rtmp() const;
[[nodiscard]] bool canManage() const;
[[nodiscard]] bool listenersHidden() const;
[[nodiscard]] bool blockchainMayBeEmpty() const;
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] MTPInputGroupCall input() const;
[[nodiscard]] QString title() const {
@ -133,6 +147,16 @@ public:
[[nodiscard]] auto participantSpeaking() const
-> rpl::producer<not_null<Participant*>>;
void setParticipantsWithAccess(base::flat_set<UserId> list);
[[nodiscard]] auto participantsWithAccessCurrent() const
-> const base::flat_set<UserId> &;
[[nodiscard]] auto participantsWithAccessValue() const
-> rpl::producer<base::flat_set<UserId>>;
[[nodiscard]] auto staleParticipantIds() const
-> rpl::producer<base::flat_set<UserId>>;
void setParticipantsLoaded();
void checkStaleParticipants();
void enqueueUpdate(const MTPUpdate &update);
void applyLocalUpdate(
const MTPDupdateGroupCallParticipants &update);
@ -153,6 +177,7 @@ public:
[[nodiscard]] int fullCount() const;
[[nodiscard]] rpl::producer<int> fullCountValue() const;
[[nodiscard]] QString conferenceInviteLink() const;
void setInCall();
void reload();
@ -191,7 +216,7 @@ private:
void applyEnqueuedUpdate(const MTPUpdate &update);
void setServerParticipantsCount(int count);
void computeParticipantsCount();
void processQueuedUpdates();
void processQueuedUpdates(bool initial = false);
void processFullCallUsersChats(const MTPphone_GroupCall &call);
void processFullCallFields(const MTPphone_GroupCall &call);
[[nodiscard]] bool requestParticipantsAfterReload(
@ -201,7 +226,7 @@ private:
[[nodiscard]] Participant *findParticipant(not_null<PeerData*> peer);
const CallId _id = 0;
const CallId _accessHash = 0;
const uint64 _accessHash = 0;
not_null<PeerData*> _peer;
int _version = 0;
@ -209,6 +234,7 @@ private:
mtpRequestId _reloadRequestId = 0;
crl::time _reloadLastFinished = 0;
rpl::variable<QString> _title;
QString _conferenceInviteLink;
base::flat_multi_map<
std::pair<int, QueuedType>,
@ -241,13 +267,19 @@ private:
rpl::event_stream<not_null<Participant*>> _participantSpeaking;
rpl::event_stream<> _participantsReloaded;
bool _joinMuted = false;
bool _canChangeJoinMuted = true;
bool _allParticipantsLoaded = false;
bool _joinedToTop = false;
bool _applyingQueuedUpdates = false;
bool _rtmp = false;
bool _listenersHidden = false;
rpl::variable<base::flat_set<UserId>> _participantsWithAccess;
rpl::event_stream<base::flat_set<UserId>> _staleParticipantIds;
rpl::lifetime _checkStaleLifetime;
bool _creator : 1 = false;
bool _joinMuted : 1 = false;
bool _canChangeJoinMuted : 1 = true;
bool _allParticipantsLoaded : 1 = false;
bool _joinedToTop : 1 = false;
bool _applyingQueuedUpdates : 1 = false;
bool _rtmp : 1 = false;
bool _conference : 1 = false;
bool _listenersHidden : 1 = false;
};

View file

@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "calls/calls_instance.h"
#include "core/application.h"
#include "core/click_handler_types.h" // ClickHandlerContext
#include "lang/lang_keys.h"
@ -455,30 +456,57 @@ Invoice ComputeInvoiceData(
return result;
}
Call ComputeCallData(const MTPDmessageActionPhoneCall &call) {
Call ComputeCallData(
not_null<Session*> owner,
const MTPDmessageActionPhoneCall &call) {
auto result = Call();
result.finishReason = [&] {
result.state = [&] {
if (const auto reason = call.vreason()) {
return reason->match([](const MTPDphoneCallDiscardReasonBusy &) {
return CallFinishReason::Busy;
return CallState::Busy;
}, [](const MTPDphoneCallDiscardReasonDisconnect &) {
return CallFinishReason::Disconnected;
return CallState::Disconnected;
}, [](const MTPDphoneCallDiscardReasonHangup &) {
return CallFinishReason::Hangup;
return CallState::Hangup;
}, [](const MTPDphoneCallDiscardReasonMissed &) {
return CallFinishReason::Missed;
}, [](const MTPDphoneCallDiscardReasonAllowGroupCall &) {
return CallFinishReason::AllowGroupCall;
return CallState::Missed;
}, [](const MTPDphoneCallDiscardReasonMigrateConferenceCall &) {
return CallState::MigrateConferenceCall;
});
Unexpected("Call reason type.");
}
return CallFinishReason::Hangup;
return CallState::Hangup;
}();
result.duration = call.vduration().value_or_empty();
result.video = call.is_video();
return result;
}
Call ComputeCallData(
not_null<Session*> owner,
const MTPDmessageActionConferenceCall &call) {
auto participants = std::vector<not_null<PeerData*>>();
if (const auto list = call.vother_participants()) {
participants.reserve(list->v.size());
for (const auto &participant : list->v) {
participants.push_back(owner->peer(peerFromMTP(participant)));
}
}
return {
.otherParticipants = std::move(participants),
.conferenceId = call.vcall_id().v,
.duration = call.vduration().value_or_empty(),
.state = (call.vduration().value_or_empty()
? CallState::Hangup
: call.is_missed()
? CallState::Missed
: call.is_active()
? CallState::Active
: CallState::Invitation),
.video = call.is_video(),
};
}
GiveawayStart ComputeGiveawayStartData(
not_null<HistoryItem*> item,
const MTPDmessageMediaGiveaway &data) {
@ -1111,7 +1139,7 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
return toGroupPreview(group->items, options);
}
}
if (const auto sticker = _document->sticker()) {
if (_document->sticker()) {
return Media::toPreview(options);
}
auto images = std::vector<ItemPreviewImage>();
@ -1178,7 +1206,7 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
}
TextWithEntities MediaFile::notificationText() const {
if (const auto sticker = _document->sticker()) {
if (_document->sticker()) {
const auto text = _emoji.isEmpty()
? tr::lng_in_dlg_sticker(tr::now)
: tr::lng_in_dlg_sticker_emoji(tr::now, lt_emoji, _emoji);
@ -1210,7 +1238,7 @@ TextWithEntities MediaFile::notificationText() const {
}
QString MediaFile::pinnedTextSubstring() const {
if (const auto sticker = _document->sticker()) {
if (_document->sticker()) {
if (!_emoji.isEmpty()) {
return tr::lng_action_pinned_media_emoji_sticker(
tr::now,
@ -1670,11 +1698,28 @@ std::unique_ptr<HistoryView::Media> MediaLocation::createView(
MediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call)
: Media(parent)
, _call(call) {
parent->history()->owner().registerCallItem(parent);
const auto peer = parent->history()->peer;
peer->owner().registerCallItem(parent);
if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {
Core::App().calls().registerConferenceInvite(
_call.conferenceId,
user,
parent->id,
!parent->out());
}
}
MediaCall::~MediaCall() {
parent()->history()->owner().unregisterCallItem(parent());
const auto parent = this->parent();
const auto peer = parent->history()->peer;
peer->owner().unregisterCallItem(parent);
if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {
Core::App().calls().unregisterConferenceInvite(
_call.conferenceId,
user,
parent->id,
!parent->out());
}
}
std::unique_ptr<Media> MediaCall::clone(not_null<HistoryItem*> parent) {
@ -1686,7 +1731,8 @@ const Call *MediaCall::call() const {
}
TextWithEntities MediaCall::notificationText() const {
auto result = Text(parent(), _call.finishReason, _call.video);
const auto conference = (_call.conferenceId != 0);
auto result = Text(parent(), _call.state, conference, _call.video);
if (_call.duration > 0) {
result = tr::lng_call_type_and_duration(
tr::now,
@ -1727,26 +1773,39 @@ std::unique_ptr<HistoryView::Media> MediaCall::createView(
QString MediaCall::Text(
not_null<HistoryItem*> item,
CallFinishReason reason,
CallState state,
bool conference,
bool video) {
if (item->out()) {
return ((reason == CallFinishReason::Missed)
? (video
if (state == CallState::Invitation) {
return tr::lng_call_invitation(tr::now);
} else if (state == CallState::Active) {
return tr::lng_call_ongoing(tr::now);
} else if (item->out()) {
return ((state == CallState::Missed)
? (conference
? tr::lng_call_group_declined
: video
? tr::lng_call_video_cancelled
: tr::lng_call_cancelled)
: (video
: (conference
? tr::lng_call_group_outgoing
: video
? tr::lng_call_video_outgoing
: tr::lng_call_outgoing))(tr::now);
} else if (reason == CallFinishReason::Missed) {
return (video
} else if (state == CallState::Missed) {
return (conference
? tr::lng_call_group_missed
: video
? tr::lng_call_video_missed
: tr::lng_call_missed)(tr::now);
} else if (reason == CallFinishReason::Busy) {
} else if (state == CallState::Busy) {
return (video
? tr::lng_call_video_declined
: tr::lng_call_declined)(tr::now);
}
return (video
return (conference
? tr::lng_call_group_incoming
: video
? tr::lng_call_video_incoming
: tr::lng_call_incoming)(tr::now);
}

View file

@ -41,12 +41,14 @@ class WallPaper;
class Session;
struct UniqueGift;
enum class CallFinishReason : char {
enum class CallState : char {
Missed,
Busy,
Disconnected,
Hangup,
AllowGroupCall,
MigrateConferenceCall,
Invitation,
Active,
};
struct SharedContact final {
@ -78,10 +80,12 @@ struct SharedContact final {
};
struct Call {
using FinishReason = CallFinishReason;
using State = CallState;
std::vector<not_null<PeerData*>> otherParticipants;
CallId conferenceId = 0;
int duration = 0;
FinishReason finishReason = FinishReason::Missed;
State state = State::Missed;
bool video = false;
};
@ -462,9 +466,10 @@ public:
not_null<HistoryItem*> realParent,
HistoryView::Element *replacing = nullptr) override;
static QString Text(
[[nodiscard]] static QString Text(
not_null<HistoryItem*> item,
CallFinishReason reason,
CallState state,
bool conference,
bool video);
private:
@ -798,7 +803,12 @@ private:
not_null<HistoryItem*> item,
const MTPDmessageMediaPaidMedia &data);
[[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call);
[[nodiscard]] Call ComputeCallData(
not_null<Session*> owner,
const MTPDmessageActionPhoneCall &call);
[[nodiscard]] Call ComputeCallData(
not_null<Session*> owner,
const MTPDmessageActionConferenceCall &call);
[[nodiscard]] GiveawayStart ComputeGiveawayStartData(
not_null<HistoryItem*> item,

View file

@ -673,7 +673,7 @@ bool PeerData::canTransferGifts() const {
bool PeerData::canEditMessagesIndefinitely() const {
if (const auto user = asUser()) {
return user->isSelf();
} else if (const auto chat = asChat()) {
} else if (isChat()) {
return false;
} else if (const auto channel = asChannel()) {
return channel->isMegagroup()
@ -1380,7 +1380,7 @@ Data::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const {
}
bool PeerData::allowsForwarding() const {
if (const auto user = asUser()) {
if (isUser()) {
return true;
} else if (const auto channel = asChannel()) {
return channel->allowsForwarding();

View file

@ -608,7 +608,7 @@ rpl::producer<int> UniqueReactionsLimitValue(
) | rpl::map([config = &peer->session().appConfig()] {
return UniqueReactionsLimit(config);
}) | rpl::distinct_until_changed();
if (const auto channel = peer->asChannel()) {
if (peer->isChannel()) {
return rpl::combine(
PeerAllowedReactionsValue(peer),
std::move(configValue)
@ -617,7 +617,7 @@ rpl::producer<int> UniqueReactionsLimitValue(
? allowedReactions.maxCount
: limit;
});
} else if (const auto chat = peer->asChat()) {
} else if (peer->isChat()) {
return rpl::combine(
PeerAllowedReactionsValue(peer),
std::move(configValue)

View file

@ -65,13 +65,13 @@ QByteArray PhotoMedia::imageBytes(PhotoSize size) const {
auto PhotoMedia::resolveLoadedImage(PhotoSize size) const
-> const PhotoImage * {
const auto &original = _images[PhotoSizeIndex(size)];
if (const auto image = original.data.get()) {
if (original.data) {
if (original.goodFor >= size) {
return &original;
}
}
const auto &valid = _images[_owner->validSizeIndex(size)];
if (const auto image = valid.data.get()) {
if (valid.data.get()) {
if (valid.goodFor >= size) {
return &valid;
}

View file

@ -10,9 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
struct PremiumSubscriptionOption {
int months = 0;
QString duration;
QString discount;
QString costPerMonth;
QString costNoDiscount;
QString costTotal;
QString total;
QString botUrl;

View file

@ -370,7 +370,7 @@ bool RepliesList::buildFromData(not_null<Viewer*> viewer) {
const auto around = [&] {
if (viewer->around != ShowAtUnreadMsgId) {
return viewer->around;
} else if (const auto item = lookupRoot()) {
} else if (lookupRoot()) {
return computeInboxReadTillFull();
} else if (_owningTopic) {
// Somehow we don't want always to jump to computed inboxReadTill

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