diff --git a/.cursor/api_usage.md b/.cursor/api_usage.md new file mode 100644 index 0000000000..c551f0b7ce --- /dev/null +++ b/.cursor/api_usage.md @@ -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() // 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 }): + // 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(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. \ No newline at end of file diff --git a/.cursor/localization.md b/.cursor/localization.md new file mode 100644 index 0000000000..cf39e7d6fd --- /dev/null +++ b/.cursor/localization.md @@ -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 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`. 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 + +// Key: "confirm_delete_item" = "Are you sure you want to delete {item_name}?"; +auto itemNameProducer = /* ... */; // Type: rpl::producer +auto confirmationProducer = tr::lng_confirm_delete_item( // Type: rpl::producer + 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`). 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`. Use the `tr::to_count()` helper to convert an `rpl::producer` or wrap a single value. + ```cpp + // From an existing int producer: + auto countProducer = /* ... */; // Type: rpl::producer + auto filesTextProducer = tr::lng_files_selected( // Type: rpl::producer + 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 + 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` (reactive). +* Placeholder values must match the projector's *input* requirements. For `Ui::Text::WithEntities`, placeholders expect `TextWithEntities` (immediate) or `rpl::producer` (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 +auto messageProducer = tr::lng_user_posted_photo( // Type: rpl::producer + 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` (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`, 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. \ No newline at end of file diff --git a/.cursor/rpl_guide.md b/.cursor/rpl_guide.md new file mode 100644 index 0000000000..8ff7be46a5 --- /dev/null +++ b/.cursor/rpl_guide.md @@ -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 intProducer = rpl::single(123); +const rpl::lifetime &lifetime = existingLifetime; + +// Sometimes needed if deduction is ambiguous or needs help: +auto user = std::make_shared(); +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` + +The fundamental building block is `rpl::producer`. 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 + +// A producer that emits strings and can potentially emit a CustomError. +auto stringProducerWithError = /* ... */; // Type: rpl::producer +``` + +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 +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 +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 +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 + // 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 + 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 + auto textProducer = rpl::single(u"hello"_q); // Type: rpl::producer + 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 + auto sourceB = /* ... */; // Type: rpl::producer + + // 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` 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 ` \ No newline at end of file diff --git a/.cursor/styling.md b/.cursor/styling.md new file mode 100644 index 0000000000..8e3578068f --- /dev/null +++ b/.cursor/styling.md @@ -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`). \ No newline at end of file diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000000..db6b53b765 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,2 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +Telegram/ThirdParty/ diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0c88d5d498..f91548d4fa 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 (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 diff --git a/Telegram/Resources/icons/calls/call_group.png b/Telegram/Resources/icons/calls/call_group.png new file mode 100644 index 0000000000..d60c581b1f Binary files /dev/null and b/Telegram/Resources/icons/calls/call_group.png differ diff --git a/Telegram/Resources/icons/calls/call_group@2x.png b/Telegram/Resources/icons/calls/call_group@2x.png new file mode 100644 index 0000000000..ab1dd6a150 Binary files /dev/null and b/Telegram/Resources/icons/calls/call_group@2x.png differ diff --git a/Telegram/Resources/icons/calls/call_group@3x.png b/Telegram/Resources/icons/calls/call_group@3x.png new file mode 100644 index 0000000000..67f363752b Binary files /dev/null and b/Telegram/Resources/icons/calls/call_group@3x.png differ diff --git a/Telegram/Resources/icons/calls/calls_add_people.png b/Telegram/Resources/icons/calls/calls_add_people.png new file mode 100644 index 0000000000..4e07a92f47 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_add_people.png differ diff --git a/Telegram/Resources/icons/calls/calls_add_people@2x.png b/Telegram/Resources/icons/calls/calls_add_people@2x.png new file mode 100644 index 0000000000..bf55e663c3 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_add_people@2x.png differ diff --git a/Telegram/Resources/icons/calls/calls_add_people@3x.png b/Telegram/Resources/icons/calls/calls_add_people@3x.png new file mode 100644 index 0000000000..c138ad5c24 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_add_people@3x.png differ diff --git a/Telegram/Resources/icons/calls/group_call_logo.png b/Telegram/Resources/icons/calls/group_call_logo.png new file mode 100644 index 0000000000..b083149297 Binary files /dev/null and b/Telegram/Resources/icons/calls/group_call_logo.png differ diff --git a/Telegram/Resources/icons/calls/group_call_logo@2x.png b/Telegram/Resources/icons/calls/group_call_logo@2x.png new file mode 100644 index 0000000000..ca935f2249 Binary files /dev/null and b/Telegram/Resources/icons/calls/group_call_logo@2x.png differ diff --git a/Telegram/Resources/icons/calls/group_call_logo@3x.png b/Telegram/Resources/icons/calls/group_call_logo@3x.png new file mode 100644 index 0000000000..890bab3cb2 Binary files /dev/null and b/Telegram/Resources/icons/calls/group_call_logo@3x.png differ diff --git a/Telegram/Resources/icons/chat/input_gift.png b/Telegram/Resources/icons/chat/input_gift.png index c1f42e5e84..eac02d4b2a 100644 Binary files a/Telegram/Resources/icons/chat/input_gift.png and b/Telegram/Resources/icons/chat/input_gift.png differ diff --git a/Telegram/Resources/icons/chat/input_gift@2x.png b/Telegram/Resources/icons/chat/input_gift@2x.png index 79bacc11a5..51c3472a6e 100644 Binary files a/Telegram/Resources/icons/chat/input_gift@2x.png and b/Telegram/Resources/icons/chat/input_gift@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_gift@3x.png b/Telegram/Resources/icons/chat/input_gift@3x.png index 83f17c0b99..d88028bb45 100644 Binary files a/Telegram/Resources/icons/chat/input_gift@3x.png and b/Telegram/Resources/icons/chat/input_gift@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8f3ca87072..37e3631b56 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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 you’re celebrating."; +"lng_dialogs_suggestions_birthday_contact_title" = "It’s {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" = "Don’t 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..."; diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 8fa9ad8dc6..202f6b843a 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.14.1.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index e0136007f9..a0bb4a1fb6 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,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" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index cb637e054d..bf1bdc6e42 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,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" diff --git a/Telegram/SourceFiles/api/api_cloud_password.cpp b/Telegram/SourceFiles/api/api_cloud_password.cpp index e974154b98..c87b9fc15f 100644 --- a/Telegram/SourceFiles/api/api_cloud_password.cpp +++ b/Telegram/SourceFiles/api/api_cloud_password.cpp @@ -544,4 +544,38 @@ auto CloudPassword::checkRecoveryEmailAddressCode(const QString &code) }; } +void RequestLoginEmailCode( + MTP::Sender &api, + const QString &sendToEmail, + Fn done, + Fn 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 done, + Fn 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 diff --git a/Telegram/SourceFiles/api/api_cloud_password.h b/Telegram/SourceFiles/api/api_cloud_password.h index 30bc379382..64b0d54617 100644 --- a/Telegram/SourceFiles/api/api_cloud_password.h +++ b/Telegram/SourceFiles/api/api_cloud_password.h @@ -70,4 +70,15 @@ private: }; +void RequestLoginEmailCode( + MTP::Sender &api, + const QString &sendToEmail, + Fn done, + Fn fail); +void VerifyLoginEmail( + MTP::Sender &api, + const QString &code, + Fn done, + Fn fail); + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index 0dc21c6361..2c06385f6d 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -348,12 +348,15 @@ void CreditsHistory::request( void CreditsHistory::requestSubscriptions( const Data::CreditsStatusSlice::OffsetToken &token, - Fn done) { + Fn 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) { diff --git a/Telegram/SourceFiles/api/api_credits.h b/Telegram/SourceFiles/api/api_credits.h index c7005073e8..c9c87b2e8e 100644 --- a/Telegram/SourceFiles/api/api_credits.h +++ b/Telegram/SourceFiles/api/api_credits.h @@ -82,7 +82,8 @@ public: Fn done); void requestSubscriptions( const Data::CreditsStatusSlice::OffsetToken &token, - Fn done); + Fn done, + bool missingBalance = false); private: using HistoryTL = MTPpayments_GetStarsTransactions; diff --git a/Telegram/SourceFiles/api/api_messages_search.cpp b/Telegram/SourceFiles/api/api_messages_search.cpp index 8bc7a4e3d4..fb390564ef 100644 --- a/Telegram/SourceFiles/api/api_messages_search.cpp +++ b/Telegram/SourceFiles/api/api_messages_search.cpp @@ -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(), diff --git a/Telegram/SourceFiles/api/api_premium_option.cpp b/Telegram/SourceFiles/api/api_premium_option.cpp index d3c67e23b5..f5f0af380d 100644 --- a/Telegram/SourceFiles/api/api_premium_option.cpp +++ b/Telegram/SourceFiles/api/api_premium_option.cpp @@ -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, }; diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp index daed8f1eef..400ad7fcae 100644 --- a/Telegram/SourceFiles/api/api_statistics.cpp +++ b/Telegram/SourceFiles/api/api_statistics.cpp @@ -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; } diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp index cfafeb3647..9f400d05cc 100644 --- a/Telegram/SourceFiles/api/api_text_entities.cpp +++ b/Telegram/SourceFiles/api/api_text_entities.cpp @@ -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 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; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 9c959bbf99..98ad297caf 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -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); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 43b3cab7d7..d43e4c1f15 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -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; +} diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index d278d2681f..0fa39c2ee4 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -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(), diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index d9df69287f..33cba88f34 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -142,15 +142,16 @@ void PeerListBox::createMultiSelect() { } }); _select->resizeToWidth(_controller->contentWidth()); - _select->moveToLeft(0, 0); + _select->moveToLeft(0, topSelectSkip()); } void PeerListBox::appendQueryChangedCallback(Fn 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,12 +168,19 @@ 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()) { - _scrollBottomFixed = true; + setInnerTopSkip(topScrollSkip(), _scrollBottomFixed); + if (_select) { + _select->moveToLeft(0, topSelectSkip()); + if (!_select->animating()) { + _scrollBottomFixed = true; + } } } @@ -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(); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index b8dbecb8ce..068906fa26 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -1142,7 +1142,7 @@ public: void peerListScrollToTop() override; std::shared_ptr 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 _controller; Fn _init; bool _scrollBottomFixed = false; + bool _addedTopScrollAboveSearch = false; int _addedTopScrollSkip = 0; }; diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 4701cac1e7..25853e0f85 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -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 row) { updateTitle(); } else if (const auto channel = _peer ? _peer->asChannel() : nullptr) { if (!_peer->isMegagroup()) { - showBox(Box(_peer->asChannel())); + showBox(Box(channel)); } } else if (count >= serverConfig.chatSizeMax && count < serverConfig.megagroupSizeMax) { diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 33ce09ac44..bc336675f8 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -453,7 +453,7 @@ void ChoosePeerBoxController::rowClicked(not_null row) { const auto onstack = callback; onstack({ peer }); }; - if (const auto user = peer->asUser()) { + if (peer->isUser()) { done(); } else { delegate()->peerListUiShow()->showBox( diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 1147922a04..e168e800e0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -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); }); diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index 9341b7089a..4e170736bc 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -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; diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index 634e4a2480..354b1bb5a5 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -550,13 +550,13 @@ object_ptr CreateUserpicsTransfer( rpl::variable 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(parent, full); const auto raw = result.data(); - const auto &st = st::boostReplaceUserpic; - const auto right = CreateChild(raw, to, st); + const auto right = CreateChild(raw, to, st->button); const auto overlay = CreateChild(raw); const auto state = raw->lifetime().make_state(); @@ -564,7 +564,6 @@ object_ptr CreateUserpicsTransfer( from ) | rpl::start_with_next([=]( const std::vector> &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 CreateUserpicsTransfer( state->buttons.push_back(std::move(buttons[index])); } else { state->buttons.push_back( - std::make_unique(raw, peer, st)); + std::make_unique(raw, peer, st->button)); const auto raw = state->buttons.back().get(); base::install_event_filter(raw, [=](not_null e) { return (e->type() == QEvent::Paint && !state->painting) @@ -598,7 +597,7 @@ object_ptr 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 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 CreateUserpicsTransfer( object_ptr CreateUserpicsWithMoreBadge( not_null parent, rpl::producer>> peers, + const style::UserpicsRow &st, int limit) { struct State { std::vector> from; @@ -693,9 +693,8 @@ object_ptr CreateUserpicsWithMoreBadge( rpl::variable 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(parent, full); const auto raw = result.data(); const auto overlay = CreateChild(raw); @@ -703,9 +702,8 @@ object_ptr CreateUserpicsWithMoreBadge( const auto state = raw->lifetime().make_state(); std::move( peers - ) | rpl::start_with_next([=]( + ) | rpl::start_with_next([=, &st]( const std::vector> &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 CreateUserpicsWithMoreBadge( state->buttons.push_back(std::move(buttons[index])); } else { state->buttons.push_back( - std::make_unique(raw, peer, st)); + std::make_unique(raw, peer, st.button)); const auto raw = state->buttons.back().get(); base::install_event_filter(raw, [=](not_null e) { return (e->type() == QEvent::Paint && !state->painting) @@ -732,16 +730,21 @@ object_ptr 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 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,31 +771,40 @@ object_ptr 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 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 last = state->buttons.back().get(); - const auto add = st::boostReplaceIconAdd; - const auto skip = st::boostReplaceIconSkip; - const auto w = st::boostReplaceIcon.width() + 2 * skip; - 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()) { + if (st.complex && !text.isEmpty()) { + const auto last = state->buttons.back().get(); + const auto add = st::boostReplaceIconAdd; + const auto skip = st::boostReplaceIconSkip; + const auto w = st::boostReplaceIcon.width() + 2 * skip; + 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 &font = st::semiboldFont; const auto width = font->width(text); const auto padded = std::max(w, width + 2 * font->spacew); diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h index 6e622479dc..71f5bab64d 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -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 CreateUserpicsWithMoreBadge( not_null parent, rpl::producer>> peers, + const style::UserpicsRow &st, int limit); diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index f02ea735c8..4247e936c3 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -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, PickType)>; + struct PremiumGiftsDescriptor { std::vector list; std::shared_ptr 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 updateCallback) override; + void rightActionStopLastRipple() override; + +private: + std::unique_ptr _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 updateCallback) { + if (!_actionRipple) { + auto mask = Ui::RippleAnimation::EllipseMask( + Size(st::inviteLinkThreeDotsIcon.height())); + _actionRipple = std::make_unique( + 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>( + object_ptr>( content, object_ptr(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 CreateRowContextMenu( + QWidget *parent, + not_null peer, + PickCallback pick) { + auto result = base::make_unique_q( + 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 content = { nullptr }; Fn overrideKey; Fn activate; + Fn hasSelection; }; class Controller final : public ContactsBoxController { public: - Controller( - not_null session, - Fn)> choose); + Controller(not_null session, PickCallback pick); void noSearchSubmit(); bool overrideKeyboardNavigation( int direction, int fromIndex, - int toIndex) override; + int toIndex) override final; + + void rowRightActionClicked(not_null row) override final; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override final; private: std::unique_ptr createRow( @@ -2283,41 +2390,71 @@ private: void prepareViewHook() override; void rowClicked(not_null row) override; - const Fn)> _choose; - SelfOption _selfOption; + const PickCallback _pick; + const std::vector _contactBirthdays; + CustomList _selfOption; + CustomList _birthdayOptions; + + base::unique_qptr _menu; + + bool _skipUpDirectionSelect = false; }; -[[nodiscard]] SelfOption MakeSelfOption( +[[nodiscard]] CustomList MakeCustomList( not_null session, - Fn activate) { - class SelfController final : public PeerListController { + Fn)> fill, + PickCallback pick, + rpl::producer below) { + class CustomController final : public PeerListController { public: - SelfController( + CustomController( not_null session, - Fn activate) + Fn)> 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(_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 row) override { - _activate(); + _pick(row->peer(), PickType::Activate); } Main::Session &session() const override { return *_session; } + void rowRightActionClicked(not_null row) override { + delegate()->peerListShowRowMenu(row, true); + } + + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override { + auto result = CreateRowContextMenu(parent, row->peer(), _pick); + + if (result) { + base::take(_menu); + + _menu = base::unique_qptr(result.get()); + } + + return result; + } + private: const not_null _session; - Fn _activate; + PickCallback _pick; + Fn)> _fill; + + base::unique_qptr _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( + session, + fill, + pick); + controller->setStyleOverrides(&st::peerListSingleRow); const auto content = container->add(object_ptr( container, @@ -2339,10 +2479,12 @@ private: delegate->setContent(content); controller->setDelegate(delegate); - Ui::AddSkip(container); - container->add(CreatePeerListSectionSubtitle( - container, - tr::lng_contacts_header())); + if (below) { + Ui::AddSkip(container); + container->add(CreatePeerListSectionSubtitle( + container, + std::move(below))); + } const auto overrideKey = [=](int direction, int from, int to) { if (!content->isVisible()) { @@ -2368,77 +2510,242 @@ 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 session, - Fn)> choose) +Controller::Controller(not_null 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{})) +, _selfOption( + MakeCustomList( + session, + [=](not_null controller) { + auto row = std::make_unique(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 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(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(nullptr) + : tr::lng_contacts_header())) { setStyleOverrides(&st::peerListSmallSkips); } +void Controller::rowRightActionClicked(not_null row) { + delegate()->peerListShowRowMenu(row, true); +} + +base::unique_qptr Controller::rowContextMenu( + QWidget *parent, + not_null 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(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 Controller::createRow( not_null 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(user); } void Controller::prepareViewHook() { - delegate()->peerListSetAboveWidget(std::move(_selfOption.content)); + auto list = object_ptr((QWidget*)nullptr); + list->add(std::move(_selfOption.content)); + list->add(std::move(_birthdayOptions.content)); + delegate()->peerListSetAboveWidget(std::move(list)); } void Controller::rowClicked(not_null row) { - _choose(row->peer()); + _pick(row->peer(), PickType::Activate); } } // namespace void ChooseStarGiftRecipient( not_null window) { - auto controller = std::make_unique( - &window->session(), - [=](not_null peer) { - ShowStarGiftBox(window, peer); - }); - const auto controllerRaw = controller.get(); - auto initBox = [=](not_null box) { - box->setTitle(tr::lng_gift_premium_or_stars()); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + const auto session = &window->session(); + const auto lifetime = std::make_shared(); + session->data().contactBirthdays( + ) | rpl::start_with_next(crl::guard(session, [=] { + lifetime->destroy(); + auto controller = std::make_unique( + session, + [=](not_null 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 box) { + box->setTitle(tr::lng_gift_premium_or_stars()); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); - box->noSearchSubmits() | rpl::start_with_next([=] { - controllerRaw->noSearchSubmit(); - }, box->lifetime()); - }; - window->show( - Box(std::move(controller), std::move(initBox)), - LayerOption::KeepOther); + box->noSearchSubmits() | rpl::start_with_next([=] { + controllerRaw->noSearchSubmit(); + }, box->lifetime()); + }; + window->show( + Box(std::move(controller), std::move(initBox)), + LayerOption::KeepOther); + }), *lifetime); } void ShowStarGiftBox( diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 3f3d965981..bfca445777 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -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()) { diff --git a/Telegram/SourceFiles/boxes/url_auth_box.cpp b/Telegram/SourceFiles/boxes/url_auth_box.cpp index ef72584cf5..3dc29d9222 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.cpp +++ b/Telegram/SourceFiles/boxes/url_auth_box.cpp @@ -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)) diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index d48cfdaa06..304a318cbf 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -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; diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index a3d0d53694..372aa37c53 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -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 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) +ListController::ListController(not_null<::Window::SessionController*> window) : _window(window) { setStyleOverrides(&st::peerListSingleRow); } @@ -227,7 +239,7 @@ void ListController::rowClicked(not_null 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) +BoxController::BoxController(not_null<::Window::SessionController*> window) : _window(window) , _api(&_window->session().mtp()) { } @@ -591,7 +603,7 @@ void BoxController::rowClicked(not_null 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 &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 BoxController::createRow( void ClearCallsBox( not_null box, - not_null window) { + not_null<::Window::SessionController*> window) { const auto weak = Ui::MakeWeak(box); box->addRow( object_ptr( @@ -756,4 +768,133 @@ void ClearCallsBox( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } +[[nodiscard]] not_null AddCreateCallButton( + not_null container, + not_null<::Window::SessionController*> controller, + Fn done) { + const auto result = container->add(object_ptr( + 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( + 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 menu; + }; + + window->show(Box([=](not_null box) { + const auto state = box->lifetime().make_state(window); + + const auto groupCalls = box->addRow( + object_ptr>( + box, + object_ptr(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(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 e) { + return (e->type() == QEvent::Enter); + }) | rpl::start_with_next([=] { + state->callsDelegate.peerListMouseLeftGeometry(); + }, button->lifetime()); + + const auto content = box->addRow( + object_ptr(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( + 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 diff --git a/Telegram/SourceFiles/calls/calls_box_controller.h b/Telegram/SourceFiles/calls/calls_box_controller.h index 860839cdf2..9c183f1088 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.h +++ b/Telegram/SourceFiles/calls/calls_box_controller.h @@ -20,7 +20,7 @@ namespace GroupCalls { class ListController : public PeerListController { public: - explicit ListController(not_null window); + explicit ListController(not_null<::Window::SessionController*> window); [[nodiscard]] rpl::producer shownValue() const; @@ -30,7 +30,7 @@ public: void rowRightActionClicked(not_null row) override; private: - const not_null _window; + const not_null<::Window::SessionController*> _window; base::flat_map> _groupCalls; rpl::variable _fullCount; @@ -40,7 +40,7 @@ private: class BoxController : public PeerListController { public: - explicit BoxController(not_null window); + explicit BoxController(not_null<::Window::SessionController*> window); Main::Session &session() const override; void prepare() override; @@ -68,7 +68,7 @@ private: std::unique_ptr createRow( not_null item) const; - const not_null _window; + const not_null<::Window::SessionController*> _window; MTP::Sender _api; MsgId _offsetId = 0; @@ -79,6 +79,8 @@ private: void ClearCallsBox( not_null box, - not_null window); + not_null<::Window::SessionController*> window); + +void ShowCallsBox(not_null<::Window::SessionController*> window); } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 54d2b9a2e2..bef4567648 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -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(); const auto RegisterV2 = tgcalls::Register(); const auto RegV2Ref = tgcalls::Register(); -const auto RegisterLegacy = tgcalls::Register(); [[nodiscard]] base::flat_set CollectEndpointIds( const QVector &list) { @@ -246,7 +245,52 @@ Call::Call( setupOutgoingVideo(); } +Call::Call( + not_null delegate, + not_null user, + CallId conferenceId, + MsgId conferenceInviteMsgId, + std::vector> 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()) +, _conferenceId(conferenceId) +, _conferenceInviteMsgId(conferenceInviteMsgId) +, _conferenceParticipants(std::move(conferenceParticipants)) +, _videoIncoming( + std::make_unique( + StartVideoState(video))) +, _videoOutgoing( + std::make_unique( + 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()), 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 uniqueId) { _videoOutgoing->setState(Webrtc::VideoState::Active); } +auto Call::peekVideoCapture() const +-> std::shared_ptr { + return _videoCapture; +} + auto Call::playbackDeviceIdValue() const -> rpl::producer { return _playbackDeviceId.value(); @@ -1324,7 +1477,10 @@ rpl::producer 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; - } - if (!_id) { + } else if (conferenceInvite()) { + if (migrateCall) { + _delegate->callFinished(this); + } else { + Core::App().calls().declineIncomingConferenceInvites(_conferenceId); + setState(finalState); + } + 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 diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 1d01d6d5f5..41ee5dd9ad 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -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 user, Type type, bool video); + Call( + not_null delegate, + not_null user, + CallId conferenceId, + MsgId conferenceInviteMsgId, + std::vector> 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> & { + 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 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 uniqueId); + [[nodiscard]] auto peekVideoCapture() const + -> std::shared_ptr; [[nodiscard]] auto playbackDeviceIdValue() const -> rpl::producer; @@ -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; const not_null _user; MTP::Sender _api; Type _type = Type::Outgoing; rpl::variable _state = State::Starting; + rpl::variable _conferenceSupported = false; rpl::variable _remoteAudioState = RemoteAudioState::Active; rpl::variable _remoteVideoState; @@ -316,6 +358,10 @@ private: uint64 _accessHash = 0; uint64 _keyFingerprint = 0; + CallId _conferenceId = 0; + MsgId _conferenceInviteMsgId = 0; + std::vector> _conferenceParticipants; + std::unique_ptr _instance; std::shared_ptr _videoCapture; QString _videoCaptureDeviceId; diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp index 4606c31489..a36dff6044 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp @@ -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(bytes[0]) & 0x7F) << 56) | (gsl::to_integer(bytes[1]) << 48) | (gsl::to_integer(bytes[2]) << 40) @@ -121,40 +133,41 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) { | (gsl::to_integer(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(Data + offset), + size); + return Ui::Emoji::Find(string); +} + } // namespace std::vector ComputeEmojiFingerprint(not_null call) { - auto result = std::vector(); - 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(Data + offset), - size); - auto emoji = Ui::Emoji::Find(string); - Assert(emoji != nullptr); + if (!call->isKeyShaForFingerprintReady()) { + return {}; } - 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(Data + offset), - size); - auto emoji = Ui::Emoji::Find(string); - Assert(emoji != nullptr); - result.push_back(emoji); - } + return ComputeEmojiFingerprint(call->getKeyShaForFingerprint()); +} + +std::vector ComputeEmojiFingerprint( + bytes::const_span fingerprint) { + auto result = std::vector(); + constexpr auto kPartSize = 8; + 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 CreateFingerprintAndSignalBars( +base::unique_qptr CreateFingerprintAndSignalBars( not_null parent, not_null call) { class EmojiTooltipShower final : public Ui::AbstractTooltipShower { @@ -180,8 +193,8 @@ object_ptr CreateFingerprintAndSignalBars( }; - auto result = object_ptr(parent); - const auto raw = result.data(); + auto result = base::make_unique_q(parent); + const auto raw = result.get(); // Emoji tooltip. const auto shower = raw->lifetime().make_state( @@ -295,4 +308,516 @@ object_ptr CreateFingerprintAndSignalBars( return result; } +FingerprintBadge SetupFingerprintBadge( + rpl::lifetime &on, + rpl::producer fingerprint) { + struct State { + FingerprintBadgeState data; + Ui::Animations::Basic animation; + Fn update; + rpl::event_stream<> repaints; + }; + const auto state = on.make_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 &buffered) { + auto &entry = state->data.entries[index]; + auto indices = std::vector(); + 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 &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( + kEmojiInCarousel * kEmojiInFingerprint); + const auto now = crl::now(); + const auto emoji = (fingerprint.size() >= 32) + ? ComputeEmojiFingerprint( + bytes::make_span(fingerprint).subspan(0, 32)) + : std::vector(); + 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 widget) { + struct State { + std::unique_ptr tooltip; + Fn updateGeometry; + Fn toggleTooltip; + }; + const auto state = widget->lifetime().make_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(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( + widget->window(), + Ui::MakeNiceTooltipLabel( + widget, + rpl::single(text), + st::confcallFingerprintTooltipMaxWidth, + st::confcallFingerprintTooltipLabel), + st::confcallFingerprintTooltip); + const auto raw = state->tooltip.get(); + const auto weak = QPointer(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 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 widget, + not_null state, + rpl::producer<> repaints) { + auto &lifetime = widget->lifetime(); + + const auto button = Ui::CreateChild(widget); + button->show(); + + const auto label = Ui::CreateChild( + 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(); + 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 diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h index 9ec4e9b684..56776b8e1b 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.h @@ -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 ComputeEmojiFingerprint( not_null call); +[[nodiscard]] std::vector ComputeEmojiFingerprint( + bytes::const_span fingerprint); -[[nodiscard]] object_ptr CreateFingerprintAndSignalBars( +[[nodiscard]] base::unique_qptr CreateFingerprintAndSignalBars( not_null parent, not_null call); +struct FingerprintBadgeState { + struct Entry { + EmojiPtr emoji = nullptr; + std::vector sliding; + std::vector carousel; + crl::time time = 0; + float64 speed = 0.; + float64 position = 0.; + int added = 0; + }; + std::vector entries; + float64 speed = 1.; +}; +struct FingerprintBadge { + not_null state; + rpl::producer<> repaints; +}; +FingerprintBadge SetupFingerprintBadge( + rpl::lifetime &on, + rpl::producer fingerprint); + +void SetupFingerprintBadgeWidget( + not_null widget, + not_null state, + rpl::producer<> repaints); + +struct FingerprintBadgeCache { + struct Emoji { + EmojiPtr ptr = nullptr; + QImage image; + }; + struct Entry { + std::vector emoji; + }; + std::vector entries; + QImage shadow; +}; +void PaintFingerprintEntry( + QPainter &p, + const FingerprintBadgeState::Entry &entry, + FingerprintBadgeCache::Entry &cache, + int esize); + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 02c5fb1444..d79a317018 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -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) { } void Instance::Delegate::callRedial(not_null 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(_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( + 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 call, + StartConferenceInfo args) { + if (_startingGroupCall.get() != call) { + return; + } + const auto migrationInfo = _currentCallPanel + ? _currentCallPanel->migrationInfo() + : ConferencePanelMigration(); + _currentGroupCallPanel = std::make_unique( + 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 show, not_null peer, @@ -310,7 +390,11 @@ void Instance::playSoundOnce(const QString &key) { void Instance::destroyCall(not_null 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) { void Instance::createCall( not_null user, - Call::Type type, + CallType type, bool isVideo) { struct Performer final { explicit Performer(Fn callback) @@ -385,6 +469,8 @@ void Instance::destroyGroupCall(not_null 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 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 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,19 +789,24 @@ 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()) { - current->hangup(); - if (const auto still = currentGroupCall()) { - destroyGroupCall(still); + if (!migrateCall || current->lookupReal() != migrateCall) { + current->hangup(); + if (const auto still = currentGroupCall()) { + destroyGroupCall(still); + } } } + base::take(_startingGroupCall); } bool Instance::hasVisiblePanel(Main::Session *session) const { @@ -849,4 +960,188 @@ std::shared_ptr 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 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 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 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(); + 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 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( + 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(raw); + _currentCall = std::move(call); + } + _currentCallChanges.fire_copy(raw); + } +} + + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index d52a0e9a23..1c3d8b9212 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -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 incoming; + base::flat_set outgoing; +}; + +struct ConferenceInvites { + base::flat_map, ConferenceInviteMessages> users; +}; + class Instance final : public base::has_weak_ptr { public: Instance(); @@ -69,6 +85,10 @@ public: std::shared_ptr show, not_null peer, StartGroupCallArgs args); + void startOrJoinConferenceCall(StartConferenceInfo args); + void startedConferenceReady( + not_null call, + StartConferenceInfo args); void showStartWithRtmp( std::shared_ptr show, not_null peer); @@ -103,6 +123,28 @@ public: -> std::shared_ptr; void requestPermissionsOrFail(Fn onSuccess, bool video = true); + [[nodiscard]] const ConferenceInvites &conferenceInvites( + CallId conferenceId) const; + void registerConferenceInvite( + CallId conferenceId, + not_null user, + MsgId messageId, + bool incoming); + void unregisterConferenceInvite( + CallId conferenceId, + not_null user, + MsgId messageId, + bool incoming, + bool onlyStopCalling = false); + void showConferenceInvite( + not_null user, + MsgId conferenceInviteMsgId); + void declineIncomingConferenceInvites(CallId conferenceId); + void declineOutgoingConferenceInvite( + CallId conferenceId, + not_null user, + bool discard = false); + [[nodiscard]] FnMut addAsyncWaiter(); [[nodiscard]] bool isSharingScreen() const; @@ -117,6 +159,7 @@ private: void createCall(not_null user, CallType type, bool isVideo); void destroyCall(not_null call); + void finishConferenceInvitations(const StartConferenceInfo &args); void createGroupCall( Group::JoinInfo info, @@ -136,7 +179,9 @@ private: void refreshServerConfig(not_null 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 session, const MTPPhoneCall &call); @@ -159,6 +204,7 @@ private: std::unique_ptr _currentCallPanel; std::unique_ptr _currentGroupCall; + std::unique_ptr _startingGroupCall; rpl::event_stream _currentGroupCallChanges; std::unique_ptr _currentGroupCallPanel; @@ -167,6 +213,8 @@ private: const std::unique_ptr _chooseJoinAs; const std::unique_ptr _startWithRtmp; + base::flat_map _conferenceInvites; + base::flat_set> _asyncWaiters; }; diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index e5af06c171..dbc9da8630 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -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) , _user(call->user()) -, _layerBg(std::make_unique(widget())) -#ifndef Q_OS_MAC -, _controls(Ui::Platform::SetupSeparateTitleControls( - window(), - st::callTitle, - [=](bool maximized) { toggleFullScreen(maximized); })) -#endif // !Q_OS_MAC +, _window(std::make_shared()) , _bodySt(&st::callBodyLayout) -, _answerHangupRedial(widget(), st::callAnswer, &st::callHangup) -, _decline(widget(), object_ptr(widget(), st::callHangup)) -, _cancel(widget(), object_ptr(widget(), st::callCancel)) +, _answerHangupRedial( + std::in_place, + widget(), + st::callAnswer, + &st::callHangup) +, _decline( + std::in_place, + widget(), + object_ptr(widget(), st::callHangup)) +, _cancel( + std::in_place, + widget(), + object_ptr(widget(), st::callCancel)) , _screencast( + std::in_place, widget(), object_ptr( 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( widget(), st::callMicrophoneMute, &st::callMicrophoneUnmute)) -, _name(widget(), st::callName) -, _status(widget(), st::callStatus) +, _addPeople( + std::in_place, + widget(), + object_ptr(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 Panel::sessionShow() { + return Main::MakeSessionShow(uiShow(), &_user->session()); +} + +std::shared_ptr 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(); + const auto create = [=](std::vector 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 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(widget()); + _conferenceParticipants->show(); + const auto raw = _conferenceParticipants.get(); + + auto peers = std::vector>(); + 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( + 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( @@ -483,7 +610,7 @@ void Panel::reinitWithCall(Call *call) { _incoming = std::make_unique( 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::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>( widget(), object_ptr( widget(), @@ -694,7 +815,7 @@ void Panel::createRemoteAudioMute() { } void Panel::createRemoteLowBattery() { - _remoteLowBattery.create( + _remoteLowBattery = base::make_unique_q>( widget(), object_ptr( 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() { - window()->close(); +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 Panel::window() const { - return _window.window(); + return _window->window(); } not_null Panel::widget() const { - return _window.widget(); + return _window->widget(); +} + +not_null 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); diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index ee24ab2c4a..bcdf373c42 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -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; class IconButton; class CallButton; class LayerManager; @@ -38,14 +42,17 @@ template 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); ~Panel(); + [[nodiscard]] not_null widget() const; + [[nodiscard]] not_null 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); - void closeBeforeDestroy(); + void closeBeforeDestroy(bool windowIsReused = false); QWidget *chooseSourceParent() override; QString chooseSourceActiveDeviceId() override; @@ -83,6 +99,11 @@ public: [[nodiscard]] rpl::producer startOutgoingRequests() const; + [[nodiscard]] std::shared_ptr sessionShow(); + [[nodiscard]] std::shared_ptr uiShow(); + + [[nodiscard]] not_null window() const; + [[nodiscard]] rpl::lifetime &lifetime(); private: @@ -96,14 +117,12 @@ private: StartCall, }; - [[nodiscard]] not_null window() const; - [[nodiscard]] not_null 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 _user; - Ui::GL::Window _window; - const std::unique_ptr _layerBg; + std::shared_ptr _window; std::unique_ptr _incoming; -#ifndef Q_OS_MAC - std::unique_ptr _controls; -#endif // !Q_OS_MAC - - std::unique_ptr _powerSaveBlocker; - QSize _incomingFrameSize; rpl::lifetime _callLifetime; not_null _bodySt; - object_ptr _answerHangupRedial; - object_ptr> _decline; - object_ptr> _cancel; + base::unique_qptr _answerHangupRedial; + base::unique_qptr> _decline; + base::unique_qptr> _cancel; bool _hangupShown = false; + bool _conferenceSupported = false; bool _outgoingPreviewInBody = false; std::optional _answerHangupRedialState; Ui::Animations::Simple _hangupShownProgress; - object_ptr> _screencast; - object_ptr _camera; + base::unique_qptr> _screencast; + base::unique_qptr _camera; Ui::CallButton *_cameraDeviceToggle = nullptr; base::unique_qptr _startVideo; - object_ptr> _mute; + base::unique_qptr> _mute; Ui::CallButton *_audioDeviceToggle = nullptr; - object_ptr _name; - object_ptr _status; - object_ptr _fingerprint = { nullptr }; - object_ptr> _remoteAudioMute = { nullptr }; - object_ptr> _remoteLowBattery - = { nullptr }; + base::unique_qptr> _addPeople; + base::unique_qptr _name; + base::unique_qptr _status; + base::unique_qptr _conferenceParticipants; + base::unique_qptr _fingerprint; + base::unique_qptr> _remoteAudioMute; + base::unique_qptr> _remoteLowBattery; std::unique_ptr _userpic; std::unique_ptr _outgoingVideoBubble; QPixmap _bottomShadow; @@ -200,6 +214,8 @@ private: rpl::event_stream _startOutgoingRequests; + rpl::lifetime _lifetime; + }; } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index dea58fd2f1..03e92a3ee4 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -228,14 +228,14 @@ private: TopBar::TopBar( QWidget *parent, - const base::weak_ptr &call, + Call *call, std::shared_ptr show) : TopBar(parent, show, call, nullptr) { } TopBar::TopBar( QWidget *parent, - const base::weak_ptr &call, + GroupCall *call, std::shared_ptr show) : TopBar(parent, show, nullptr, call) { } @@ -243,8 +243,8 @@ TopBar::TopBar( TopBar::TopBar( QWidget *parent, std::shared_ptr show, - const base::weak_ptr &call, - const base::weak_ptr &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( diff --git a/Telegram/SourceFiles/calls/calls_top_bar.h b/Telegram/SourceFiles/calls/calls_top_bar.h index 7b62e6673a..6f3610773c 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.h +++ b/Telegram/SourceFiles/calls/calls_top_bar.h @@ -42,11 +42,11 @@ class TopBar : public Ui::RpWidget { public: TopBar( QWidget *parent, - const base::weak_ptr &call, + Call *call, std::shared_ptr show); TopBar( QWidget *parent, - const base::weak_ptr &call, + GroupCall *call, std::shared_ptr show); ~TopBar(); @@ -64,8 +64,8 @@ private: TopBar( QWidget *parent, std::shared_ptr show, - const base::weak_ptr &call, - const base::weak_ptr &groupCall); + Call *call, + GroupCall *groupCall); void initControls(); void setupInitialBrush(); diff --git a/Telegram/SourceFiles/calls/calls_window.cpp b/Telegram/SourceFiles/calls/calls_window.cpp new file mode 100644 index 0000000000..16b754d660 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_window.cpp @@ -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); + ~Show(); + + void showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const override; + [[nodiscard]] not_null toastParent() const override; + [[nodiscard]] bool valid() const override; + operator bool() const override; + +private: + const base::weak_ptr _window; + +}; + +Show::Show(not_null window) +: _window(base::make_weak(window)) { +} + +Show::~Show() = default; + +void Show::showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const { + using UniqueLayer = std::unique_ptr; + using ObjectBox = object_ptr; + if (auto layerWidget = std::get_if(&layer)) { + if (const auto window = _window.get()) { + window->showLayer(std::move(*layerWidget), options, animated); + } + } else if (auto box = std::get_if(&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 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(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 Window::window() const { + return _window.window(); +} + +not_null 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 { +#ifndef Q_OS_MAC + return _controls->controls.layout().changes(); +#else // Q_OS_MAC + return rpl::never(); +#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 Window::maximizeRequests() const { + return _maximizeRequests.events(); +} + +base::weak_ptr Window::showToast( + const QString &text, + crl::time duration) { + return Show(this).showToast(text, duration); +} + +base::weak_ptr Window::showToast( + TextWithEntities &&text, + crl::time duration) { + return Show(this).showToast(std::move(text), duration); +} + +base::weak_ptr 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 box) { + showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal); +} + +void Window::showBox( + object_ptr 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 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 Window::uiShow() { + return std::make_shared(this); +} + +void Window::togglePowerSaveBlocker(bool enabled) { + if (!enabled) { + _powerSaveBlocker = nullptr; + } else if (!_powerSaveBlocker) { + _powerSaveBlocker = std::make_unique( + base::PowerSaveBlockType::PreventDisplaySleep, + u"Video call is active"_q, + window()->windowHandle()); + } +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_window.h b/Telegram/SourceFiles/calls/calls_window.h new file mode 100644 index 0000000000..5963b4fc4d --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_window.h @@ -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; +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 window() const; + [[nodiscard]] not_null 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; + [[nodiscard]] bool controlsHasHitTest(QPoint widgetPoint) const; + [[nodiscard]] rpl::producer maximizeRequests() const; + + void raiseLayers(); + [[nodiscard]] const Ui::LayerWidget *topShownLayer() const; + + base::weak_ptr showToast( + const QString &text, + crl::time duration = 0); + base::weak_ptr showToast( + TextWithEntities &&text, + crl::time duration = 0); + base::weak_ptr showToast( + Ui::Toast::Config &&config); + + void showBox(object_ptr box); + void showBox( + object_ptr box, + Ui::LayerOptions options, + anim::type animated = anim::type::normal); + void showLayer( + std::unique_ptr 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 uiShow(); + + void togglePowerSaveBlocker(bool enabled); + +private: + Ui::GL::Window _window; + const std::unique_ptr _layerBg; + +#ifndef Q_OS_MAC + rpl::variable _controlsTop = 0; + const std::unique_ptr _controls; +#endif // !Q_OS_MAC + + std::unique_ptr _powerSaveBlocker; + + rpl::event_stream _maximizeRequests; + rpl::event_stream<> _showingLayer; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 8c1dbb4fee..8663c534c3 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -8,6 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_group_call.h" #include "calls/group/calls_group_common.h" +#include "calls/calls_instance.h" +#include "main/session/session_show.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "api/api_send_progress.h" #include "api/api_updates.h" @@ -15,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "lang/lang_hardcoded.h" #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration. +#include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/ui_utility.h" #include "base/unixtime.h" @@ -29,6 +33,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "base/global_shortcuts.h" #include "base/random.h" +#include "tde2e/tde2e_api.h" +#include "tde2e/tde2e_integration.h" #include "webrtc/webrtc_video_track.h" #include "webrtc/webrtc_create_adm.h" #include "webrtc/webrtc_environment.h" @@ -52,15 +58,13 @@ constexpr auto kFixManualLargeVideoDuration = 5 * crl::time(1000); constexpr auto kFixSpeakingLargeVideoDuration = 3 * crl::time(1000); constexpr auto kFullAsMediumsCount = 4; // 1 Full is like 4 Mediums. constexpr auto kMaxMediumQualities = 16; // 4 Fulls or 16 Mediums. +constexpr auto kShortPollChainBlocksPerRequest = 50; [[nodiscard]] const Data::GroupCallParticipant *LookupParticipant( - not_null peer, - CallId id, + not_null call, not_null participantPeer) { - const auto call = peer->groupCall(); - return (id && call && call->id() == id) - ? call->participantByPeer(participantPeer) - : nullptr; + const auto real = call->lookupReal(); + return real ? real->participantByPeer(participantPeer) : nullptr; } [[nodiscard]] double TimestampFromMsgId(mtpMsgId msgId) { @@ -569,18 +573,39 @@ GroupCall::GroupCall( not_null delegate, Group::JoinInfo info, const MTPInputGroupCall &inputCall) +: GroupCall(delegate, info, {}, inputCall) { +} + +GroupCall::GroupCall( + not_null delegate, + StartConferenceInfo info) +: GroupCall(delegate, Group::JoinInfo{ + .peer = info.call ? info.call->peer() : info.show->session().user(), + .joinAs = info.call ? info.call->peer() : info.show->session().user(), +}, info, info.call + ? info.call->input() + : MTP_inputGroupCall(MTP_long(0), MTP_long(0))) { +} + +GroupCall::GroupCall( + not_null delegate, + Group::JoinInfo join, + StartConferenceInfo conference, + const MTPInputGroupCall &inputCall) : _delegate(delegate) -, _peer(info.peer) +, _conferenceCall(std::move(conference.call)) +, _peer(join.peer) , _history(_peer->owner().history(_peer)) , _api(&_peer->session().mtp()) -, _joinAs(info.joinAs) -, _possibleJoinAs(std::move(info.possibleJoinAs)) -, _joinHash(info.joinHash) -, _rtmpUrl(info.rtmpInfo.url) -, _rtmpKey(info.rtmpInfo.key) +, _joinAs(join.joinAs) +, _possibleJoinAs(std::move(join.possibleJoinAs)) +, _joinHash(join.joinHash) +, _conferenceLinkSlug(conference.linkSlug) +, _conferenceJoinMessageId(conference.joinMessageId) +, _rtmpUrl(join.rtmpInfo.url) +, _rtmpKey(join.rtmpInfo.key) , _canManage(Data::CanManageGroupCallValue(_peer)) -, _id(inputCall.c_inputGroupCall().vid().v) -, _scheduleDate(info.scheduleDate) +, _scheduleDate(join.scheduleDate) , _lastSpokeCheckTimer([=] { checkLastSpoke(); }) , _checkJoinedTimer([=] { checkJoined(); }) , _playbackDeviceId( @@ -601,9 +626,11 @@ GroupCall::GroupCall( Webrtc::DeviceIdOrDefault(Core::App().settings().cameraDeviceIdValue())) , _pushToTalkCancelTimer([=] { pushToTalkCancel(); }) , _connectingSoundTimer([=] { playConnectingSoundOnce(); }) -, _listenersHidden(info.rtmp) -, _rtmp(info.rtmp) +, _listenersHidden(join.rtmp) +, _rtmp(join.rtmp) , _rtmpVolume(Group::kDefaultVolume) { + applyInputCall(inputCall); + _muted.value( ) | rpl::combine_previous( ) | rpl::start_with_next([=](MuteState previous, MuteState state) { @@ -635,7 +662,7 @@ GroupCall::GroupCall( if (!canManage() && real->joinMuted()) { _muted = MuteState::ForceMuted; } - } else { + } else if (!conference.migrating && !conference.show) { _peer->session().changes().peerFlagsValue( _peer, Data::PeerUpdate::Flag::GroupCall @@ -655,18 +682,67 @@ GroupCall::GroupCall( setupMediaDevices(); setupOutgoingVideo(); + if (_conferenceCall) { + setupConferenceCall(); + initConferenceE2E(); + } else if (conference.migrating || conference.show) { + initConferenceE2E(); + } + if (conference.migrating || (conference.show && !_conferenceCall)) { + if (!conference.muted) { + setMuted(MuteState::Active); + } + _startConferenceInfo = std::make_shared( + std::move(conference)); + } - if (_id) { - join(inputCall); + if (_id || (!_conferenceCall && _startConferenceInfo)) { + initialJoin(); } else { - start(info.scheduleDate, info.rtmp); + start(join.scheduleDate, join.rtmp); } if (_scheduleDate) { saveDefaultJoinAs(joinAs()); } } +void GroupCall::processConferenceStart(StartConferenceInfo conference) { + if (!conference.videoCapture) { + return; + } + fillActiveVideoEndpoints(); + const auto weak = base::make_weak(this); + if (!conference.videoCaptureScreenId.isEmpty()) { + _screenCapture = std::move(conference.videoCapture); + _screenDeviceId = conference.videoCaptureScreenId; + _screenCapture->setOnFatalError([=] { + crl::on_main(weak, [=] { + emitShareScreenError(Error::ScreenFailed); + }); + }); + _screenCapture->setOnPause([=](bool paused) { + crl::on_main(weak, [=] { + if (isSharingScreen()) { + _screenState = paused + ? Webrtc::VideoState::Paused + : Webrtc::VideoState::Active; + } + }); + }); + _screenState = Webrtc::VideoState::Active; + } else { + _cameraCapture = std::move(conference.videoCapture); + _cameraCapture->setOnFatalError([=] { + crl::on_main(weak, [=] { + emitShareCameraError(Error::CameraFailed); + }); + }); + _cameraState = Webrtc::VideoState::Active; + } +} + GroupCall::~GroupCall() { + _e2e = nullptr; destroyScreencast(); destroyController(); if (!_rtmp) { @@ -674,6 +750,106 @@ GroupCall::~GroupCall() { } } +void GroupCall::initConferenceE2E() { + if (!_e2eEncryptDecrypt) { + _e2eEncryptDecrypt = std::make_shared(); + } + + for (auto &state : _subchains) { + _api.request(base::take(state.requestId)).cancel(); + state = SubChainState(); + } + _e2e = nullptr; + _pendingOutboundBlock = QByteArray(); + + const auto tde2eUserId = TdE2E::MakeUserId(_peer->session().user()); + _e2e = std::make_unique(tde2eUserId); + + _e2e->subchainRequests( + ) | rpl::start_with_next([=](TdE2E::Call::SubchainRequest request) { + requestSubchainBlocks(request.subchain, request.height); + }, _e2e->lifetime()); + + _e2e->sendOutboundBlock( + ) | rpl::start_with_next([=](QByteArray &&block) { + sendOutboundBlock(std::move(block)); + }, _e2e->lifetime()); + + _e2e->failures() | rpl::start_with_next([=] { + LOG(("TdE2E: Got failure, scheduling rejoin!")); + crl::on_main(this, [=] { startRejoin(); }); + }, _e2e->lifetime()); + + _e2e->registerEncryptDecrypt(_e2eEncryptDecrypt); + + _emojiHash = _e2e->emojiHashValue(); +} + +void GroupCall::setupConferenceCall() { + Expects(_conferenceCall != nullptr); + + _conferenceCall->staleParticipantIds( + ) | rpl::start_with_next([=](const base::flat_set &staleIds) { + removeConferenceParticipants(staleIds, true); + }, _lifetime); +} + +void GroupCall::trackParticipantsWithAccess() { + if (!_conferenceCall || !_e2e) { + return; + } + + _e2e->participantsSetValue( + ) | rpl::start_with_next([=](const TdE2E::ParticipantsSet &set) { + auto users = base::flat_set(); + users.reserve(set.list.size()); + for (const auto &id : set.list) { + users.emplace(UserId(id.v)); + } + _conferenceCall->setParticipantsWithAccess(std::move(users)); + }, _e2e->lifetime()); +} + +void GroupCall::removeConferenceParticipants( + const base::flat_set userIds, + bool removingStale) { + Expects(_e2e != nullptr); + Expects(!userIds.empty()); + + auto inputs = QVector(); + inputs.reserve(userIds.size()); + auto ids = base::flat_set(); + ids.reserve(userIds.size()); + for (const auto &id : userIds) { + inputs.push_back(MTP_long(peerToUser(id).bare)); + ids.emplace(TdE2E::MakeUserId(id)); + } + const auto block = _e2e->makeRemoveBlock(ids); + if (block.data.isEmpty()) { + return; + } + using Flag = MTPphone_DeleteConferenceCallParticipants::Flag; + _api.request(MTPphone_DeleteConferenceCallParticipants( + MTP_flags(removingStale ? Flag::f_only_left : Flag::f_kick), + inputCall(), + MTP_vector(std::move(inputs)), + MTP_bytes(block.data) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).fail([=](const MTP::Error &error) { + const auto type = error.type(); + if (type == u"GROUPCALL_FORBIDDEN"_q) { + LOG(("Call Info: " + "Rejoin after error '%1' in delete confcall participants" + ).arg(type)); + startRejoin(); + } else { + LOG(("Call Error: Could not remove confcall participants: %1" + ).arg(type)); + } + }).send(); +} + bool GroupCall::isSharingScreen() const { return _isSharingScreen.current(); } @@ -776,7 +952,7 @@ void GroupCall::setScheduledDate(TimeId date) { const auto was = _scheduleDate; _scheduleDate = date; if (was && !date) { - join(inputCall()); + initialJoin(); } } @@ -955,8 +1131,8 @@ void GroupCall::setState(State state) { if (state == State::Joined) { stopConnectingSound(); - if (const auto call = _peer->groupCall(); call && call->id() == _id) { - call->setInCall(); + if (const auto real = lookupReal()) { + real->setInCall(); } } @@ -1029,6 +1205,10 @@ bool GroupCall::rtmp() const { return _rtmp; } +bool GroupCall::conference() const { + return _conferenceCall || _startConferenceInfo; +} + bool GroupCall::listenersHidden() const { return _listenersHidden; } @@ -1055,10 +1235,17 @@ void GroupCall::setRtmpInfo(const Calls::Group::RtmpInfo &value) { } Data::GroupCall *GroupCall::lookupReal() const { + if (const auto conference = _conferenceCall.get()) { + return conference; + } const auto real = _peer->groupCall(); return (real && real->id() == _id) ? real : nullptr; } +std::shared_ptr GroupCall::conferenceCall() const { + return _conferenceCall; +} + rpl::producer> GroupCall::real() const { if (const auto real = lookupReal()) { return rpl::single(not_null{ real }); @@ -1066,6 +1253,10 @@ rpl::producer> GroupCall::real() const { return _realChanges.events(); } +rpl::producer GroupCall::emojiHashValue() const { + return _emojiHash.value(); +} + void GroupCall::start(TimeId scheduleDate, bool rtmp) { using Flag = MTPphone_CreateGroupCall::Flag; _createRequestId = _api.request(MTPphone_CreateGroupCall( @@ -1076,29 +1267,40 @@ void GroupCall::start(TimeId scheduleDate, bool rtmp) { MTPstring(), // title MTP_int(scheduleDate) )).done([=](const MTPUpdates &result) { + _createRequestId = 0; _reloadedStaleCall = true; _acceptFields = true; _peer->session().api().applyUpdates(result); _acceptFields = false; }).fail([=](const MTP::Error &error) { + _createRequestId = 0; LOG(("Call Error: Could not create, error: %1" ).arg(error.type())); hangup(); }).send(); } -void GroupCall::join(const MTPInputGroupCall &inputCall) { +void GroupCall::applyInputCall(const MTPInputGroupCall &inputCall) { inputCall.match([&](const MTPDinputGroupCall &data) { _id = data.vid().v; _accessHash = data.vaccess_hash().v; + }, [&](const auto &) { + Unexpected("slug/msg in GroupCall::join."); }); - setState(_scheduleDate ? State::Waiting : State::Joining); +} +void GroupCall::initialJoin() { + setState(_scheduleDate ? State::Waiting : State::Joining); if (_scheduleDate) { return; } rejoin(); + if (_id) { + initialJoinRequested(); + } +} +void GroupCall::initialJoinRequested() { using Update = Data::GroupCall::ParticipantUpdate; const auto real = lookupReal(); Assert(real != nullptr); @@ -1116,12 +1318,17 @@ void GroupCall::join(const MTPInputGroupCall &inputCall) { } }, _lifetime); + if (_conferenceCall) { + _canManage = _conferenceCall->canManage(); + return; + } _peer->session().updates().addActiveChat( _peerStream.events_starting_with_copy(_peer)); - SubscribeToMigration(_peer, _lifetime, [=](not_null group) { - _peer = group; + _canManage = Data::CanManageGroupCallValue(_peer); + SubscribeToMigration(_peer, _lifetime, [=](not_null peer) { + _peer = peer; _canManage = Data::CanManageGroupCallValue(_peer); - _peerStream.fire_copy(group); + _peerStream.fire_copy(peer); }); } @@ -1293,6 +1500,21 @@ void GroupCall::markTrackPaused(const VideoEndpoint &endpoint, bool paused) { : Webrtc::VideoState::Active); } +void GroupCall::startRejoin() { + if (_joinState.action != JoinAction::None || _createRequestId) { + // Don't reset _e2e in that case, if rejoin() is a no-op. + return; + } + for (const auto &[task, part] : _broadcastParts) { + _api.request(part.requestId).cancel(); + } + if (_conferenceCall || _startConferenceInfo) { + initConferenceE2E(); + } + setState(State::Joining); + rejoin(); +} + void GroupCall::rejoin() { rejoin(joinAs()); } @@ -1326,7 +1548,7 @@ void GroupCall::rejoin(not_null as) { && state() != State::Joined && state() != State::Connecting) { return; - } else if (_joinState.action != JoinAction::None) { + } else if (_joinState.action != JoinAction::None || _createRequestId) { return; } @@ -1350,89 +1572,315 @@ void GroupCall::rejoin(not_null as) { const auto weak = base::make_weak(&_instanceGuard); _instance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) { crl::on_main(weak, [=, payload = std::move(payload)] { - if (state() != State::Joining) { - _joinState.finish(); - checkNextJoinAction(); - return; - } - const auto ssrc = payload.audioSsrc; + _joinState.payload = { + .ssrc = payload.audioSsrc, + .json = QByteArray::fromStdString(payload.json), + }; LOG(("Call Info: Join payload received, joining with ssrc: %1." - ).arg(ssrc)); - - const auto json = QByteArray::fromStdString(payload.json); - const auto wasMuteState = muted(); - const auto wasVideoStopped = !isSharingCamera(); - using Flag = MTPphone_JoinGroupCall::Flag; - const auto flags = (wasMuteState != MuteState::Active - ? Flag::f_muted - : Flag(0)) - | (_joinHash.isEmpty() - ? Flag(0) - : Flag::f_invite_hash) - | (wasVideoStopped - ? Flag::f_video_stopped - : Flag(0)); - _api.request(MTPphone_JoinGroupCall( - MTP_flags(flags), - inputCall(), - joinAs()->input, - MTP_string(_joinHash), - MTPlong(), // key_fingerprint - MTP_dataJSON(MTP_bytes(json)) - )).done([=]( - const MTPUpdates &updates, - const MTP::Response &response) { - _serverTimeMs = TimestampInMsFromMsgId(response.outerMsgId); - _serverTimeMsGotAt = crl::now(); - - _joinState.finish(ssrc); - _mySsrcs.emplace(ssrc); - - setState((_instanceState.current() - == InstanceState::Disconnected) - ? State::Connecting - : State::Joined); - applyMeInCallLocally(); - maybeSendMutedUpdate(wasMuteState); - _peer->session().api().applyUpdates(updates); - applyQueuedSelfUpdates(); - checkFirstTimeJoined(); - _screenJoinState.nextActionPending = true; - checkNextJoinAction(); - if (wasVideoStopped == isSharingCamera()) { - sendSelfUpdate(SendUpdateType::CameraStopped); - } - if (isCameraPaused()) { - sendSelfUpdate(SendUpdateType::CameraPaused); - } - sendPendingSelfUpdates(); - if (!_reloadedStaleCall - && _state.current() != State::Joining) { - if (const auto real = lookupReal()) { - _reloadedStaleCall = true; - real->reloadIfStale(); - } - } - }).fail([=](const MTP::Error &error) { - _joinState.finish(); - - const auto type = error.type(); - LOG(("Call Error: Could not join, error: %1").arg(type)); - - if (type == u"GROUPCALL_SSRC_DUPLICATE_MUCH") { - rejoin(); - return; - } - - hangup(); - Ui::Toast::Show((type == u"GROUPCALL_FORBIDDEN"_q) - ? tr::lng_group_not_accessible(tr::now) - : Lang::Hard::ServerError()); - }).send(); + ).arg(_joinState.payload.ssrc)); + if (!_conferenceCall && _startConferenceInfo) { + startConference(); + } else if (_conferenceCall + && !_conferenceCall->blockchainMayBeEmpty() + && !_e2e->hasLastBlock0()) { + refreshLastBlockAndJoin(); + } else { + sendJoinRequest(); + } }); }); } +void GroupCall::sendJoinRequest() { + if (state() != State::Joining) { + _joinState.finish(); + checkNextJoinAction(); + return; + } + const auto joinBlock = _e2e ? _e2e->makeJoinBlock().data : QByteArray(); + if (_e2e && joinBlock.isEmpty()) { + _joinState.finish(); + LOG(("Call Error: Could not generate join block.")); + hangup(); + Ui::Toast::Show(u"Could not generate join block."_q); + return; + } + const auto wasMuteState = muted(); + const auto wasVideoStopped = !isSharingCamera(); + using Flag = MTPphone_JoinGroupCall::Flag; + const auto flags = (wasMuteState != MuteState::Active + ? Flag::f_muted + : Flag(0)) + | (_joinHash.isEmpty() ? Flag(0) : Flag::f_invite_hash) + | (wasVideoStopped ? Flag::f_video_stopped : Flag(0)) + | (_e2e ? (Flag::f_public_key | Flag::f_block) : Flag()); + _api.request(MTPphone_JoinGroupCall( + MTP_flags(flags), + inputCallSafe(), + joinAs()->input, + MTP_string(_joinHash), + (_e2e ? TdE2E::PublicKeyToMTP(_e2e->myKey()) : MTPint256()), + MTP_bytes(joinBlock), + MTP_dataJSON(MTP_bytes(_joinState.payload.json)) + )).done([=]( + const MTPUpdates &result, + const MTP::Response &response) { + joinDone( + TimestampInMsFromMsgId(response.outerMsgId), + result, + wasMuteState, + wasVideoStopped); + }).fail([=](const MTP::Error &error) { + joinFail(error.type()); + }).send(); +} + +void GroupCall::refreshLastBlockAndJoin() { + Expects(_e2e != nullptr); + + if (state() != State::Joining) { + _joinState.finish(); + checkNextJoinAction(); + return; + } + _api.request(MTPphone_GetGroupCallChainBlocks( + inputCallSafe(), + MTP_int(0), + MTP_int(-1), + MTP_int(1) + )).done([=](const MTPUpdates &result) { + if (result.type() != mtpc_updates) { + _joinState.finish(); + LOG(("Call Error: Bad result in GroupCallChainBlocks.")); + hangup(); + Ui::Toast::Show(u"Bad Updates in GroupCallChainBlocks."_q); + return; + } + _e2e->refreshLastBlock0({}); + const auto &data = result.c_updates(); + for (const auto &update : data.vupdates().v) { + if (update.type() != mtpc_updateGroupCallChainBlocks) { + continue; + } + const auto &data = update.c_updateGroupCallChainBlocks(); + const auto &blocks = data.vblocks().v; + if (!blocks.isEmpty()) { + _e2e->refreshLastBlock0(TdE2E::Block{ blocks.back().v }); + break; + } + } + sendJoinRequest(); + }).fail([=](const MTP::Error &error) { + _joinState.finish(); + const auto &type = error.type(); + LOG(("Call Error: Could not get last block, error: %1").arg(type)); + hangup(); + Ui::Toast::Show(error.type()); + }).send(); +} + +void GroupCall::startConference() { + Expects(_e2e != nullptr && _startConferenceInfo != nullptr); + + const auto joinBlock = _e2e->makeJoinBlock().data; + Assert(!joinBlock.isEmpty()); + + const auto wasMuteState = muted(); + const auto wasVideoStopped = !isSharingCamera(); + using Flag = MTPphone_CreateConferenceCall::Flag; + const auto flags = Flag::f_join + | Flag::f_public_key + | Flag::f_block + | Flag::f_params + | ((wasMuteState != MuteState::Active) ? Flag::f_muted : Flag(0)) + | (wasVideoStopped ? Flag::f_video_stopped : Flag(0)); + _createRequestId = _api.request(MTPphone_CreateConferenceCall( + MTP_flags(flags), + MTP_int(base::RandomValue()), + TdE2E::PublicKeyToMTP(_e2e->myKey()), + MTP_bytes(joinBlock), + MTP_dataJSON(MTP_bytes(_joinState.payload.json)) + )).done([=]( + const MTPUpdates &result, + const MTP::Response &response) { + _createRequestId = 0; + _conferenceCall = _peer->owner().sharedConferenceCallFind(result); + if (!_conferenceCall) { + joinFail(u"Call not found!"_q); + return; + } + applyInputCall(_conferenceCall->input()); + initialJoinRequested(); + joinDone( + TimestampInMsFromMsgId(response.outerMsgId), + result, + wasMuteState, + wasVideoStopped, + true); + }).fail([=](const MTP::Error &error) { + _createRequestId = 0; + LOG(("Call Error: Could not create, error: %1" + ).arg(error.type())); + hangup(); + }).send(); +} + +void GroupCall::joinDone( + int64 serverTimeMs, + const MTPUpdates &result, + MuteState wasMuteState, + bool wasVideoStopped, + bool justCreated) { + Expects(!justCreated || _startConferenceInfo != nullptr); + + _serverTimeMs = serverTimeMs; + _serverTimeMsGotAt = crl::now(); + + _joinState.finish(_joinState.payload.ssrc); + _mySsrcs.emplace(_joinState.ssrc); + + setState((_instanceState.current() + == InstanceState::Disconnected) + ? State::Connecting + : State::Joined); + applyMeInCallLocally(); + maybeSendMutedUpdate(wasMuteState); + + for (auto &state : _subchains) { + // Accept initial join blocks. + _api.request(base::take(state.requestId)).cancel(); + state.inShortPoll = true; + } + _peer->session().api().applyUpdates(result); + for (auto &state : _subchains) { + state.inShortPoll = false; + } + + if (justCreated) { + subscribeToReal(_conferenceCall.get()); + setupConferenceCall(); + _conferenceLinkSlug = Group::ExtractConferenceSlug( + _conferenceCall->conferenceInviteLink()); + Core::App().calls().startedConferenceReady( + this, + *_startConferenceInfo); + } + + trackParticipantsWithAccess(); + applyQueuedSelfUpdates(); + checkFirstTimeJoined(); + _screenJoinState.nextActionPending = true; + checkNextJoinAction(); + if (wasVideoStopped == isSharingCamera()) { + sendSelfUpdate(SendUpdateType::CameraStopped); + } + if (isCameraPaused()) { + sendSelfUpdate(SendUpdateType::CameraPaused); + } + sendPendingSelfUpdates(); + if (!_reloadedStaleCall + && _state.current() != State::Joining) { + if (const auto real = lookupReal()) { + _reloadedStaleCall = true; + real->reloadIfStale(); + } + } + if (_e2e) { + _e2e->joined(); + if (!_pendingOutboundBlock.isEmpty()) { + sendOutboundBlock(base::take(_pendingOutboundBlock)); + } + } + if (const auto once = base::take(_startConferenceInfo)) { + processConferenceStart(*once); + } + for (const auto &callback : base::take(_rejoinedCallbacks)) { + callback(); + } +} + +void GroupCall::joinFail(const QString &error) { + if (_e2e) { + if (error.startsWith(u"CONF_WRITE_CHAIN_INVALID"_q)) { + if (_id) { + refreshLastBlockAndJoin(); + } else { + hangup(); + } + return; + } + } + _joinState.finish(); + LOG(("Call Error: Could not join, error: %1").arg(error)); + + if (_id && error == u"GROUPCALL_SSRC_DUPLICATE_MUCH") { + rejoin(); + return; + } + + hangup(); + Ui::Toast::Show((error == u"GROUPCALL_FORBIDDEN"_q + || error == u"GROUPCALL_INVALID"_q) + ? tr::lng_confcall_not_accessible(tr::now) + : error); +} + +void GroupCall::requestSubchainBlocks(int subchain, int height) { + Expects(subchain >= 0 && subchain < kSubChainsCount); + + auto &state = _subchains[subchain]; + _api.request(base::take(state.requestId)).cancel(); + state.requestId = _api.request(MTPphone_GetGroupCallChainBlocks( + inputCall(), + MTP_int(subchain), + MTP_int(height), + MTP_int(kShortPollChainBlocksPerRequest) + )).done([=](const MTPUpdates &result) { + auto &state = _subchains[subchain]; + state.requestId = 0; + state.inShortPoll = true; + _peer->session().api().applyUpdates(result); + state.inShortPoll = false; + for (const auto &data : base::take(state.pending)) { + applySubChainUpdate(subchain, data.blocks, data.next); + } + _e2e->subchainBlocksRequestFinished(subchain); + }).fail([=](const MTP::Error &error) { + auto &state = _subchains[subchain]; + state.requestId = 0; + _e2e->subchainBlocksRequestFinished(subchain); + if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { + LOG(("Call Info: Rejoin after error '%1' in get chain blocks." + ).arg(error.type())); + startRejoin(); + } + }).send(); +} + +void GroupCall::sendOutboundBlock(QByteArray block) { + _pendingOutboundBlock = QByteArray(); + _api.request(MTPphone_SendConferenceCallBroadcast( + inputCall(), + MTP_bytes(block) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).fail([=](const MTP::Error &error) { + const auto type = error.type(); + if (type == u"GROUPCALL_FORBIDDEN"_q) { + _pendingOutboundBlock = block; + LOG(("Call Info: Rejoin after error '%1' in send confcall block." + ).arg(type)); + startRejoin(); + } else if (type == u"BLOCK_INVALID"_q + || type.startsWith(u"CONF_WRITE_CHAIN_INVALID"_q)) { + LOG(("Call Error: Could not broadcast block: %1").arg(type)); + } else { + LOG(("Call Error: Got '%1' in send confcall block.").arg(type)); + sendOutboundBlock(block); + } + }).send(); +} + void GroupCall::checkNextJoinAction() { if (_joinState.action != JoinAction::None) { return; @@ -1615,7 +2063,7 @@ void GroupCall::applyParticipantLocally( not_null participantPeer, bool mute, std::optional volume) { - const auto participant = LookupParticipant(_peer, _id, participantPeer); + const auto participant = LookupParticipant(this, participantPeer); if (!participant || !participant->ssrc) { return; } @@ -1639,7 +2087,7 @@ void GroupCall::applyParticipantLocally( | (participant->raisedHandRating ? Flag::f_raise_hand_rating : Flag(0)); - _peer->groupCall()->applyLocalUpdate( + lookupReal()->applyLocalUpdate( MTP_updateGroupCallParticipants( inputCall(), MTP_vector( @@ -1860,7 +2308,8 @@ void GroupCall::handlePossibleCreateOrJoinResponse( } else { Unexpected("Peer type in GroupCall::join."); } - join(input); + applyInputCall(input); + initialJoin(); } return; } else if (_id != data.vid().v || !_instance) { @@ -1953,6 +2402,8 @@ void GroupCall::handleUpdate(const MTPUpdate &update) { handleUpdate(data); }, [&](const MTPDupdateGroupCallParticipants &data) { handleUpdate(data); + }, [&](const MTPDupdateGroupCallChainBlocks &data) { + handleUpdate(data); }, [](const auto &) { Unexpected("Type in Instance::applyGroupCallUpdateChecked."); }); @@ -1966,8 +2417,11 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCall &data) { } void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) { - const auto callId = data.vcall().match([](const auto &data) { + const auto callId = data.vcall().match([]( + const MTPDinputGroupCall &data) { return data.vid().v; + }, [](const auto &) -> CallId { + Unexpected("slug/msg in GroupCall::handleUpdate."); }); if (_id != callId) { return; @@ -1991,6 +2445,46 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) { } } +void GroupCall::handleUpdate(const MTPDupdateGroupCallChainBlocks &data) { + const auto callId = data.vcall().match([]( + const MTPDinputGroupCall &data) { + return data.vid().v; + }, [](const auto &) -> CallId { + Unexpected("slug/msg in GroupCall::handleUpdate."); + }); + if (_id != callId || !_e2e) { + return; + } + const auto subchain = data.vsub_chain_id().v; + if (subchain < 0 || subchain >= kSubChainsCount) { + return; + } + auto &entry = _subchains[subchain]; + const auto &blocks = data.vblocks().v; + const auto next = data.vnext_offset().v; + if (entry.requestId) { + Assert(!entry.inShortPoll); + entry.pending.push_back({ blocks, next }); + } else { + applySubChainUpdate(subchain, blocks, next); + } +} + +void GroupCall::applySubChainUpdate( + int subchain, + const QVector &blocks, + int next) { + Expects(subchain >= 0 && subchain < kSubChainsCount); + + auto &entry = _subchains[subchain]; + auto raw = std::vector(); + raw.reserve(blocks.size()); + for (const auto &block : blocks) { + raw.push_back({ block.v }); + } + _e2e->apply(subchain, next, raw, entry.inShortPoll); +} + void GroupCall::applyQueuedSelfUpdates() { const auto weak = base::make_weak(this); while (weak @@ -2011,8 +2505,7 @@ void GroupCall::applySelfUpdate(const MTPDgroupCallParticipant &data) { // I was removed from the call, rejoin. LOG(("Call Info: " "Rejoin after got 'left' with my ssrc.")); - setState(State::Joining); - rejoin(); + startRejoin(); } return; } else if (data.vsource().v != _joinState.ssrc) { @@ -2039,8 +2532,7 @@ void GroupCall::applySelfUpdate(const MTPDgroupCallParticipant &data) { : MuteState::ForceMuted); } else if (_instanceMode == InstanceMode::Stream) { LOG(("Call Info: Rejoin after unforcemute in stream mode.")); - setState(State::Joining); - rejoin(); + startRejoin(); } else if (mutedByAdmin()) { setMuted(MuteState::Muted); if (!_instanceTransitioning) { @@ -2059,7 +2551,7 @@ void GroupCall::applyOtherParticipantUpdate( } const auto participantPeer = _peer->owner().peer( peerFromMTP(data.vpeer())); - if (!LookupParticipant(_peer, _id, participantPeer)) { + if (!LookupParticipant(this, participantPeer)) { return; } _otherParticipantStateValue.fire(Group::ParticipantState{ @@ -2370,6 +2862,15 @@ void GroupCall::toggleRecording( }).send(); } +auto GroupCall::lookupVideoCodecPreferences() const +-> std::vector { + auto result = std::vector(); + if (_peer->session().appConfig().confcallPrioritizeVP8()) { + result.push_back(tgcalls::VideoCodecName::VP8); + } + return result; +} + bool GroupCall::tryCreateController() { if (_instance) { return false; @@ -2476,6 +2977,7 @@ bool GroupCall::tryCreateController() { .videoContentType = tgcalls::VideoContentType::Generic, .initialEnableNoiseSuppression = settings.groupCallNoiseSuppression(), + .videoCodecPreferences = lookupVideoCodecPreferences(), .requestMediaChannelDescriptions = [=, call = base::make_weak(this)]( const std::vector &ssrcs, std::functioncallback() + : nullptr), }; if (Logs::DebugEnabled()) { auto callLogFolder = cWorkingDir() + u"DebugLogs"_q; @@ -2540,6 +3045,10 @@ bool GroupCall::tryCreateScreencast() { .createAudioDeviceModule = Webrtc::LoopbackAudioDeviceModuleCreator(), .videoCapture = _screenCapture, .videoContentType = tgcalls::VideoContentType::Screencast, + .videoCodecPreferences = lookupVideoCodecPreferences(), + .e2eEncryptDecrypt = (_e2eEncryptDecrypt + ? _e2eEncryptDecrypt->callback() + : nullptr), }; LOG(("Call Info: Creating group screen instance")); @@ -2605,11 +3114,7 @@ void GroupCall::broadcastPartStart(std::shared_ptr task) { }).fail([=](const MTP::Error &error, const MTP::Response &response) { if (error.type() == u"GROUPCALL_JOIN_MISSING"_q || error.type() == u"GROUPCALL_FORBIDDEN"_q) { - for (const auto &[task, part] : _broadcastParts) { - _api.request(part.requestId).cancel(); - } - setState(State::Joining); - rejoin(); + startRejoin(); return; } const auto status = (MTP::IsFloodError(error) @@ -2673,6 +3178,7 @@ bool GroupCall::mediaChannelDescriptionsFill( add(Channel{ .type = Channel::Type::Audio, .audioSsrc = ssrc, + .userId = int64_t(peerToUser(byAudio->id).bare), }); } else if (!resolved) { _unresolvedSsrcs.emplace(ssrc); @@ -2728,11 +3234,7 @@ void GroupCall::requestCurrentTimeStart( if (error.type() == u"GROUPCALL_JOIN_MISSING"_q || error.type() == u"GROUPCALL_FORBIDDEN"_q) { - for (const auto &[task, part] : _broadcastParts) { - _api.request(part.requestId).cancel(); - } - setState(State::Joining); - rejoin(); + startRejoin(); } }).handleAllErrors().toDC( MTP::groupCallStreamDcId(_broadcastDcId) @@ -2809,6 +3311,7 @@ void GroupCall::updateRequestedVideoChannels() { } channels.push_back({ .audioSsrc = participant->ssrc, + .userId = int64_t(peerToUser(participant->peer->id).bare), .endpointId = endpointId, .ssrcGroups = (params->camera.endpointId == endpointId ? params->camera.ssrcGroups @@ -3138,7 +3641,7 @@ void GroupCall::checkJoined() { }).fail([=](const MTP::Error &error) { LOG(("Call Info: Full rejoin after error '%1' in checkGroupCall." ).arg(error.type())); - rejoin(); + startRejoin(); }).send(); } @@ -3313,7 +3816,7 @@ void GroupCall::sendSelfUpdate(SendUpdateType type) { if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." ).arg(error.type())); - rejoin(); + startRejoin(); } }).send(); } @@ -3382,7 +3885,7 @@ void GroupCall::editParticipant( not_null participantPeer, bool mute, std::optional volume) { - const auto participant = LookupParticipant(_peer, _id, participantPeer); + const auto participant = LookupParticipant(this, participantPeer); if (!participant) { return; } @@ -3407,50 +3910,118 @@ void GroupCall::editParticipant( if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." ).arg(error.type())); - rejoin(); + startRejoin(); } }).send(); } -std::variant> GroupCall::inviteUsers( - const std::vector> &users) { +void GroupCall::inviteToConference( + InviteRequest request, + Fn()> resultAddress, + Fn finishRequest) { + using Flag = MTPphone_InviteConferenceCallParticipant::Flag; + const auto user = request.user; + _api.request(MTPphone_InviteConferenceCallParticipant( + MTP_flags(request.video ? Flag::f_video : Flag()), + inputCall(), + user->inputUser + )).done([=](const MTPUpdates &result) { + const auto call = _conferenceCall.get(); + user->owner().registerInvitedToCallUser(_id, call, user, true); + _peer->session().api().applyUpdates(result); + resultAddress()->invited.push_back(user); + finishRequest(); + }).fail([=](const MTP::Error &error) { + const auto result = resultAddress(); + const auto type = error.type(); + if (type == u"USER_PRIVACY_RESTRICTED"_q) { + result->privacyRestricted.push_back(user); + } else if (type == u"USER_ALREADY_PARTICIPANT"_q) { + result->alreadyIn.push_back(user); + } else if (type == u"USER_WAS_KICKED"_q) { + result->kicked.push_back(user); + } else if (type == u"GROUPCALL_FORBIDDEN"_q) { + startRejoin(); + _rejoinedCallbacks.push_back([=] { + inviteToConference(request, resultAddress, finishRequest); + }); + return; + } else { + result->failed.push_back(user); + } + finishRequest(); + }).send(); +} + +void GroupCall::inviteUsers( + const std::vector &requests, + Fn done) { const auto real = lookupReal(); if (!real) { - return 0; + if (done) { + done({}); + } + return; } const auto owner = &_peer->owner(); - auto count = 0; + struct State { + InviteResult result; + int requests = 0; + }; + const auto state = std::make_shared(); + const auto finishRequest = [=] { + if (!--state->requests) { + if (done) { + done(std::move(state->result)); + } + } + }; + + if (const auto call = _conferenceCall.get()) { + for (const auto &request : requests) { + inviteToConference(request, [=] { + return &state->result; + }, finishRequest); + ++state->requests; + } + return; + } + + auto usersSlice = std::vector>(); + usersSlice.reserve(kMaxInvitePerSlice); auto slice = QVector(); - auto result = std::variant>(0); slice.reserve(kMaxInvitePerSlice); const auto sendSlice = [&] { - count += slice.size(); _api.request(MTPphone_InviteToGroupCall( inputCall(), MTP_vector(slice) )).done([=](const MTPUpdates &result) { _peer->session().api().applyUpdates(result); + for (const auto &user : usersSlice) { + state->result.invited.push_back(user); + } + finishRequest(); + }).fail([=](const MTP::Error &error) { + finishRequest(); }).send(); + ++state->requests; + slice.clear(); + usersSlice.clear(); }; - for (const auto &user : users) { - if (!count && slice.empty()) { - result = user; - } - owner->registerInvitedToCallUser(_id, _peer, user); + for (const auto &request : requests) { + const auto user = request.user; + owner->registerInvitedToCallUser(_id, _peer, user, false); + usersSlice.push_back(user); slice.push_back(user->inputUser); if (slice.size() == kMaxInvitePerSlice) { sendSlice(); } } - if (count != 0 || slice.size() != 1) { - result = int(count + slice.size()); - } if (!slice.empty()) { sendSlice(); } - return result; } auto GroupCall::ensureGlobalShortcutManager() @@ -3528,9 +4099,16 @@ auto GroupCall::otherParticipantStateValue() const MTPInputGroupCall GroupCall::inputCall() const { Expects(_id != 0); - return MTP_inputGroupCall( - MTP_long(_id), - MTP_long(_accessHash)); + return MTP_inputGroupCall(MTP_long(_id), MTP_long(_accessHash)); +} + +MTPInputGroupCall GroupCall::inputCallSafe() const { + const auto inviteMsgId = _conferenceJoinMessageId.bare; + return inviteMsgId + ? MTP_inputGroupCallInviteMessage(MTP_int(inviteMsgId)) + : _conferenceLinkSlug.isEmpty() + ? inputCall() + : MTP_inputGroupCallSlug(MTP_string(_conferenceLinkSlug)); } void GroupCall::destroyController() { @@ -3538,7 +4116,7 @@ void GroupCall::destroyController() { DEBUG_LOG(("Call Info: Destroying call controller..")); invalidate_weak_ptrs(&_instanceGuard); - _instance->stop(); + _instance->stop(nullptr); crl::async([ instance = base::take(_instance), done = _delegate->groupCallAddAsyncWaiter() @@ -3555,7 +4133,7 @@ void GroupCall::destroyScreencast() { DEBUG_LOG(("Call Info: Destroying call screen controller..")); invalidate_weak_ptrs(&_screenInstanceGuard); - _screenInstance->stop(); + _screenInstance->stop(nullptr); crl::async([ instance = base::take(_screenInstance), done = _delegate->groupCallAddAsyncWaiter() @@ -3567,4 +4145,87 @@ void GroupCall::destroyScreencast() { } } +TextWithEntities ComposeInviteResultToast( + const InviteResult &result) { + auto text = TextWithEntities(); + const auto append = [&](TextWithEntities part) { + if (!text.empty()) { + text.append(u"\n\n"_q); + } + text.append(part); + }; + + const auto invited = int(result.invited.size()); + const auto already = int(result.alreadyIn.size()); + const auto restricted = int(result.privacyRestricted.size()); + const auto kicked = int(result.kicked.size()); + const auto failed = int(result.failed.size()); + if (invited == 1) { + append(tr::lng_confcall_invite_done_user( + tr::now, + lt_user, + Ui::Text::Bold(result.invited.front()->shortName()), + Ui::Text::RichLangValue)); + } else if (invited > 1) { + append(tr::lng_confcall_invite_done_many( + tr::now, + lt_count, + invited, + Ui::Text::RichLangValue)); + } + if (already == 1) { + append(tr::lng_confcall_invite_already_user( + tr::now, + lt_user, + Ui::Text::Bold(result.alreadyIn.front()->shortName()), + Ui::Text::RichLangValue)); + } else if (already > 1) { + append(tr::lng_confcall_invite_already_many( + tr::now, + lt_count, + already, + Ui::Text::RichLangValue)); + } + if (restricted == 1) { + append(tr::lng_confcall_invite_fail_user( + tr::now, + lt_user, + Ui::Text::Bold(result.privacyRestricted.front()->shortName()), + Ui::Text::RichLangValue)); + } else if (restricted > 1) { + append(tr::lng_confcall_invite_fail_many( + tr::now, + lt_count, + restricted, + Ui::Text::RichLangValue)); + } + if (kicked == 1) { + append(tr::lng_confcall_invite_kicked_user( + tr::now, + lt_user, + Ui::Text::Bold(result.kicked.front()->shortName()), + Ui::Text::RichLangValue)); + } else if (kicked > 1) { + append(tr::lng_confcall_invite_kicked_many( + tr::now, + lt_count, + kicked, + Ui::Text::RichLangValue)); + } + if (failed == 1) { + append(tr::lng_confcall_invite_fail_user( + tr::now, + lt_user, + Ui::Text::Bold(result.failed.front()->shortName()), + Ui::Text::RichLangValue)); + } else if (failed > 1) { + append(tr::lng_confcall_invite_fail_many( + tr::now, + lt_count, + failed, + Ui::Text::RichLangValue)); + } + return text; +} + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index 8f93793bd9..e727173cd3 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -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, Group::JoinInfo info, const MTPInputGroupCall &inputCall); + GroupCall(not_null 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 emptyRtmpValue() const; @@ -247,14 +260,19 @@ public: void setRtmpInfo(const Group::RtmpInfo &value); [[nodiscard]] Data::GroupCall *lookupReal() const; + [[nodiscard]] std::shared_ptr conferenceCall() const; [[nodiscard]] rpl::producer> real() const; + [[nodiscard]] rpl::producer 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 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 errors() const { return _errors.events(); } @@ -404,8 +433,10 @@ public: void toggleMute(const Group::MuteRequest &data); void changeVolume(const Group::VolumeRequest &data); - std::variant> inviteUsers( - const std::vector> &users); + + void inviteUsers( + const std::vector &requests, + Fn done); std::shared_ptr 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 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 blocks; + int next = 0; + }; + struct SubChainState { + std::vector pending; + mtpRequestId requestId = 0; + bool inShortPoll = false; + }; friend inline constexpr bool is_flag_type(SendUpdateType) { return true; } + GroupCall( + not_null delegate, + Group::JoinInfo join, + StartConferenceInfo conference, + const MTPInputGroupCall &inputCall); + void broadcastPartStart(std::shared_ptr task); void broadcastPartCancel(not_null 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 &blocks, + int next); + [[nodiscard]] auto lookupVideoCodecPreferences() const + -> std::vector; bool tryCreateController(); void destroyController(); bool tryCreateScreencast(); @@ -508,6 +567,7 @@ private: const std::optional &was, const Data::GroupCallParticipant &now); void applyMeInCallLocally(); + void startRejoin(); void rejoin(); void leave(); void rejoin(not_null 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()> resultAddress, + Fn finishRequest); + [[nodiscard]] int activeVideoSendersCount() const; [[nodiscard]] MTPInputGroupCall inputCall() const; + [[nodiscard]] MTPInputGroupCall inputCallSafe() const; const not_null _delegate; + std::shared_ptr _conferenceCall; + std::unique_ptr _e2e; + std::shared_ptr _e2eEncryptDecrypt; + rpl::variable _emojiHash; + QByteArray _pendingOutboundBlock; + std::shared_ptr _startConferenceInfo; + not_null _peer; // Can change in legacy group migration. rpl::event_stream _peerStream; not_null _history; // Can change in legacy group migration. @@ -583,6 +664,7 @@ private: rpl::variable _state = State::Creating; base::flat_set _unresolvedSsrcs; rpl::event_stream _errors; + std::vector> _rejoinedCallbacks; bool _recordingStoppedByMe = false; bool _requestedVideoChannelsUpdateScheduled = false; @@ -601,6 +683,8 @@ private: rpl::variable> _joinAs; std::vector> _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 diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.cpp b/Telegram/SourceFiles/calls/group/calls_group_common.cpp index 487629893f..fab26cc837 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_common.cpp @@ -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 +#include namespace Calls::Group { @@ -50,4 +77,422 @@ object_ptr ScreenSharingPrivacyRequestBox() { #endif // Q_OS_MAC } +object_ptr MakeJoinCallLogo(not_null parent) { + const auto logoSize = st::confcallJoinLogo.size(); + const auto logoOuter = logoSize.grownBy(st::confcallJoinLogoPadding); + auto result = object_ptr(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 box, + std::shared_ptr call, + UserData *maybeInviter, + Fn 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>( + box, + object_ptr( + box, + tr::lng_confcall_join_title(), + st::boxTitle)), + st::boxRowPadding + st::confcallLinkTitlePadding); + const auto wrapName = [&](not_null peer) { + return rpl::single(Ui::Text::Bold(peer->shortName())); + }; + box->addRow( + object_ptr( + 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(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>(); + 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( + 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( + DarkShareBoxStyle()), + }; +} + +void ShowConferenceCallLinkBox( + std::shared_ptr show, + std::shared_ptr call, + const ConferenceCallLinkArgs &args) { + const auto st = args.st; + const auto initial = args.initial; + const auto link = call->conferenceInviteLink(); + show->showBox(Box([=](not_null box) { + struct State { + base::unique_qptr menu; + bool resetting = false; + }; + const auto state = box->lifetime().make_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( + 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( + 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>( + box, + object_ptr( + box, + tr::lng_confcall_link_title(), + st.box ? st.box->title : st::boxTitle)), + st::boxRowPadding + st::confcallLinkTitlePadding); + box->addRow( + object_ptr( + 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( + 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( + 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()), + 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 diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.h b/Telegram/SourceFiles/calls/group/calls_group_common.h index 670378aa74..7391cc191b 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.h +++ b/Telegram/SourceFiles/calls/group/calls_group_common.h @@ -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 user; + bool video = false; +}; + +struct InviteResult { + std::vector> invited; + std::vector> alreadyIn; + std::vector> privacyRestricted; + std::vector> kicked; + std::vector> failed; +}; + +struct StartConferenceInfo { + std::shared_ptr show; + std::shared_ptr call; + QString linkSlug; + MsgId joinMessageId; + std::vector invite; + bool sharingLink = false; + bool migrating = false; + bool muted = false; + std::shared_ptr videoCapture; + QString videoCaptureScreenId; +}; + +struct ConferencePanelMigration { + std::shared_ptr window; +}; + +} // namespace Calls + namespace Calls::Group { constexpr auto kDefaultVolume = 10000; @@ -93,4 +161,44 @@ using StickedTooltips = base::flags; [[nodiscard]] object_ptr ScreenSharingPrivacyRequestBox(); +[[nodiscard]] object_ptr MakeJoinCallLogo( + not_null parent); + +void ConferenceCallJoinConfirm( + not_null box, + std::shared_ptr call, + UserData *maybeInviter, + Fn 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 shareBox; +}; +[[nodiscard]] ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle(); + +struct ConferenceCallLinkArgs { + ConferenceCallLinkStyleOverrides st; + bool initial = false; +}; +void ShowConferenceCallLinkBox( + std::shared_ptr show, + std::shared_ptr call, + const ConferenceCallLinkArgs &args); + +struct ConferenceFactoryArgs { + std::shared_ptr show; + Fn finished; + bool joining = false; + StartConferenceInfo info; +}; +void MakeConferenceCall(ConferenceFactoryArgs &&args); + +[[nodiscard]] QString ExtractConferenceSlug(const QString &link); + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index bb8c908559..cd04eb32fa 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -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 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 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 _videoRipple; + std::unique_ptr _audioRipple; + bool _alreadyIn = false; + bool _video = false; + +}; + +struct PrioritizedSelector { + object_ptr content = { nullptr }; + Fn init; + Fn overrideKey; + Fn deselect; + Fn activate; + rpl::producer scrollToRequests; +}; + +class ConfInviteController final : public ContactsBoxController { +public: + ConfInviteController( + not_null session, + ConfInviteStyles st, + base::flat_set> alreadyIn, + Fn shareLink, + std::vector> prioritize); + + [[nodiscard]] rpl::producer hasSelectedValue() const; + [[nodiscard]] std::vector requests( + const std::vector> &peers) const; + + void noSearchSubmit(); + [[nodiscard]] auto prioritizeScrollRequests() const + -> rpl::producer; + +protected: + void prepareViewHook() override; + + std::unique_ptr createRow( + not_null user) override; + + void rowClicked(not_null row) override; + void rowElementClicked(not_null 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 row, bool video); + [[nodiscard]] bool toggleRowGetChecked( + not_null row, + bool video); + void addShareLinkButton(); + void addPriorityInvites(); + + const ConfInviteStyles _st; + const base::flat_set> _alreadyIn; + const std::vector> _prioritize; + const Fn _shareLink; + PrioritizedSelector _prioritizeRows; + rpl::event_stream _prioritizeScrollRequests; + base::flat_set> _skip; + rpl::variable _hasSelected; + base::flat_set> _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 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 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( + 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> users, + Fn, bool, anim::type)> toggleGetChecked, + Fn lastSelectWithVideo, + Fn setLastSelectWithVideo) { + class PrioritizedController final : public PeerListController { + public: + PrioritizedController( + const ConfInviteStyles &st, + std::vector> users, + Fn, + bool, + anim::type)> toggleGetChecked, + Fn lastSelectWithVideo, + Fn 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(user, _st)); + } + delegate()->peerListRefreshRows(); + } + void loadMoreRows() override { + } + void rowClicked(not_null row) override { + toggleRowSelected(row, _lastSelectWithVideo()); + } + void rowElementClicked( + not_null row, + int element) override { + if (row->checked()) { + static_cast(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 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> _users; + Fn, bool, anim::type)> _toggleGetChecked; + Fn _lastSelectWithVideo; + Fn _setLastSelectWithVideo; + + }; + + auto result = object_ptr((QWidget*)nullptr); + const auto container = result.data(); + + const auto delegate = container->lifetime().make_state< + PeerListContentDelegateSimple + >(); + const auto controller = container->lifetime( + ).make_state( + st, + users, + toggleGetChecked, + lastSelectWithVideo, + setLastSelectWithVideo); + controller->setStyleOverrides(&st::createCallList); + const auto content = container->add(object_ptr( + 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 session, + ConfInviteStyles st, + base::flat_set> alreadyIn, + Fn shareLink, + std::vector> 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 ConfInviteController::hasSelectedValue() const { + return _hasSelected.value(); +} + +std::vector ConfInviteController::requests( + const std::vector> &peers) const { + auto result = std::vector(); + 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 ConfInviteController::createRow( + not_null user) { + if (user->isSelf() + || user->isBot() + || user->isServiceUser() + || user->isInaccessible() + || _skip.contains(user)) { + return nullptr; + } + auto result = std::make_unique(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 row) { + toggleRowSelected(row, _lastSelectWithVideo); +} + +void ConfInviteController::rowElementClicked( + not_null row, + int element) { + if (row->checked()) { + static_cast(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 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 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(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 { + return _prioritizeScrollRequests.events(); +} + +void ConfInviteController::prepareViewHook() { + if (_shareLink) { + addShareLinkButton(); + } else if (!_prioritize.empty()) { + addPriorityInvites(); + } +} + +void ConfInviteController::addPriorityInvites() { + const auto toggleGetChecked = [=]( + not_null 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>()); + } + delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content)); +} + +void ConfInviteController::addShareLinkButton() { + auto button = object_ptr>( + nullptr, + object_ptr( + 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( + 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 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 InviteContactsController::createRow( object_ptr PrepareInviteBox( not_null call, - Fn showToast) { + Fn showToast, + Fn 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>(); + 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>(); + const auto shareLink = [=] { + Expects(shareConferenceLink != nullptr); + + shareConferenceLink(); + (*close)(); + }; + auto controller = std::make_unique( + &real->session(), + ConfInviteDarkStyles(), + alreadyIn, + shareLink, + std::vector>()); + const auto raw = controller.get(); + raw->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + auto initBox = [=](not_null 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(std::move(controller), initBox); + } + auto controller = std::make_unique(peer, alreadyIn); controller->setStyleOverrides( &st::groupCallInviteMembersList, @@ -194,30 +884,31 @@ object_ptr PrepareInviteBox( &st::groupCallInviteMembersList, &st::groupCallMultiSelect); - const auto weak = base::make_weak(call); const auto invite = [=](const std::vector> &users) { const auto call = weak.get(); if (!call) { return; } - const auto result = call->inviteUsers(users); - if (const auto user = std::get_if>(&result)) { - showToast(tr::lng_group_call_invite_done_user( - tr::now, - lt_user, - Ui::Text::Bold((*user)->firstName), - Ui::Text::WithEntities)); - } else if (const auto count = std::get_if(&result)) { - if (*count > 0) { + auto requests = ranges::views::all( + users + ) | ranges::views::transform([](not_null 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(result.invited.front()->firstName), + Ui::Text::WithEntities)); + } 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 show, @@ -308,4 +999,215 @@ object_ptr PrepareInviteBox( return Box(std::move(controllers), initBox); } +object_ptr PrepareInviteBox( + not_null call, + Fn)> inviteUsers, + Fn shareLink) { + const auto user = call->user(); + const auto weak = base::make_weak(call); + auto alreadyIn = base::flat_set>{ user }; + auto controller = std::make_unique( + &user->session(), + ConfInviteDarkStyles(), + alreadyIn, + shareLink, + std::vector>()); + const auto raw = controller.get(); + raw->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + auto initBox = [=](not_null 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(std::move(controller), initBox); +} + +not_null CreateReActivateHeader(not_null parent) { + const auto result = Ui::CreateChild(parent); + result->add( + MakeJoinCallLogo(result), + st::boxRowPadding + st::confcallLinkHeaderIconPadding); + + result->add( + object_ptr>( + result, + object_ptr( + result, + tr::lng_confcall_inactive_title(), + st::boxTitle)), + st::boxRowPadding + st::confcallLinkTitlePadding); + result->add( + object_ptr( + result, + tr::lng_confcall_inactive_about(), + st::confcallLinkCenteredText), + st::boxRowPadding + st::confcallLinkTitlePadding + )->setTryMakeSimilarLines(true); + Ui::AddDivider(result); + + return result; +} + +void InitReActivate(not_null box) { + box->setTitle(rpl::producer(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 PrepareInviteToEmptyBox( + std::shared_ptr call, + MsgId inviteMsgId, + std::vector> prioritize) { + auto controller = std::make_unique( + &call->session(), + ConfInviteDefaultStyles(), + base::flat_set>(), + nullptr, + std::move(prioritize)); + const auto raw = controller.get(); + raw->setStyleOverrides(&st::createCallList); + const auto initBox = [=](not_null 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(std::move(controller), initBox); +} + +object_ptr PrepareCreateCallBox( + not_null<::Window::SessionController*> window, + Fn created, + MsgId discardedInviteMsgId, + std::vector> prioritize) { + struct State { + bool creatingLink = false; + QPointer box; + }; + const auto state = std::make_shared(); + 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( + &window->session(), + ConfInviteDefaultStyles(), + base::flat_set>(), + discardedInviteMsgId ? Fn() : shareLink, + std::move(prioritize)); + const auto raw = controller.get(); + if (discardedInviteMsgId) { + raw->setStyleOverrides(&st::createCallList); + } + const auto initBox = [=](not_null 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(std::move(controller), initBox); + state->box = result.data(); + return result; +} + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h index e1e10ff4e7..cbfac6af3e 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h @@ -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 PrepareInviteBox( not_null call, - Fn showToast); + Fn showToast, + Fn shareConferenceLink = nullptr); + +[[nodiscard]] object_ptr PrepareInviteBox( + not_null call, + Fn)> inviteUsers, + Fn shareLink); + +[[nodiscard]] object_ptr PrepareInviteToEmptyBox( + std::shared_ptr call, + MsgId inviteMsgId, + std::vector> prioritize); + +[[nodiscard]] object_ptr PrepareCreateCallBox( + not_null<::Window::SessionController*> window, + Fn created = nullptr, + MsgId discardedInviteMsgId = 0, + std::vector> prioritize = {}); } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index 928c3b1f5d..d5ce0e5791 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -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 createRow( const Data::GroupCallParticipant &participant); [[nodiscard]] std::unique_ptr createInvitedRow( + not_null participantPeer, + bool calling); + [[nodiscard]] std::unique_ptr createWithAccessRow( not_null participantPeer); [[nodiscard]] bool isMe(not_null participantPeer) const; @@ -128,7 +133,8 @@ private: void updateRow( not_null row, const std::optional &was, - const Data::GroupCallParticipant *participant); + const Data::GroupCallParticipant *participant, + Row::State noParticipantState = Row::State::Invited); void updateRowInSoundingMap( not_null 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 &&nowIds); void hideRowsWithVideoExcept(const VideoEndpoint &large); void showAllHiddenRows(); @@ -205,6 +216,8 @@ private: Ui::RoundRect _narrowRoundRect; QImage _narrowShadow; + base::flat_set _withAccess; + rpl::lifetime _lifetime; }; @@ -414,6 +427,9 @@ void Members::Controller::subscribeToChanges(not_null 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 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 &&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(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(row).state(); + return (state != Row::State::Invited) + && (state != Row::State::Calling); + }); + } + delegate()->peerListRefreshRows(); + }, _lifetime); +} + void Members::Controller::updateRow( const std::optional &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)->state() == kInvited); + reorderIfNonRealBefore - 1).get(); + using State = Row::State; + const auto state = static_cast(row)->state(); + return (state == State::Invited) + || (state == State::Calling) + || (state == State::WithAccess); }(); if (reorder) { - delegate()->peerListPartitionRows([](const PeerListRow &row) { - return static_cast(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(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(row).state(); + return (state != Row::State::Invited) + && (state != Row::State::Calling); + }); + } +} + bool Members::Controller::allRowsAboveAreSpeaking(not_null 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) 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) { 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) { void Members::Controller::updateRow( not_null row, const std::optional &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 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 participantPeer) { } bool Members::Controller::rowCanMuteMembers() { - return _peer->canManageGroupCall(); + return _call->canManage(); } void Members::Controller::rowUpdateRow(not_null 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 Members::Controller::createRowContextMenu( const auto participantPeer = row->peer(); const auto real = static_cast(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 Members::Controller::createRowContextMenu( window->invokeForSessionController( account, participantPeer, - [&](not_null newController) { + [&](not_null<::Window::SessionController*> newController) { callback(newController); newController->widget()->activate(); }); } }; const auto showProfile = [=] { - withActiveWindow([=](not_null window) { + withActiveWindow([=](not_null<::Window::SessionController*> window) { window->showPeerInfo(participantPeer); }); }; const auto showHistory = [=] { - withActiveWindow([=](not_null 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 Members::Controller::createRowContextMenu( false, static_cast(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 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 Members::Controller::createRowContextMenu( } const auto canKick = [&] { const auto user = participantPeer->asUser(); - if (static_cast(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) { 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 Members::Controller::createRow( } std::unique_ptr Members::Controller::createInvitedRow( + not_null 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(this, participantPeer); + updateRow(result.get(), std::nullopt, nullptr, state); + return result; +} + +std::unique_ptr Members::Controller::createWithAccessRow( not_null participantPeer) { if (findRow(participantPeer)) { return nullptr; } auto result = std::make_unique(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(call, parent, mode)) , _layout(_scroll->setOwnedWidget( object_ptr(_scroll.data()))) +, _fingerprint(call->conference() + ? _layout->add(object_ptr(_layout.get())) + : nullptr) , _videoWrap(_layout->add(object_ptr(_layout.get()))) , _viewport( std::make_unique( @@ -1567,6 +1757,7 @@ Members::Members( backend)) { setupList(); setupAddMember(call); + setupFingerprint(); setContent(_list); setupFakeRoundCorners(); _listController->setDelegate(static_cast(this)); @@ -1615,6 +1806,7 @@ rpl::producer 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 call) { using namespace rpl::mappers; const auto peer = call->peer(); + const auto conference = call->conference(); const auto canAddByPeer = [=](not_null 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 call) { }) | rpl::type_erased(); }; const auto canInviteByLinkByPeer = [=](not_null 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 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 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 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 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 }); + const auto heightValue = [=](Ui::RpWidget *widget) { + topleft->raise(); + topright->raise(); + bottomleft->raise(); + bottomright->raise(); + return widget ? widget->heightValue() : rpl::single(0); + }; rpl::combine( _list->geometryValue(), - _addMemberButton.value() | rpl::map([=](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) { + _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); diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.h b/Telegram/SourceFiles/calls/group/calls_group_members.h index 47cb059143..739cccc990 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members.h @@ -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 peer) const; [[nodiscard]] not_null rtmpFakeRow( @@ -93,6 +97,7 @@ private: void setupAddMember(not_null call); void resizeToList(); void setupList(); + void setupFingerprint(); void setupFakeRoundCorners(); void trackViewportGeometry(); @@ -103,13 +108,18 @@ private: object_ptr _scroll; std::unique_ptr _listController; not_null _layout; + Ui::RpWidget *_fingerprint = nullptr; + rpl::event_stream<> _fingerprintRepaints; + const FingerprintBadgeState *_fingerprintState = nullptr; const not_null _videoWrap; std::unique_ptr _viewport; rpl::variable _addMemberButton = nullptr; + rpl::variable _shareLinkButton = nullptr; RpWidget *_topSkip = nullptr; RpWidget *_bottomSkip = nullptr; ListWidget *_list = nullptr; rpl::event_stream<> _addMemberRequests; + rpl::event_stream<> _shareLinkRequests; mutable std::unique_ptr _rtmpFakeRow; diff --git a/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp index 02ae9cd685..3f213d61d5 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp @@ -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); - setSounding(false); - setSpeaking(false); - _mutedByMe = false; - _raisedHandRating = 0; - } else if (!participant->muted - || (participant->sounding && participant->ssrc != 0) - || (participant->additionalSounding - && GetAdditionalAudioSsrc(participant->videoParams) != 0)) { +void MembersRow::updateStateInvited(bool calling) { + setVolume(Group::kDefaultVolume); + setState(calling ? State::Calling : State::Invited); + setSounding(false); + setSpeaking(false); + _mutedByMe = false; + _raisedHandRating = 0; + refreshStatus(); +} + +void MembersRow::updateStateWithAccess() { + setVolume(Group::kDefaultVolume); + setState(State::WithAccess); + setSounding(false); + setSpeaking(false); + _mutedByMe = false; + _raisedHandRating = 0; + 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; + 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) { + } 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, }; } diff --git a/Telegram/SourceFiles/calls/group/calls_group_members_row.h b/Telegram/SourceFiles/calls/group/calls_group_members_row.h index 5ba910c8e2..2aab2ead48 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members_row.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members_row.h @@ -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 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( diff --git a/Telegram/SourceFiles/calls/group/calls_group_menu.cpp b/Telegram/SourceFiles/calls/group/calls_group_menu.cpp index 51a9068a73..b41dac66c6 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_menu.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_menu.cpp @@ -416,10 +416,13 @@ void LeaveBox( not_null 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( box.get(), (scheduled @@ -490,20 +495,24 @@ void FillMenu( Fn)> 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(); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 4ae84ffcc6..3a40c36f49 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -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); - ~Show(); - - void showOrHideBoxOrLayer( - std::variant< - v::null_t, - object_ptr, - std::unique_ptr> &&layer, - Ui::LayerOptions options, - anim::type animated) const override; - [[nodiscard]] not_null toastParent() const override; - [[nodiscard]] bool valid() const override; - operator bool() const override; - - [[nodiscard]] Main::Session &session() const override; - -private: - const base::weak_ptr _panel; - -}; - -Show::Show(not_null panel) -: _panel(base::make_weak(panel)) { -} - -Show::~Show() = default; - -void Show::showOrHideBoxOrLayer( - std::variant< - v::null_t, - object_ptr, - std::unique_ptr> &&layer, - Ui::LayerOptions options, - anim::type animated) const { - using UniqueLayer = std::unique_ptr; - using ObjectBox = object_ptr; - if (auto layerWidget = std::get_if(&layer)) { - if (const auto panel = _panel.get()) { - panel->showLayer(std::move(*layerWidget), options, animated); - } - } else if (auto box = std::get_if(&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 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 widget) { SetWindowPos( @@ -177,22 +107,18 @@ struct Panel::ControlsBackgroundNarrow { }; Panel::Panel(not_null call) +: Panel(call, ConferencePanelMigration()) { +} + +Panel::Panel(not_null call, ConferencePanelMigration info) : _call(call) , _peer(call->peer()) -, _layerBg(std::make_unique(widget())) -#ifndef Q_OS_MAC -, _controls(Ui::Platform::SetupSeparateTitleControls( - window(), - st::groupCallTitle, - nullptr, - _controlsTop.value())) -#endif // !Q_OS_MAC -, _powerSaveBlocker(std::make_unique( - base::PowerSaveBlockType::PreventDisplaySleep, - u"Video chat is active"_q, - window()->windowHandle())) +, _window(info.window ? info.window : std::make_shared()) , _viewport( - std::make_unique(widget(), PanelMode::Wide, _window.backend())) + std::make_unique( + widget(), + PanelMode::Wide, + _window->backend())) , _mute(std::make_unique( widget(), st::callMuteButton, @@ -222,9 +148,6 @@ Panel::Panel(not_null 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 call) initWindow(); initWidget(); initControls(); - initLayout(); + initLayout(info); showAndActivate(); } @@ -268,25 +191,12 @@ bool Panel::isActive() const { return window()->isActiveWindow() && isVisible(); } -base::weak_ptr Panel::showToast( - const QString &text, - crl::time duration) { - return Show(this).showToast(text, duration); +std::shared_ptr Panel::sessionShow() { + return Main::MakeSessionShow(uiShow(), &_peer->session()); } -base::weak_ptr Panel::showToast( - TextWithEntities &&text, - crl::time duration) { - return Show(this).showToast(std::move(text), duration); -} - -base::weak_ptr Panel::showToast( - Ui::Toast::Config &&config) { - return Show(this).showToast(std::move(config)); -} - -std::shared_ptr Panel::uiShow() { - return std::make_shared(this); +std::shared_ptr Panel::uiShow() { + return _window->uiShow(); } void Panel::minimize() { @@ -367,14 +277,28 @@ void Panel::initWindow() { window()->setAttribute(Qt::WA_NoSystemBackground); window()->setTitleStyle(st::groupCallTitle); - subscribeToPeerChanges(); + 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 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(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 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); - } + _call->toggleMute(request); }, _callLifetime); _members->changeVolumeRequests( ) | rpl::start_with_next([=](VolumeRequest request) { - if (_call) { - _call->changeVolume(request); - } + _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 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 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 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 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(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 participantPeer) { - showBox(Box([=](not_null box) { + uiShow()->showBox(Box([=](not_null box) { box->addRow( object_ptr( box.get(), @@ -1525,7 +1512,9 @@ void Panel::kickParticipant(not_null 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 participantPeer) { })); } -void Panel::showBox(object_ptr box) { - showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal); -} - -void Panel::showBox( - object_ptr 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 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 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 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,25 +1586,27 @@ void Panel::closeBeforeDestroy() { } rpl::lifetime &Panel::lifetime() { - return window()->lifetime(); + return _lifetime; } -void Panel::initGeometry() { - const auto center = Core::App().getPointForCallPanelCenter(); - const auto width = _call->rtmp() - ? st::groupCallWidthRtmp - : st::groupCallWidth; - const auto height = _call->rtmp() - ? st::groupCallHeightRtmp - : st::groupCallHeight; +void Panel::initGeometry(ConferencePanelMigration info) { 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())); + if (!info.window) { + const auto center = Core::App().getPointForCallPanelCenter(); + const auto width = _call->rtmp() + ? st::groupCallWidthRtmp + : st::groupCallWidth; + const auto height = _call->rtmp() + ? st::groupCallHeightRtmp + : 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 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,19 +2398,26 @@ void Panel::updateMembersGeometry() { } } +rpl::producer Panel::titleText() { + if (_call->conference()) { + return tr::lng_confcall_join_title(); + } + return rpl::combine( + Info::Profile::NameValue(_peer), + rpl::single( + QString() + ) | rpl::then(_call->real( + ) | rpl::map([=](not_null real) { + return real->titleValue(); + }) | rpl::flatten_latest()) + ) | rpl::map([=](const QString &name, const QString &title) { + return title.isEmpty() ? name : title; + }); +} + void Panel::refreshTitle() { if (!_title) { - auto text = rpl::combine( - Info::Profile::NameValue(_peer), - rpl::single( - QString() - ) | rpl::then(_call->real( - ) | rpl::map([=](not_null real) { - return real->titleValue(); - }) | rpl::flatten_latest()) - ) | rpl::map([=](const QString &name, const QString &title) { - return title.isEmpty() ? name : title; - }) | rpl::after_next([=] { + 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 Panel::window() const { - return _window.window(); + return _window->window(); } not_null Panel::widget() const { - return _window.widget(); + return _window->widget(); } } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 851cc91d8f..94519c9a99 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -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 class FadeWrap; template 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 call); + explicit Panel(not_null call); + Panel(not_null call, ConferencePanelMigration info); ~Panel(); [[nodiscard]] not_null widget() const; @@ -94,34 +87,20 @@ public: [[nodiscard]] bool isVisible() const; [[nodiscard]] bool isActive() const; - base::weak_ptr showToast( - const QString &text, - crl::time duration = 0); - base::weak_ptr showToast( - TextWithEntities &&text, - crl::time duration = 0); - base::weak_ptr showToast( - Ui::Toast::Config &&config); - - void showBox(object_ptr box); - void showBox( - object_ptr box, - Ui::LayerOptions options, - anim::type animated = anim::type::normal); - void showLayer( - std::unique_ptr 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 users); void minimize(); void toggleFullScreen(); + void toggleFullScreen(bool fullscreen); void close(); void showAndActivate(); void closeBeforeDestroy(); - [[nodiscard]] std::shared_ptr uiShow(); + [[nodiscard]] std::shared_ptr sessionShow(); + [[nodiscard]] std::shared_ptr uiShow(); + + [[nodiscard]] not_null window() const; rpl::lifetime &lifetime(); @@ -139,8 +118,6 @@ private: Discarded, }; - [[nodiscard]] not_null 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 date); void setupMembers(); void setupVideo(not_null viewport); void setupRealMuteButtonState(not_null real); + [[nodiscard]] rpl::producer titleText(); bool handleClose(); void startScheduledNow(); @@ -192,6 +170,7 @@ private: void toggleWideControls(bool shown); void updateWideControlsVisibility(); [[nodiscard]] bool videoButtonInNarrowMode() const; + [[nodiscard]] Fn shareConferenceLinkCallback(); void endCall(); @@ -225,18 +204,11 @@ private: const not_null _call; not_null _peer; - Ui::GL::Window _window; - const std::unique_ptr _layerBg; + std::shared_ptr _window; rpl::variable _mode; rpl::variable _fullScreenOrMaximized = false; bool _unpinnedMaximized = false; - -#ifndef Q_OS_MAC - rpl::variable _controlsTop = 0; - const std::unique_ptr _controls; -#endif // !Q_OS_MAC - - const std::unique_ptr _powerSaveBlocker; + bool _rtmpFull = false; rpl::lifetime _callLifetime; @@ -293,6 +265,7 @@ private: rpl::lifetime _hideControlsTimerLifetime; rpl::lifetime _peerLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 7c2bdfd4ce..0adbb8e3e9 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -766,7 +766,7 @@ void SettingsBox( }, volumeItem->lifetime()); } - if (peer->canManageGroupCall()) { + if (call->canManage()) { layout->add(object_ptr( layout, (peer->isBroadcast() diff --git a/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp b/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp index 55e67be95e..584a30b551 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp @@ -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); } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index bc7f46d696..89bcc12fce 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -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(&_pickerSelected)) { + if (v::is(_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(&data)) { return resolveCustomRecent(document->id); - } else if (const auto emoji = std::get_if(&data)) { + } else if (v::is(data)) { return nullptr; } Unexpected("Custom recent emoji id."); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 61d27c3408..fb82472a0b 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -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; } diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index c8134da03f..4ba6f2e614 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -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 @@ -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()); setLastActiveWindow(_windows.front().second.get()); diff --git a/Telegram/SourceFiles/core/bank_card_click_handler.cpp b/Telegram/SourceFiles/core/bank_card_click_handler.cpp new file mode 100644 index 0000000000..1bcca7f0ec --- /dev/null +++ b/Telegram/SourceFiles/core/bank_card_click_handler.cpp @@ -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 session) : sender(&session->mtp()) { + } + MTP::Sender sender; +}; + +struct BankCardData final { + QString title; + std::vector links; +}; + +enum class Status { + Loading, + Resolved, + Failed, +}; + +void RequestResolveBankCard( + not_null state, + const QString &bankCard, + Fn done, + Fn 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 parent, + const style::Menu &st); + + void setStatus(Status status); + + bool isEnabled() const override; + not_null action() const override; + +protected: + int contentHeight() const override; + + void paintEvent(QPaintEvent *e) override; + +private: + void paint(Painter &p); + + const not_null _dummyAction; + const style::Menu &_st; + const int _height = 0; + Status _status = Status::Loading; + + Ui::Text::String _text; + +}; + +ResolveBankCardAction::ResolveBankCardAction( + not_null parent, + const style::Menu &st) +: ItemBase(parent, st) +, _dummyAction(Ui::CreateChild(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 ResolveBankCardAction::action() const { + return _dummyAction; +} + +int ResolveBankCardAction::contentHeight() const { + if (_status == Status::Resolved) { + return 0; + } + return _height; +} + +} // namespace + +BankCardClickHandler::BankCardClickHandler( + not_null 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(); + const auto controller = my.sessionWindow.get(); + const auto pos = QCursor::pos(); + if (!controller) { + return; + } + const auto menu = Ui::CreateChild( + 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( + 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( + 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( + &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; +} diff --git a/Telegram/SourceFiles/core/bank_card_click_handler.h b/Telegram/SourceFiles/core/bank_card_click_handler.h new file mode 100644 index 0000000000..ff45af82e1 --- /dev/null +++ b/Telegram/SourceFiles/core/bank_card_click_handler.h @@ -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 session, QString text); + + void onClick(ClickContext context) const override; + + TextEntity getTextEntity() const override; + + QString tooltip() const override; + +private: + const not_null _session; + QString _text; + +}; diff --git a/Telegram/SourceFiles/core/core_cloud_password.cpp b/Telegram/SourceFiles/core/core_cloud_password.cpp index a835112dc9..95e1782a8f 100644 --- a/Telegram/SourceFiles/core/core_cloud_password.cpp +++ b/Telegram/SourceFiles/core/core_cloud_password.cpp @@ -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 = [&] { diff --git a/Telegram/SourceFiles/core/core_cloud_password.h b/Telegram/SourceFiles/core/core_cloud_password.h index 2c4d06bc86..8840ae04a8 100644 --- a/Telegram/SourceFiles/core/core_cloud_password.h +++ b/Telegram/SourceFiles/core/core_cloud_password.h @@ -135,6 +135,7 @@ struct CloudPasswordState { bool outdatedClient = false; QString hint; QString unconfirmedPattern; + QString loginEmailPattern; TimeId pendingResetDate = 0; }; diff --git a/Telegram/SourceFiles/core/file_utilities.cpp b/Telegram/SourceFiles/core/file_utilities.cpp index 21d912f63c..3c1769cf57 100644 --- a/Telegram/SourceFiles/core/file_utilities.cpp +++ b/Telegram/SourceFiles/core/file_utilities.cpp @@ -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(); } diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 0c8899cf7a..4cc15d9aa1 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -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 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 setupAboveWidget( + not_null controller, + not_null parent, + rpl::producer