Merge tag 'v5.14.1' into dev

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

100
.cursor/api_usage.md Normal file
View file

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

159
.cursor/localization.md Normal file
View file

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

211
.cursor/rpl_guide.md Normal file
View file

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

149
.cursor/styling.md Normal file
View file

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

2
.cursorignore Normal file
View file

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

View file

@ -26,7 +26,6 @@ get_filename_component(res_loc Resources REALPATH)
include(cmake/telegram_options.cmake) include(cmake/telegram_options.cmake)
include(cmake/lib_ffmpeg.cmake) include(cmake/lib_ffmpeg.cmake)
include(cmake/lib_stripe.cmake) include(cmake/lib_stripe.cmake)
include(cmake/lib_tgvoip.cmake)
include(cmake/lib_tgcalls.cmake) include(cmake/lib_tgcalls.cmake)
include(cmake/lib_prisma.cmake) include(cmake/lib_prisma.cmake)
include(cmake/td_export.cmake) include(cmake/td_export.cmake)
@ -34,6 +33,7 @@ include(cmake/td_iv.cmake)
include(cmake/td_lang.cmake) include(cmake/td_lang.cmake)
include(cmake/td_mtproto.cmake) include(cmake/td_mtproto.cmake)
include(cmake/td_scheme.cmake) include(cmake/td_scheme.cmake)
include(cmake/td_tde2e.cmake)
include(cmake/td_ui.cmake) include(cmake/td_ui.cmake)
include(cmake/generate_appdata_changelog.cmake) include(cmake/generate_appdata_changelog.cmake)
@ -47,17 +47,15 @@ if (WIN32)
platform/win/windows_quiethours.idl platform/win/windows_quiethours.idl
platform/win/windows_toastactivator.idl platform/win/windows_toastactivator.idl
) )
nuget_add_winrt(Telegram)
endif() endif()
set_target_properties(Telegram PROPERTIES AUTOMOC ON) set_target_properties(Telegram PROPERTIES AUTOMOC ON)
target_link_libraries(Telegram target_link_libraries(Telegram
PRIVATE PRIVATE
tdesktop::lib_tgcalls_legacy # tdesktop::lib_tgcalls_legacy
tdesktop::lib_tgcalls tdesktop::lib_tgcalls
tdesktop::lib_tgvoip # tdesktop::lib_tgvoip
# Order in this list defines the order of include paths in command line. # 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 # We need to place desktop-app::external_minizip this early to have its
@ -71,6 +69,7 @@ PRIVATE
tdesktop::td_lang tdesktop::td_lang
tdesktop::td_mtproto tdesktop::td_mtproto
tdesktop::td_scheme tdesktop::td_scheme
tdesktop::td_tde2e
tdesktop::td_ui tdesktop::td_ui
desktop-app::lib_webrtc desktop-app::lib_webrtc
desktop-app::lib_base desktop-app::lib_base
@ -478,6 +477,8 @@ PRIVATE
calls/calls_video_bubble.h calls/calls_video_bubble.h
calls/calls_video_incoming.cpp calls/calls_video_incoming.cpp
calls/calls_video_incoming.h calls/calls_video_incoming.h
calls/calls_window.cpp
calls/calls_window.h
chat_helpers/compose/compose_features.h chat_helpers/compose/compose_features.h
chat_helpers/compose/compose_show.cpp chat_helpers/compose/compose_show.cpp
chat_helpers/compose/compose_show.h chat_helpers/compose/compose_show.h
@ -527,6 +528,8 @@ PRIVATE
chat_helpers/ttl_media_layer_widget.h chat_helpers/ttl_media_layer_widget.h
core/application.cpp core/application.cpp
core/application.h core/application.h
core/bank_card_click_handler.cpp
core/bank_card_click_handler.h
core/base_integration.cpp core/base_integration.cpp
core/base_integration.h core/base_integration.h
core/changelogs.cpp core/changelogs.cpp
@ -774,6 +777,8 @@ PRIVATE
dialogs/dialogs_search_from_controllers.h dialogs/dialogs_search_from_controllers.h
dialogs/dialogs_search_tags.cpp dialogs/dialogs_search_tags.cpp
dialogs/dialogs_search_tags.h dialogs/dialogs_search_tags.h
dialogs/dialogs_top_bar_suggestion.cpp
dialogs/dialogs_top_bar_suggestion.h
dialogs/dialogs_widget.cpp dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h dialogs/dialogs_widget.h
editor/color_picker.cpp editor/color_picker.cpp
@ -1161,6 +1166,8 @@ PRIVATE
inline_bots/inline_bot_result.h inline_bots/inline_bot_result.h
inline_bots/inline_bot_send_data.cpp inline_bots/inline_bot_send_data.cpp
inline_bots/inline_bot_send_data.h 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.cpp
inline_bots/inline_results_inner.h inline_bots/inline_results_inner.h
inline_bots/inline_results_widget.cpp 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_hint.h
settings/cloud_password/settings_cloud_password_input.cpp settings/cloud_password/settings_cloud_password_input.cpp
settings/cloud_password/settings_cloud_password_input.h 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.cpp
settings/cloud_password/settings_cloud_password_manage.h settings/cloud_password/settings_cloud_password_manage.h
settings/cloud_password/settings_cloud_password_start.cpp settings/cloud_password/settings_cloud_password_start.cpp
@ -1618,6 +1629,8 @@ PRIVATE
support/support_preload.h support/support_preload.h
support/support_templates.cpp support/support_templates.cpp
support/support_templates.h 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.cpp
ui/boxes/edit_invite_link_session.h ui/boxes/edit_invite_link_session.h
ui/boxes/peer_qr_box.cpp ui/boxes/peer_qr_box.cpp
@ -1814,6 +1827,10 @@ if (WIN32)
# COMMENT # COMMENT
# $<IF:${release},"Appending compatibility manifest.","Finalizing build."> # $<IF:${release},"Appending compatibility manifest.","Finalizing build.">
# ) # )
if (QT_VERSION LESS 6)
target_link_libraries(Telegram PRIVATE desktop-app::win_directx_helper)
endif()
elseif (APPLE) elseif (APPLE)
if (NOT DESKTOP_APP_USE_PACKAGED) if (NOT DESKTOP_APP_USE_PACKAGED)
target_link_libraries(Telegram PRIVATE desktop-app::external_iconv) target_link_libraries(Telegram PRIVATE desktop-app::external_iconv)
@ -1991,66 +2008,8 @@ if (MSVC)
) )
target_link_options(Telegram target_link_options(Telegram
PRIVATE PRIVATE
/DELAYLOAD:secur32.dll
/DELAYLOAD:winmm.dll
/DELAYLOAD:ws2_32.dll
/DELAYLOAD:user32.dll
/DELAYLOAD:gdi32.dll
/DELAYLOAD:advapi32.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() endif()
target_prepare_qrc(Telegram) 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 base/platform/win/base_windows_safe_library.h
) )
target_include_directories(Updater PRIVATE ${lib_base_loc}) 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) elseif (APPLE)
add_custom_command(TARGET Updater add_custom_command(TARGET Updater
PRE_LINK PRE_LINK

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -882,6 +882,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_cloud_password_email_confirm" = "Confirm and Finish"; "lng_settings_cloud_password_email_confirm" = "Confirm and Finish";
"lng_settings_cloud_password_reset_in" = "You can reset your password in {duration}."; "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_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_about" = "Automatically delete messages for everyone after a period of time in all new chats you start.";
"lng_settings_ttl_after" = "After {after_duration}"; "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_delete" = "Delete";
"lng_settings_quick_dialog_action_disabled" = "Change folder"; "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" = "Subscribe to {link} to use this setting.";
"lng_settings_generic_subscribe_link" = "Telegram Premium"; "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#one" = "You paid {count} Star.";
"lng_you_paid_stars#other" = "You paid {count} Stars."; "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_title" = "Similar channels";
"lng_similar_channels_view_all" = "View all"; "lng_similar_channels_view_all" = "View all";
"lng_similar_channels_more" = "More Channels"; "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_show_on_channel" = "Display in channel's Gifts";
"lng_gift_availability" = "Availability"; "lng_gift_availability" = "Availability";
"lng_gift_from_hidden" = "Hidden User"; "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_status" = "buy yourself a gift";
"lng_gift_self_title" = "Buy 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."; "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_restrict_users" = "Restrict users";
"lng_delete_all_from_user" = "Delete all from {user}"; "lng_delete_all_from_user" = "Delete all from {user}";
"lng_delete_all_from_users" = "Delete all from users"; "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_user_part" = "Partially restrict this user {emoji}";
"lng_restrict_users_part" = "Partially restrict users {emoji}"; "lng_restrict_users_part" = "Partially restrict users {emoji}";
"lng_restrict_user_full" = "Fully ban this user {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_skip_archive_in_search" = "Skip results from archive";
"lng_dialogs_show_archive_in_search" = "With results from archive"; "lng_dialogs_show_archive_in_search" = "With results from archive";
"lng_dialogs_suggestions_birthday_title" = "Add your birthday! 🎂";
"lng_dialogs_suggestions_birthday_about" = "Let your contacts know when youre celebrating.";
"lng_dialogs_suggestions_birthday_contact_title" = "Its {text}'s **birthday** today! 🎂";
"lng_dialogs_suggestions_birthday_contact_about" = "Send them a Gift.";
"lng_dialogs_suggestions_birthday_contacts_title#one" = "{count} contact have **birthdays** today! 🎂";
"lng_dialogs_suggestions_birthday_contacts_title#other" = "{count} contacts have **birthdays** today! 🎂";
"lng_dialogs_suggestions_birthday_contacts_about" = "Send them a Gift.";
"lng_dialogs_suggestions_birthday_contact_dismiss" = "You can send a Gift later in Settings";
"lng_dialogs_suggestions_premium_annual_title" = "Telegram Premium with a {text} discount";
"lng_dialogs_suggestions_premium_annual_about" = "Sign up for the annual payment plan for Telegram Premium now to get the discount.";
"lng_dialogs_suggestions_premium_upgrade_title" = "Telegram Premium with a {text} discount";
"lng_dialogs_suggestions_premium_upgrade_about" = "Upgrade to the annual payment plan for Telegram Premium now to get the discount.";
"lng_dialogs_suggestions_premium_restore_title" = "Get Premium back with up to {text} off";
"lng_dialogs_suggestions_premium_restore_about" = "Your Telegram Premium has recently expired. Tap here to extend it.";
"lng_dialogs_suggestions_premium_grace_title" = "⚠️ Your Premium subscription is expiring!";
"lng_dialogs_suggestions_premium_grace_about" = "Dont lose access to exclusive features.";
"lng_dialogs_suggestions_userpics_title" = "Add your photo! 📸";
"lng_dialogs_suggestions_userpics_about" = "Help your friends spot you easily.";
"lng_dialogs_suggestions_credits_sub_low_title#one" = "{emoji} {count} Star needed for {channels}";
"lng_dialogs_suggestions_credits_sub_low_title#other" = "{emoji} {count} Stars needed for {channels}";
"lng_dialogs_suggestions_credits_sub_low_about" = "Insufficient funds to cover your subscription.";
"lng_about_random" = "Send a {emoji} emoji to any chat to try your luck."; "lng_about_random" = "Send a {emoji} emoji to any chat to try your luck.";
"lng_about_random_send" = "Send"; "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_failed" = "failed to connect";
"lng_call_status_ringing" = "ringing..."; "lng_call_status_ringing" = "ringing...";
"lng_call_status_busy" = "line busy"; "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_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"; "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_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_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_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_bar_hangup" = "End call";
"lng_call_leave_to_other_sure" = "End your active call and join this video chat?"; "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_outgoing" = "Outgoing call";
"lng_call_video_outgoing" = "Outgoing video call"; "lng_call_video_outgoing" = "Outgoing video call";
"lng_call_group_outgoing" = "Outgoing group call";
"lng_call_incoming" = "Incoming call"; "lng_call_incoming" = "Incoming call";
"lng_call_video_incoming" = "Incoming video call"; "lng_call_video_incoming" = "Incoming video call";
"lng_call_group_incoming" = "Incoming group call";
"lng_call_missed" = "Missed call"; "lng_call_missed" = "Missed call";
"lng_call_video_missed" = "Missed video call"; "lng_call_video_missed" = "Missed video call";
"lng_call_group_missed" = "Missed group call";
"lng_call_cancelled" = "Canceled call"; "lng_call_cancelled" = "Canceled call";
"lng_call_video_cancelled" = "Canceled video call"; "lng_call_video_cancelled" = "Canceled video call";
"lng_call_declined" = "Declined call"; "lng_call_declined" = "Declined call";
"lng_call_video_declined" = "Declined video call"; "lng_call_video_declined" = "Declined video call";
"lng_call_group_declined" = "Declined group call";
"lng_call_duration_info" = "{time}, {duration}"; "lng_call_duration_info" = "{time}, {duration}";
"lng_call_type_and_duration" = "{type} ({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_label" = "Please rate the quality of your call";
"lng_call_rate_comment" = "Comment (optional)"; "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_start_video" = "Start Video";
"lng_call_stop_video" = "Stop Video"; "lng_call_stop_video" = "Stop Video";
"lng_call_screencast" = "Screencast"; "lng_call_screencast" = "Screencast";
"lng_call_add_people" = "Add People";
"lng_call_end_call" = "End Call"; "lng_call_end_call" = "End Call";
"lng_call_mute_audio" = "Mute"; "lng_call_mute_audio" = "Mute";
"lng_call_unmute_audio" = "Unmute"; "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_connecting" = "Connecting...";
"lng_group_call_leave" = "Leave"; "lng_group_call_leave" = "Leave";
"lng_group_call_leave_title" = "Leave video chat"; "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_title_channel" = "Leave live stream";
"lng_group_call_leave_sure" = "Do you want to leave this video chat?"; "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_leave_sure_channel" = "Are you sure you want to leave this live stream?";
"lng_group_call_close" = "Close"; "lng_group_call_close" = "Close";
"lng_group_call_close_sure" = "Video chat is scheduled. You can abort it or just close this panel."; "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_also_end_channel" = "End live stream";
"lng_group_call_settings_title" = "Settings"; "lng_group_call_settings_title" = "Settings";
"lng_group_call_invite" = "Invite Members"; "lng_group_call_invite" = "Invite Members";
"lng_group_call_invite_conf" = "Add People";
"lng_group_call_invited_status" = "invited"; "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_muted_by_me_status" = "muted for you";
"lng_group_call_invite_title" = "Invite members"; "lng_group_call_invite_title" = "Invite members";
"lng_group_call_invite_button" = "Invite"; "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_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_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_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_members" = "Group members";
"lng_group_call_invite_search_results" = "Search results"; "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_new_muted" = "Mute new participants";
"lng_group_call_speakers" = "Speakers"; "lng_group_call_speakers" = "Speakers";
"lng_group_call_microphone" = "Microphone"; "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_pin_screen" = "Pin screencast";
"lng_group_call_context_unpin_screen" = "Unpin screencast"; "lng_group_call_context_unpin_screen" = "Unpin screencast";
"lng_group_call_context_remove" = "Remove"; "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" = "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_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."; "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#one" = "{count} viewer";
"lng_group_call_rtmp_viewers#other" = "{count} viewers"; "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_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages.";
"lng_player_message_today" = "today at {time}"; "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_stickerset" = "View stickers";
"lng_view_button_emojipack" = "View emoji"; "lng_view_button_emojipack" = "View emoji";
"lng_view_button_collectible" = "View collectible"; "lng_view_button_collectible" = "View collectible";
"lng_view_button_call" = "Join call";
"lng_sponsored_hide_ads" = "Hide"; "lng_sponsored_hide_ads" = "Hide";
"lng_sponsored_title" = "What are sponsored messages?"; "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_chart_revenue" = "Revenue";
"lng_bot_earn_overview_title" = "Proceeds overview"; "lng_bot_earn_overview_title" = "Proceeds overview";
"lng_bot_earn_available" = "Available balance"; "lng_bot_earn_available" = "Available balance";
"lng_bot_earn_reward" = "Total balance";
"lng_bot_earn_total" = "Total lifetime proceeds"; "lng_bot_earn_total" = "Total lifetime proceeds";
"lng_bot_earn_balance_title" = "Available balance"; "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."; "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_text3" = "Appeal via {link} before {date}, or your account will be deleted.";
"lng_frozen_appeal_button" = "Submit an Appeal"; "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 // Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program..."; "lng_wnd_choose_program_menu" = "Choose Default Program...";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -348,12 +348,15 @@ void CreditsHistory::request(
void CreditsHistory::requestSubscriptions( void CreditsHistory::requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token, const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done) { Fn<void(Data::CreditsStatusSlice)> done,
bool missingBalance) {
if (_requestId) { if (_requestId) {
return; return;
} }
_requestId = _api.request(MTPpayments_GetStarsSubscriptions( _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, _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token) MTP_string(token)
)).done([=](const MTPpayments_StarsStatus &result) { )).done([=](const MTPpayments_StarsStatus &result) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,112 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
#include "ui/effects/animations.h"
#include "ui/gl/gl_window.h"
namespace base {
class PowerSaveBlocker;
} // namespace base
namespace style {
struct WindowTitle;
} // namespace style
namespace Ui {
class BoxContent;
class RpWindow;
class RpWidget;
class LayerManager;
class LayerWidget;
enum class LayerOption;
using LayerOptions = base::flags<LayerOption>;
class Show;
} // namespace Ui
namespace Ui::Platform {
struct SeparateTitleControls;
struct TitleLayout;
} // namespace Ui::Platform
namespace Ui::Toast {
struct Config;
class Instance;
} // namespace Ui::Toast
namespace Calls {
class Window final : public base::has_weak_ptr {
public:
Window();
~Window();
[[nodiscard]] Ui::GL::Backend backend() const;
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
void raiseControls();
void setControlsStyle(const style::WindowTitle &st);
void setControlsShown(float64 shown);
[[nodiscard]] int controlsWrapTop() const;
[[nodiscard]] QRect controlsGeometry() const;
[[nodiscard]] auto controlsLayoutChanges() const
-> rpl::producer<Ui::Platform::TitleLayout>;
[[nodiscard]] bool controlsHasHitTest(QPoint widgetPoint) const;
[[nodiscard]] rpl::producer<bool> maximizeRequests() const;
void raiseLayers();
[[nodiscard]] const Ui::LayerWidget *topShownLayer() const;
base::weak_ptr<Ui::Toast::Instance> showToast(
const QString &text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
TextWithEntities &&text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
Ui::Toast::Config &&config);
void showBox(object_ptr<Ui::BoxContent> box);
void showBox(
object_ptr<Ui::BoxContent> box,
Ui::LayerOptions options,
anim::type animated = anim::type::normal);
void showLayer(
std::unique_ptr<Ui::LayerWidget> layer,
Ui::LayerOptions options,
anim::type animated = anim::type::normal);
void hideLayer(anim::type animated = anim::type::normal);
[[nodiscard]] bool isLayerShown() const;
[[nodiscard]] rpl::producer<> showingLayer() const {
return _showingLayer.events();
}
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
void togglePowerSaveBlocker(bool enabled);
private:
Ui::GL::Window _window;
const std::unique_ptr<Ui::LayerManager> _layerBg;
#ifndef Q_OS_MAC
rpl::variable<int> _controlsTop = 0;
const std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
#endif // !Q_OS_MAC
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
rpl::event_stream<bool> _maximizeRequests;
rpl::event_stream<> _showingLayer;
};
} // namespace Calls

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,30 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/basic_click_handlers.h"
namespace Main {
class Session;
} // namespace Main
class BankCardClickHandler : public ClickHandler {
public:
BankCardClickHandler(not_null<Main::Session*> session, QString text);
void onClick(ClickContext context) const override;
TextEntity getTextEntity() const override;
QString tooltip() const override;
private:
const not_null<Main::Session*> _session;
QString _text;
};

View file

@ -315,6 +315,8 @@ CloudPasswordState ParseCloudPasswordState(
ParseSecureSecretAlgo(data.vnew_secure_algo())); ParseSecureSecretAlgo(data.vnew_secure_algo()));
result.unconfirmedPattern = qs( result.unconfirmedPattern = qs(
data.vemail_unconfirmed_pattern().value_or_empty()); 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.pendingResetDate = data.vpending_reset_date().value_or_empty();
result.outdatedClient = [&] { result.outdatedClient = [&] {

View file

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

View file

@ -332,7 +332,7 @@ QString ImagesOrAllFilter() {
} }
QString PhotoVideoFilesFilter() { 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(); + AllFilesFilter();
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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