Merge tag 'v5.14.1' into dev
100
.cursor/api_usage.md
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,2 @@
|
|||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
Telegram/ThirdParty/
|
|
@ -26,7 +26,6 @@ get_filename_component(res_loc Resources REALPATH)
|
|||
include(cmake/telegram_options.cmake)
|
||||
include(cmake/lib_ffmpeg.cmake)
|
||||
include(cmake/lib_stripe.cmake)
|
||||
include(cmake/lib_tgvoip.cmake)
|
||||
include(cmake/lib_tgcalls.cmake)
|
||||
include(cmake/lib_prisma.cmake)
|
||||
include(cmake/td_export.cmake)
|
||||
|
@ -34,6 +33,7 @@ include(cmake/td_iv.cmake)
|
|||
include(cmake/td_lang.cmake)
|
||||
include(cmake/td_mtproto.cmake)
|
||||
include(cmake/td_scheme.cmake)
|
||||
include(cmake/td_tde2e.cmake)
|
||||
include(cmake/td_ui.cmake)
|
||||
include(cmake/generate_appdata_changelog.cmake)
|
||||
|
||||
|
@ -47,17 +47,15 @@ if (WIN32)
|
|||
platform/win/windows_quiethours.idl
|
||||
platform/win/windows_toastactivator.idl
|
||||
)
|
||||
|
||||
nuget_add_winrt(Telegram)
|
||||
endif()
|
||||
|
||||
set_target_properties(Telegram PROPERTIES AUTOMOC ON)
|
||||
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
tdesktop::lib_tgcalls_legacy
|
||||
# tdesktop::lib_tgcalls_legacy
|
||||
tdesktop::lib_tgcalls
|
||||
tdesktop::lib_tgvoip
|
||||
# tdesktop::lib_tgvoip
|
||||
|
||||
# Order in this list defines the order of include paths in command line.
|
||||
# We need to place desktop-app::external_minizip this early to have its
|
||||
|
@ -71,6 +69,7 @@ PRIVATE
|
|||
tdesktop::td_lang
|
||||
tdesktop::td_mtproto
|
||||
tdesktop::td_scheme
|
||||
tdesktop::td_tde2e
|
||||
tdesktop::td_ui
|
||||
desktop-app::lib_webrtc
|
||||
desktop-app::lib_base
|
||||
|
@ -478,6 +477,8 @@ PRIVATE
|
|||
calls/calls_video_bubble.h
|
||||
calls/calls_video_incoming.cpp
|
||||
calls/calls_video_incoming.h
|
||||
calls/calls_window.cpp
|
||||
calls/calls_window.h
|
||||
chat_helpers/compose/compose_features.h
|
||||
chat_helpers/compose/compose_show.cpp
|
||||
chat_helpers/compose/compose_show.h
|
||||
|
@ -527,6 +528,8 @@ PRIVATE
|
|||
chat_helpers/ttl_media_layer_widget.h
|
||||
core/application.cpp
|
||||
core/application.h
|
||||
core/bank_card_click_handler.cpp
|
||||
core/bank_card_click_handler.h
|
||||
core/base_integration.cpp
|
||||
core/base_integration.h
|
||||
core/changelogs.cpp
|
||||
|
@ -774,6 +777,8 @@ PRIVATE
|
|||
dialogs/dialogs_search_from_controllers.h
|
||||
dialogs/dialogs_search_tags.cpp
|
||||
dialogs/dialogs_search_tags.h
|
||||
dialogs/dialogs_top_bar_suggestion.cpp
|
||||
dialogs/dialogs_top_bar_suggestion.h
|
||||
dialogs/dialogs_widget.cpp
|
||||
dialogs/dialogs_widget.h
|
||||
editor/color_picker.cpp
|
||||
|
@ -1161,6 +1166,8 @@ PRIVATE
|
|||
inline_bots/inline_bot_result.h
|
||||
inline_bots/inline_bot_send_data.cpp
|
||||
inline_bots/inline_bot_send_data.h
|
||||
inline_bots/inline_bot_storage.cpp
|
||||
inline_bots/inline_bot_storage.h
|
||||
inline_bots/inline_results_inner.cpp
|
||||
inline_bots/inline_results_inner.h
|
||||
inline_bots/inline_results_widget.cpp
|
||||
|
@ -1507,6 +1514,10 @@ PRIVATE
|
|||
settings/cloud_password/settings_cloud_password_hint.h
|
||||
settings/cloud_password/settings_cloud_password_input.cpp
|
||||
settings/cloud_password/settings_cloud_password_input.h
|
||||
settings/cloud_password/settings_cloud_password_login_email.cpp
|
||||
settings/cloud_password/settings_cloud_password_login_email.h
|
||||
settings/cloud_password/settings_cloud_password_login_email_confirm.cpp
|
||||
settings/cloud_password/settings_cloud_password_login_email_confirm.h
|
||||
settings/cloud_password/settings_cloud_password_manage.cpp
|
||||
settings/cloud_password/settings_cloud_password_manage.h
|
||||
settings/cloud_password/settings_cloud_password_start.cpp
|
||||
|
@ -1618,6 +1629,8 @@ PRIVATE
|
|||
support/support_preload.h
|
||||
support/support_templates.cpp
|
||||
support/support_templates.h
|
||||
tde2e/tde2e_integration.cpp
|
||||
tde2e/tde2e_integration.h
|
||||
ui/boxes/edit_invite_link_session.cpp
|
||||
ui/boxes/edit_invite_link_session.h
|
||||
ui/boxes/peer_qr_box.cpp
|
||||
|
@ -1814,6 +1827,10 @@ if (WIN32)
|
|||
# COMMENT
|
||||
# $<IF:${release},"Appending compatibility manifest.","Finalizing build.">
|
||||
# )
|
||||
|
||||
if (QT_VERSION LESS 6)
|
||||
target_link_libraries(Telegram PRIVATE desktop-app::win_directx_helper)
|
||||
endif()
|
||||
elseif (APPLE)
|
||||
if (NOT DESKTOP_APP_USE_PACKAGED)
|
||||
target_link_libraries(Telegram PRIVATE desktop-app::external_iconv)
|
||||
|
@ -1991,66 +2008,8 @@ if (MSVC)
|
|||
)
|
||||
target_link_options(Telegram
|
||||
PRIVATE
|
||||
/DELAYLOAD:secur32.dll
|
||||
/DELAYLOAD:winmm.dll
|
||||
/DELAYLOAD:ws2_32.dll
|
||||
/DELAYLOAD:user32.dll
|
||||
/DELAYLOAD:gdi32.dll
|
||||
/DELAYLOAD:advapi32.dll
|
||||
/DELAYLOAD:shell32.dll
|
||||
/DELAYLOAD:ole32.dll
|
||||
/DELAYLOAD:oleaut32.dll
|
||||
/DELAYLOAD:shlwapi.dll
|
||||
/DELAYLOAD:iphlpapi.dll
|
||||
/DELAYLOAD:gdiplus.dll
|
||||
/DELAYLOAD:version.dll
|
||||
/DELAYLOAD:dwmapi.dll
|
||||
/DELAYLOAD:uxtheme.dll
|
||||
/DELAYLOAD:crypt32.dll
|
||||
/DELAYLOAD:bcrypt.dll
|
||||
/DELAYLOAD:netapi32.dll
|
||||
/DELAYLOAD:imm32.dll
|
||||
/DELAYLOAD:userenv.dll
|
||||
/DELAYLOAD:wtsapi32.dll
|
||||
/DELAYLOAD:propsys.dll
|
||||
)
|
||||
if (QT_VERSION GREATER 6)
|
||||
if (NOT build_winarm)
|
||||
target_link_options(Telegram PRIVATE
|
||||
/DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_options(Telegram
|
||||
PRIVATE
|
||||
/DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-File-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-LibraryLoader-l1-2-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-Localization-l1-2-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-Memory-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-Memory-l1-1-1.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib
|
||||
/DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll
|
||||
/DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll
|
||||
# /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll # We shadowed GetDpiForMonitor
|
||||
/DELAYLOAD:authz.dll # Authz.lib
|
||||
/DELAYLOAD:comdlg32.dll
|
||||
/DELAYLOAD:dwrite.dll # DWrite.lib
|
||||
/DELAYLOAD:dxgi.dll # DXGI.lib
|
||||
/DELAYLOAD:d3d9.dll # D3D9.lib
|
||||
/DELAYLOAD:d3d11.dll # D3D11.lib
|
||||
/DELAYLOAD:d3d12.dll # D3D12.lib
|
||||
/DELAYLOAD:setupapi.dll # SetupAPI.lib
|
||||
/DELAYLOAD:winhttp.dll
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_prepare_qrc(Telegram)
|
||||
|
@ -2081,22 +2040,6 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins
|
|||
base/platform/win/base_windows_safe_library.h
|
||||
)
|
||||
target_include_directories(Updater PRIVATE ${lib_base_loc})
|
||||
if (MSVC)
|
||||
target_link_libraries(Updater
|
||||
PRIVATE
|
||||
delayimp
|
||||
)
|
||||
target_link_options(Updater
|
||||
PRIVATE
|
||||
/DELAYLOAD:user32.dll
|
||||
/DELAYLOAD:advapi32.dll
|
||||
/DELAYLOAD:shell32.dll
|
||||
/DELAYLOAD:ole32.dll
|
||||
/DELAYLOAD:shlwapi.dll
|
||||
)
|
||||
else()
|
||||
target_link_options(Updater PRIVATE -municode)
|
||||
endif()
|
||||
elseif (APPLE)
|
||||
add_custom_command(TARGET Updater
|
||||
PRE_LINK
|
||||
|
|
BIN
Telegram/Resources/icons/calls/call_group.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
Telegram/Resources/icons/calls/call_group@2x.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/calls/call_group@3x.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
Telegram/Resources/icons/calls/calls_add_people.png
Normal file
After Width: | Height: | Size: 529 B |
BIN
Telegram/Resources/icons/calls/calls_add_people@2x.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
Telegram/Resources/icons/calls/calls_add_people@3x.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/calls/group_call_logo.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/calls/group_call_logo@2x.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
Telegram/Resources/icons/calls/group_call_logo@3x.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 670 B After Width: | Height: | Size: 679 B |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -882,6 +882,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_settings_cloud_password_email_confirm" = "Confirm and Finish";
|
||||
"lng_settings_cloud_password_reset_in" = "You can reset your password in {duration}.";
|
||||
|
||||
"lng_settings_cloud_login_email_section_title" = "Login Email";
|
||||
"lng_settings_cloud_login_email_box_about" = "This email address will be used every time you log in to your Telegram account from a new device.";
|
||||
"lng_settings_cloud_login_email_box_ok" = "Change email";
|
||||
"lng_settings_cloud_login_email_title" = "Enter New Email";
|
||||
"lng_settings_cloud_login_email_placeholder" = "Enter Login Email";
|
||||
"lng_settings_cloud_login_email_about" = "You will receive Telegram login codes via email and not SMS. Please enter an email address to which you have access.";
|
||||
"lng_settings_cloud_login_email_confirm" = "Confirm";
|
||||
"lng_settings_cloud_login_email_code_title" = "Check Your New Email";
|
||||
"lng_settings_cloud_login_email_code_about" = "Please enter the code we have sent to your new email {email}";
|
||||
"lng_settings_cloud_login_email_success" = "Your email has been changed.";
|
||||
"lng_settings_error_email_not_alowed" = "Sorry, this email is not allowed";
|
||||
|
||||
"lng_settings_ttl_title" = "Auto-Delete Messages";
|
||||
"lng_settings_ttl_about" = "Automatically delete messages for everyone after a period of time in all new chats you start.";
|
||||
"lng_settings_ttl_after" = "After {after_duration}";
|
||||
|
@ -1195,6 +1207,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_settings_quick_dialog_action_delete" = "Delete";
|
||||
"lng_settings_quick_dialog_action_disabled" = "Change folder";
|
||||
|
||||
"lng_quick_dialog_action_toast_mute_success" = "Notifications for this chat have been muted.";
|
||||
"lng_quick_dialog_action_toast_unmute_success" = "Notifications enabled for this chat.";
|
||||
"lng_quick_dialog_action_toast_pin_success" = "The chat has been pinned.";
|
||||
"lng_quick_dialog_action_toast_unpin_success" = "The chat has been unpinned.";
|
||||
"lng_quick_dialog_action_toast_read_success" = "The chat has been marked as read.";
|
||||
"lng_quick_dialog_action_toast_unread_success" = "The chat has been marked as unread.";
|
||||
"lng_quick_dialog_action_toast_archive_success" = "The chat has been archived.";
|
||||
"lng_quick_dialog_action_toast_unarchive_success" = "The chat has been unarchived.";
|
||||
|
||||
"lng_settings_generic_subscribe" = "Subscribe to {link} to use this setting.";
|
||||
"lng_settings_generic_subscribe_link" = "Telegram Premium";
|
||||
|
||||
|
@ -2212,6 +2233,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_you_paid_stars#one" = "You paid {count} Star.";
|
||||
"lng_you_paid_stars#other" = "You paid {count} Stars.";
|
||||
|
||||
"lng_you_joined_group" = "You joined this group";
|
||||
|
||||
"lng_similar_channels_title" = "Similar channels";
|
||||
"lng_similar_channels_view_all" = "View all";
|
||||
"lng_similar_channels_more" = "More Channels";
|
||||
|
@ -3444,6 +3467,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_gift_show_on_channel" = "Display in channel's Gifts";
|
||||
"lng_gift_availability" = "Availability";
|
||||
"lng_gift_from_hidden" = "Hidden User";
|
||||
"lng_gift_subtitle_birthdays" = "Birthdays";
|
||||
"lng_gift_list_birthday_status_today" = "{emoji} Birthday today";
|
||||
"lng_gift_list_birthday_status_yesterday" = "Birthday yesterday";
|
||||
"lng_gift_list_birthday_status_tomorrow" = "Birthday tomorrow";
|
||||
"lng_gift_self_status" = "buy yourself a gift";
|
||||
"lng_gift_self_title" = "Buy a Gift";
|
||||
"lng_gift_self_about" = "Buy yourself a gift to display on your page or reserve for later.\n\nLimited-edition gifts upgraded to collectibles can be gifted to others later.";
|
||||
|
@ -3677,6 +3704,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_restrict_users" = "Restrict users";
|
||||
"lng_delete_all_from_user" = "Delete all from {user}";
|
||||
"lng_delete_all_from_users" = "Delete all from users";
|
||||
"lng_restrict_user#one" = "Restrict user";
|
||||
"lng_restrict_user#other" = "Restrict users";
|
||||
"lng_restrict_user_part" = "Partially restrict this user {emoji}";
|
||||
"lng_restrict_users_part" = "Partially restrict users {emoji}";
|
||||
"lng_restrict_user_full" = "Fully ban this user {emoji}";
|
||||
|
@ -3854,6 +3883,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_dialogs_skip_archive_in_search" = "Skip results from archive";
|
||||
"lng_dialogs_show_archive_in_search" = "With results from archive";
|
||||
|
||||
"lng_dialogs_suggestions_birthday_title" = "Add your birthday! 🎂";
|
||||
"lng_dialogs_suggestions_birthday_about" = "Let your contacts know when you’re celebrating.";
|
||||
"lng_dialogs_suggestions_birthday_contact_title" = "It’s {text}'s **birthday** today! 🎂";
|
||||
"lng_dialogs_suggestions_birthday_contact_about" = "Send them a Gift.";
|
||||
"lng_dialogs_suggestions_birthday_contacts_title#one" = "{count} contact have **birthdays** today! 🎂";
|
||||
"lng_dialogs_suggestions_birthday_contacts_title#other" = "{count} contacts have **birthdays** today! 🎂";
|
||||
"lng_dialogs_suggestions_birthday_contacts_about" = "Send them a Gift.";
|
||||
"lng_dialogs_suggestions_birthday_contact_dismiss" = "You can send a Gift later in Settings";
|
||||
"lng_dialogs_suggestions_premium_annual_title" = "Telegram Premium with a {text} discount";
|
||||
"lng_dialogs_suggestions_premium_annual_about" = "Sign up for the annual payment plan for Telegram Premium now to get the discount.";
|
||||
"lng_dialogs_suggestions_premium_upgrade_title" = "Telegram Premium with a {text} discount";
|
||||
"lng_dialogs_suggestions_premium_upgrade_about" = "Upgrade to the annual payment plan for Telegram Premium now to get the discount.";
|
||||
"lng_dialogs_suggestions_premium_restore_title" = "Get Premium back with up to {text} off";
|
||||
"lng_dialogs_suggestions_premium_restore_about" = "Your Telegram Premium has recently expired. Tap here to extend it.";
|
||||
"lng_dialogs_suggestions_premium_grace_title" = "⚠️ Your Premium subscription is expiring!";
|
||||
"lng_dialogs_suggestions_premium_grace_about" = "Don’t lose access to exclusive features.";
|
||||
"lng_dialogs_suggestions_userpics_title" = "Add your photo! 📸";
|
||||
"lng_dialogs_suggestions_userpics_about" = "Help your friends spot you easily.";
|
||||
"lng_dialogs_suggestions_credits_sub_low_title#one" = "{emoji} {count} Star needed for {channels}";
|
||||
"lng_dialogs_suggestions_credits_sub_low_title#other" = "{emoji} {count} Stars needed for {channels}";
|
||||
"lng_dialogs_suggestions_credits_sub_low_about" = "Insufficient funds to cover your subscription.";
|
||||
|
||||
"lng_about_random" = "Send a {emoji} emoji to any chat to try your luck.";
|
||||
"lng_about_random_send" = "Send";
|
||||
|
||||
|
@ -4606,6 +4657,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_call_status_failed" = "failed to connect";
|
||||
"lng_call_status_ringing" = "ringing...";
|
||||
"lng_call_status_busy" = "line busy";
|
||||
"lng_call_status_group_invite" = "Telegram Group Call";
|
||||
"lng_call_status_sure" = "Click on the Camera icon if you want to start a video call.";
|
||||
"lng_call_fingerprint_tooltip" = "If the emoji on {user}'s screen are the same, this call is 100% secure";
|
||||
|
||||
|
@ -4615,6 +4667,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_call_error_camera_not_started" = "You can switch to video call once you're connected.";
|
||||
"lng_call_error_camera_outdated" = "{user}'s app does not support video calls. They need to update their app before you can call them.";
|
||||
"lng_call_error_audio_io" = "There seems to be a problem with your sound card. Please make sure that your computer's speakers and microphone are working and try again.";
|
||||
"lng_call_error_add_not_started" = "You can add more people once you're connected.";
|
||||
|
||||
"lng_call_bar_hangup" = "End call";
|
||||
"lng_call_leave_to_other_sure" = "End your active call and join this video chat?";
|
||||
|
@ -4633,16 +4686,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
"lng_call_outgoing" = "Outgoing call";
|
||||
"lng_call_video_outgoing" = "Outgoing video call";
|
||||
"lng_call_group_outgoing" = "Outgoing group call";
|
||||
"lng_call_incoming" = "Incoming call";
|
||||
"lng_call_video_incoming" = "Incoming video call";
|
||||
"lng_call_group_incoming" = "Incoming group call";
|
||||
"lng_call_missed" = "Missed call";
|
||||
"lng_call_video_missed" = "Missed video call";
|
||||
"lng_call_group_missed" = "Missed group call";
|
||||
"lng_call_cancelled" = "Canceled call";
|
||||
"lng_call_video_cancelled" = "Canceled video call";
|
||||
"lng_call_declined" = "Declined call";
|
||||
"lng_call_video_declined" = "Declined video call";
|
||||
"lng_call_group_declined" = "Declined group call";
|
||||
"lng_call_duration_info" = "{time}, {duration}";
|
||||
"lng_call_type_and_duration" = "{type} ({duration})";
|
||||
"lng_call_invitation" = "Group call invitation";
|
||||
"lng_call_ongoing" = "Ongoing group call";
|
||||
|
||||
"lng_call_rate_label" = "Please rate the quality of your call";
|
||||
"lng_call_rate_comment" = "Comment (optional)";
|
||||
|
@ -4651,6 +4710,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_call_start_video" = "Start Video";
|
||||
"lng_call_stop_video" = "Stop Video";
|
||||
"lng_call_screencast" = "Screencast";
|
||||
"lng_call_add_people" = "Add People";
|
||||
"lng_call_end_call" = "End Call";
|
||||
"lng_call_mute_audio" = "Mute";
|
||||
"lng_call_unmute_audio" = "Unmute";
|
||||
|
@ -4682,8 +4742,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_group_call_connecting" = "Connecting...";
|
||||
"lng_group_call_leave" = "Leave";
|
||||
"lng_group_call_leave_title" = "Leave video chat";
|
||||
"lng_group_call_leave_title_call" = "Leave group call";
|
||||
"lng_group_call_leave_title_channel" = "Leave live stream";
|
||||
"lng_group_call_leave_sure" = "Do you want to leave this video chat?";
|
||||
"lng_group_call_leave_sure_call" = "Do you want to leave this group call?";
|
||||
"lng_group_call_leave_sure_channel" = "Are you sure you want to leave this live stream?";
|
||||
"lng_group_call_close" = "Close";
|
||||
"lng_group_call_close_sure" = "Video chat is scheduled. You can abort it or just close this panel.";
|
||||
|
@ -4713,15 +4775,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_group_call_also_end_channel" = "End live stream";
|
||||
"lng_group_call_settings_title" = "Settings";
|
||||
"lng_group_call_invite" = "Invite Members";
|
||||
"lng_group_call_invite_conf" = "Add People";
|
||||
"lng_group_call_invited_status" = "invited";
|
||||
"lng_group_call_calling_status" = "calling...";
|
||||
"lng_group_call_blockchain_only_status" = "listening";
|
||||
"lng_group_call_muted_by_me_status" = "muted for you";
|
||||
"lng_group_call_invite_title" = "Invite members";
|
||||
"lng_group_call_invite_button" = "Invite";
|
||||
"lng_group_call_confcall_add" = "Call";
|
||||
"lng_group_call_add_to_group_one" = "{user} isn't a member of «{group}». Add them to the group?";
|
||||
"lng_group_call_add_to_group_some" = "Some of those users aren't members of «{group}». Add them to the group?";
|
||||
"lng_group_call_add_to_group_all" = "Those users aren't members of «{group}». Add them to the group?";
|
||||
"lng_group_call_invite_members" = "Group members";
|
||||
"lng_group_call_invite_search_results" = "Search results";
|
||||
"lng_group_call_invite_limit" = "This is currently the maximum allowed number of participants.";
|
||||
"lng_group_call_new_muted" = "Mute new participants";
|
||||
"lng_group_call_speakers" = "Speakers";
|
||||
"lng_group_call_microphone" = "Microphone";
|
||||
|
@ -4760,6 +4827,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_group_call_context_pin_screen" = "Pin screencast";
|
||||
"lng_group_call_context_unpin_screen" = "Unpin screencast";
|
||||
"lng_group_call_context_remove" = "Remove";
|
||||
"lng_group_call_context_cancel_invite" = "Discard invite";
|
||||
"lng_group_call_context_stop_ringing" = "Stop calling";
|
||||
"lng_group_call_context_ban_from_call" = "Ban from call";
|
||||
"lng_group_call_remove_channel" = "Remove {channel} from the video chat and ban them?";
|
||||
"lng_group_call_remove_channel_from_channel" = "Remove {channel} from the live stream?";
|
||||
"lng_group_call_mac_access" = "Telegram Desktop does not have access to system wide keyboard input required for Push to Talk.";
|
||||
|
@ -4868,6 +4938,54 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_group_call_rtmp_viewers#one" = "{count} viewer";
|
||||
"lng_group_call_rtmp_viewers#other" = "{count} viewers";
|
||||
|
||||
"lng_confcall_join_title" = "Group Call";
|
||||
"lng_confcall_join_text" = "You are invited to join a Telegram Call.";
|
||||
"lng_confcall_join_text_inviter" = "{user} is inviting you to join a Telegram Call.";
|
||||
"lng_confcall_already_joined_one" = "{user} already joined this call.";
|
||||
"lng_confcall_already_joined_two" = "{user} and {other} already joined this call.";
|
||||
"lng_confcall_already_joined_three" = "{user}, {other} and {third} already joined this call.";
|
||||
"lng_confcall_already_joined_many#one" = "{user}, {other} and **{count}** other person already joined this call.";
|
||||
"lng_confcall_already_joined_many#other" = "{user}, {other} and **{count}** other people already joined this call.";
|
||||
"lng_confcall_join_button" = "Join Group Call";
|
||||
"lng_confcall_create_call" = "Start New Call";
|
||||
"lng_confcall_create_call_description#one" = "You can add up to {count} participant to a call.";
|
||||
"lng_confcall_create_call_description#other" = "You can add up to {count} participants to a call.";
|
||||
"lng_confcall_create_title" = "New Call";
|
||||
"lng_confcall_create_link" = "Create Call Link";
|
||||
"lng_confcall_create_link_description" = "You can create a link that will allow your friends on Telegram to join the call.";
|
||||
"lng_confcall_link_revoke" = "Revoke link";
|
||||
"lng_confcall_link_revoked_title" = "Link Revoked";
|
||||
"lng_confcall_link_inactive" = "This link is no longer active.";
|
||||
"lng_confcall_link_revoked_text" = "A new link has been generated.";
|
||||
"lng_confcall_link_title" = "Call Link";
|
||||
"lng_confcall_link_about" = "Anyone on Telegram can join your call by following the link below.";
|
||||
"lng_confcall_link_or" = "or";
|
||||
"lng_confcall_link_join" = "Be the first to join the call and add people from there. {link}";
|
||||
"lng_confcall_link_join_link" = "Open call {arrow}";
|
||||
"lng_confcall_inactive_title" = "Start Group Call";
|
||||
"lng_confcall_inactive_about" = "This call is no longer active.\nYou can start a new one.";
|
||||
"lng_confcall_invite_done_user" = "You're calling {user} to join.";
|
||||
"lng_confcall_invite_done_many#one" = "You're calling **{count} person** to join.";
|
||||
"lng_confcall_invite_done_many#other" = "You're calling **{count} people** to join.";
|
||||
"lng_confcall_invite_already_user" = "{user} is already in the call.";
|
||||
"lng_confcall_invite_already_many#one" = "**{count} person** is already in the call.";
|
||||
"lng_confcall_invite_already_many#other" = "**{count} people** are already in the call.";
|
||||
"lng_confcall_invite_privacy_user" = "You cannot call {user} because of their privacy settings.";
|
||||
"lng_confcall_invite_privacy_many#one" = "You cannot call **{count} person** because of their privacy settings.";
|
||||
"lng_confcall_invite_privacy_many#other" = "You cannot call **{count} people** because of their privacy settings.";
|
||||
"lng_confcall_invite_fail_user" = "Couldn't call {user} to join.";
|
||||
"lng_confcall_invite_fail_many#one" = "Couldn't call **{count} person** to join.";
|
||||
"lng_confcall_invite_fail_many#other" = "Couldn't call **{count} people** to join.";
|
||||
"lng_confcall_invite_kicked_user" = "{user} was banned from the call.";
|
||||
"lng_confcall_invite_kicked_many#one" = "**{count} person** was removed from the call.";
|
||||
"lng_confcall_invite_kicked_many#other" = "**{count} people** were removed from the call.";
|
||||
"lng_confcall_not_accessible" = "This call is no longer accessible.";
|
||||
"lng_confcall_participants_limit" = "This call reached the participants limit.";
|
||||
"lng_confcall_sure_remove" = "Remove {user} from the call?";
|
||||
"lng_confcall_e2e_badge" = "End-to-End Encrypted";
|
||||
"lng_confcall_e2e_badge_small" = "E2E Encrypted";
|
||||
"lng_confcall_e2e_about" = "These four emoji represent the call's encryption key. They must match for all participants and change when someone joins or leaves.";
|
||||
|
||||
"lng_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages.";
|
||||
|
||||
"lng_player_message_today" = "today at {time}";
|
||||
|
@ -5794,6 +5912,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_view_button_stickerset" = "View stickers";
|
||||
"lng_view_button_emojipack" = "View emoji";
|
||||
"lng_view_button_collectible" = "View collectible";
|
||||
"lng_view_button_call" = "Join call";
|
||||
|
||||
"lng_sponsored_hide_ads" = "Hide";
|
||||
"lng_sponsored_title" = "What are sponsored messages?";
|
||||
|
@ -6185,6 +6304,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_bot_earn_chart_revenue" = "Revenue";
|
||||
"lng_bot_earn_overview_title" = "Proceeds overview";
|
||||
"lng_bot_earn_available" = "Available balance";
|
||||
"lng_bot_earn_reward" = "Total balance";
|
||||
"lng_bot_earn_total" = "Total lifetime proceeds";
|
||||
"lng_bot_earn_balance_title" = "Available balance";
|
||||
"lng_bot_earn_balance_about" = "Stars from your total balance can be used for ads or withdrawn as rewards 21 days after they are earned.";
|
||||
|
@ -6320,6 +6440,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_frozen_text3" = "Appeal via {link} before {date}, or your account will be deleted.";
|
||||
"lng_frozen_appeal_button" = "Submit an Appeal";
|
||||
|
||||
"lng_context_bank_card_copy" = "Copy Card Number";
|
||||
"lng_context_bank_card_copied" = "Card number copied to clipboard.";
|
||||
|
||||
// Wnd specific
|
||||
|
||||
"lng_wnd_choose_program_menu" = "Choose Default Program...";
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="5.13.1.0" />
|
||||
Version="5.14.1.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
|
|
@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
|||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,13,1,0
|
||||
PRODUCTVERSION 5,13,1,0
|
||||
FILEVERSION 5,14,1,0
|
||||
PRODUCTVERSION 5,14,1,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
|
@ -62,10 +62,10 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Radolyn Labs"
|
||||
VALUE "FileDescription", "AyuGram Desktop"
|
||||
VALUE "FileVersion", "5.13.1.0"
|
||||
VALUE "FileVersion", "5.14.1.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "AyuGram Desktop"
|
||||
VALUE "ProductVersion", "5.13.1.0"
|
||||
VALUE "ProductVersion", "5.14.1.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
|||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,13,1,0
|
||||
PRODUCTVERSION 5,13,1,0
|
||||
FILEVERSION 5,14,1,0
|
||||
PRODUCTVERSION 5,14,1,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
|
@ -53,10 +53,10 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Radolyn Labs"
|
||||
VALUE "FileDescription", "AyuGram Desktop Updater"
|
||||
VALUE "FileVersion", "5.13.1.0"
|
||||
VALUE "FileVersion", "5.14.1.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "AyuGram Desktop"
|
||||
VALUE "ProductVersion", "5.13.1.0"
|
||||
VALUE "ProductVersion", "5.14.1.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -348,12 +348,15 @@ void CreditsHistory::request(
|
|||
|
||||
void CreditsHistory::requestSubscriptions(
|
||||
const Data::CreditsStatusSlice::OffsetToken &token,
|
||||
Fn<void(Data::CreditsStatusSlice)> done) {
|
||||
Fn<void(Data::CreditsStatusSlice)> done,
|
||||
bool missingBalance) {
|
||||
if (_requestId) {
|
||||
return;
|
||||
}
|
||||
_requestId = _api.request(MTPpayments_GetStarsSubscriptions(
|
||||
MTP_flags(0),
|
||||
MTP_flags(missingBalance
|
||||
? MTPpayments_getStarsSubscriptions::Flag::f_missing_balance
|
||||
: MTPpayments_getStarsSubscriptions::Flags(0)),
|
||||
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
|
||||
MTP_string(token)
|
||||
)).done([=](const MTPpayments_StarsStatus &result) {
|
||||
|
|
|
@ -82,7 +82,8 @@ public:
|
|||
Fn<void(Data::CreditsStatusSlice)> done);
|
||||
void requestSubscriptions(
|
||||
const Data::CreditsStatusSlice::OffsetToken &token,
|
||||
Fn<void(Data::CreditsStatusSlice)> done);
|
||||
Fn<void(Data::CreditsStatusSlice)> done,
|
||||
bool missingBalance = false);
|
||||
|
||||
private:
|
||||
using HistoryTL = MTPpayments_GetStarsTransactions;
|
||||
|
|
|
@ -28,8 +28,8 @@ constexpr auto kSearchPerPage = 50;
|
|||
auto result = MessageIdsList();
|
||||
for (const auto &message : messages) {
|
||||
const auto peerId = PeerFromMessage(message);
|
||||
if (const auto peer = data->peerLoaded(peerId)) {
|
||||
if (const auto lastDate = DateFromMessage(message)) {
|
||||
if (data->peerLoaded(peerId)) {
|
||||
if (DateFromMessage(message)) {
|
||||
const auto item = data->addNewMessage(
|
||||
message,
|
||||
MessageFlags(),
|
||||
|
|
|
@ -25,6 +25,7 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption(
|
|||
* kDiscountDivider;
|
||||
}();
|
||||
return {
|
||||
.months = months,
|
||||
.duration = Ui::FormatTTL(months * 86400 * 31),
|
||||
.discount = (discount > 0)
|
||||
? QString::fromUtf8("\xe2\x88\x92%1%").arg(discount)
|
||||
|
@ -32,6 +33,9 @@ Data::PremiumSubscriptionOption CreateSubscriptionOption(
|
|||
.costPerMonth = Ui::FillAmountAndCurrency(
|
||||
amount / float64(months),
|
||||
currency),
|
||||
.costNoDiscount = Ui::FillAmountAndCurrency(
|
||||
monthlyAmount * months,
|
||||
currency),
|
||||
.costTotal = Ui::FillAmountAndCurrency(amount, currency),
|
||||
.botUrl = botUrl,
|
||||
};
|
||||
|
|
|
@ -313,7 +313,7 @@ void PublicForwards::request(
|
|||
const auto msgId = IdFromMessage(message);
|
||||
const auto peerId = PeerFromMessage(message);
|
||||
const auto lastDate = DateFromMessage(message);
|
||||
if (const auto peer = owner.peerLoaded(peerId)) {
|
||||
if (owner.peerLoaded(peerId)) {
|
||||
if (!lastDate) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -202,7 +202,11 @@ EntitiesInText EntitiesFromMTP(
|
|||
d.vlength().v,
|
||||
});
|
||||
}, [&](const MTPDmessageEntityBankCard &d) {
|
||||
// Skipping cards. // #TODO entities
|
||||
result.push_back({
|
||||
EntityType::BankCard,
|
||||
d.voffset().v,
|
||||
d.vlength().v,
|
||||
});
|
||||
}, [&](const MTPDmessageEntitySpoiler &d) {
|
||||
result.push_back({
|
||||
EntityType::Spoiler,
|
||||
|
@ -273,6 +277,9 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
|
|||
case EntityType::Phone: {
|
||||
v.push_back(MTP_messageEntityPhone(offset, length));
|
||||
} break;
|
||||
case EntityType::BankCard: {
|
||||
v.push_back(MTP_messageEntityBankCard(offset, length));
|
||||
} break;
|
||||
case EntityType::Hashtag: {
|
||||
v.push_back(MTP_messageEntityHashtag(offset, length));
|
||||
} break;
|
||||
|
|
|
@ -307,14 +307,19 @@ void Updates::feedUpdateVector(
|
|||
auto list = updates.v;
|
||||
const auto hasGroupCallParticipantUpdates = ranges::contains(
|
||||
list,
|
||||
mtpc_updateGroupCallParticipants,
|
||||
&MTPUpdate::type);
|
||||
true,
|
||||
[](const MTPUpdate &update) {
|
||||
return update.type() == mtpc_updateGroupCallParticipants
|
||||
|| update.type() == mtpc_updateGroupCallChainBlocks;
|
||||
});
|
||||
if (hasGroupCallParticipantUpdates) {
|
||||
ranges::stable_sort(list, std::less<>(), [](const MTPUpdate &entry) {
|
||||
if (entry.type() == mtpc_updateGroupCallParticipants) {
|
||||
if (entry.type() == mtpc_updateGroupCallChainBlocks) {
|
||||
return 0;
|
||||
} else {
|
||||
} else if (entry.type() == mtpc_updateGroupCallParticipants) {
|
||||
return 1;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
});
|
||||
} else if (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants) {
|
||||
|
@ -328,7 +333,8 @@ void Updates::feedUpdateVector(
|
|||
if ((policy == SkipUpdatePolicy::SkipMessageIds
|
||||
&& type == mtpc_updateMessageID)
|
||||
|| (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants
|
||||
&& type != mtpc_updateGroupCallParticipants)) {
|
||||
&& type != mtpc_updateGroupCallParticipants
|
||||
&& type != mtpc_updateGroupCallChainBlocks)) {
|
||||
continue;
|
||||
}
|
||||
feedUpdate(entry);
|
||||
|
@ -958,7 +964,8 @@ void Updates::applyGroupCallParticipantUpdates(const MTPUpdates &updates) {
|
|||
data.vupdates(),
|
||||
SkipUpdatePolicy::SkipExceptGroupCallParticipants);
|
||||
}, [&](const MTPDupdateShort &data) {
|
||||
if (data.vupdate().type() == mtpc_updateGroupCallParticipants) {
|
||||
if (data.vupdate().type() == mtpc_updateGroupCallParticipants
|
||||
|| data.vupdate().type() == mtpc_updateGroupCallChainBlocks) {
|
||||
feedUpdate(data.vupdate());
|
||||
}
|
||||
}, [](const auto &) {
|
||||
|
@ -1317,7 +1324,7 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) {
|
|||
user->madeAction(base::unixtime::now());
|
||||
}
|
||||
}
|
||||
ClearMediaAsExpired(item);
|
||||
item->clearMediaAsExpired();
|
||||
}
|
||||
} else {
|
||||
// Perhaps it was an unread mention!
|
||||
|
@ -2118,6 +2125,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
|||
case mtpc_updatePhoneCall:
|
||||
case mtpc_updatePhoneCallSignalingData:
|
||||
case mtpc_updateGroupCallParticipants:
|
||||
case mtpc_updateGroupCallChainBlocks:
|
||||
case mtpc_updateGroupCallConnection:
|
||||
case mtpc_updateGroupCall: {
|
||||
Core::App().calls().handleUpdate(&session(), update);
|
||||
|
|
|
@ -24,6 +24,14 @@ UserpicButton {
|
|||
uploadIcon: icon;
|
||||
uploadIconPosition: point;
|
||||
}
|
||||
UserpicsRow {
|
||||
button: UserpicButton;
|
||||
bg: color;
|
||||
shift: pixels;
|
||||
stroke: pixels;
|
||||
complex: bool;
|
||||
invert: bool;
|
||||
}
|
||||
ShortInfoBox {
|
||||
label: FlatLabel;
|
||||
labeled: FlatLabel;
|
||||
|
@ -1129,3 +1137,10 @@ foldersMenu: PopupMenu(popupMenuWithIcons) {
|
|||
itemPadding: margins(54px, 8px, 44px, 8px);
|
||||
}
|
||||
}
|
||||
|
||||
fakeUserpicButton: UserpicButton(defaultUserpicButton) {
|
||||
size: size(1px, 1px);
|
||||
photoSize: 1px;
|
||||
changeIcon: icon {{ "settings/photo", transparent }};
|
||||
uploadBg: transparent;
|
||||
}
|
||||
|
|
|
@ -371,7 +371,9 @@ void CreateModerateMessagesBox(
|
|||
box,
|
||||
rpl::conditional(
|
||||
ownedWrap->toggledValue(),
|
||||
tr::lng_context_restrict_user(),
|
||||
tr::lng_restrict_user(
|
||||
lt_count,
|
||||
rpl::single(participants.size()) | tr::to_count()),
|
||||
rpl::conditional(
|
||||
rpl::single(isSingle),
|
||||
tr::lng_ban_user(),
|
||||
|
|
|
@ -142,15 +142,16 @@ void PeerListBox::createMultiSelect() {
|
|||
}
|
||||
});
|
||||
_select->resizeToWidth(_controller->contentWidth());
|
||||
_select->moveToLeft(0, 0);
|
||||
_select->moveToLeft(0, topSelectSkip());
|
||||
}
|
||||
|
||||
void PeerListBox::appendQueryChangedCallback(Fn<void(QString)> callback) {
|
||||
_customQueryChangedCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void PeerListBox::setAddedTopScrollSkip(int skip) {
|
||||
void PeerListBox::setAddedTopScrollSkip(int skip, bool aboveSearch) {
|
||||
_addedTopScrollSkip = skip;
|
||||
_addedTopScrollAboveSearch = aboveSearch;
|
||||
_scrollBottomFixed = false;
|
||||
updateScrollSkips();
|
||||
}
|
||||
|
@ -159,7 +160,7 @@ void PeerListBox::showFinished() {
|
|||
_controller->showFinished();
|
||||
}
|
||||
|
||||
int PeerListBox::getTopScrollSkip() const {
|
||||
int PeerListBox::topScrollSkip() const {
|
||||
auto result = _addedTopScrollSkip;
|
||||
if (_select && !_select->isHidden()) {
|
||||
result += _select->height();
|
||||
|
@ -167,12 +168,19 @@ int PeerListBox::getTopScrollSkip() const {
|
|||
return result;
|
||||
}
|
||||
|
||||
int PeerListBox::topSelectSkip() const {
|
||||
return _addedTopScrollAboveSearch ? _addedTopScrollSkip : 0;
|
||||
}
|
||||
|
||||
void PeerListBox::updateScrollSkips() {
|
||||
// If we show / hide the search field scroll top is fixed.
|
||||
// If we resize search field by bubbles scroll bottom is fixed.
|
||||
setInnerTopSkip(getTopScrollSkip(), _scrollBottomFixed);
|
||||
if (_select && !_select->animating()) {
|
||||
_scrollBottomFixed = true;
|
||||
setInnerTopSkip(topScrollSkip(), _scrollBottomFixed);
|
||||
if (_select) {
|
||||
_select->moveToLeft(0, topSelectSkip());
|
||||
if (!_select->animating()) {
|
||||
_scrollBottomFixed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,8 +244,6 @@ void PeerListBox::resizeEvent(QResizeEvent *e) {
|
|||
|
||||
if (_select) {
|
||||
_select->resizeToWidth(width());
|
||||
_select->moveToLeft(0, 0);
|
||||
|
||||
updateScrollSkips();
|
||||
}
|
||||
|
||||
|
@ -1995,9 +2001,13 @@ PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
|
|||
_selected.index.value = newSelectedIndex;
|
||||
_selected.element = 0;
|
||||
if (newSelectedIndex >= 0) {
|
||||
auto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : 0;
|
||||
auto top = (newSelectedIndex > 0) ? getRowTop(RowIndex(newSelectedIndex)) : _aboveHeight;
|
||||
auto bottom = (newSelectedIndex + 1 < rowsCount) ? getRowTop(RowIndex(newSelectedIndex + 1)) : height();
|
||||
_scrollToRequests.fire({ top, bottom });
|
||||
} else if (!_selected.index.value && direction < 0) {
|
||||
auto top = 0;
|
||||
auto bottom = _aboveHeight;
|
||||
_scrollToRequests.fire({ top, bottom });
|
||||
}
|
||||
|
||||
update();
|
||||
|
|
|
@ -1142,7 +1142,7 @@ public:
|
|||
void peerListScrollToTop() override;
|
||||
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
|
||||
|
||||
void setAddedTopScrollSkip(int skip);
|
||||
void setAddedTopScrollSkip(int skip, bool aboveSearch = false);
|
||||
|
||||
void showFinished() override;
|
||||
|
||||
|
@ -1178,7 +1178,8 @@ private:
|
|||
PaintRoundImageCallback paintUserpic,
|
||||
anim::type animated);
|
||||
void createMultiSelect();
|
||||
int getTopScrollSkip() const;
|
||||
[[nodiscard]] int topScrollSkip() const;
|
||||
[[nodiscard]] int topSelectSkip() const;
|
||||
void updateScrollSkips();
|
||||
void searchQueryChanged(const QString &query);
|
||||
|
||||
|
@ -1189,6 +1190,7 @@ private:
|
|||
std::unique_ptr<PeerListController> _controller;
|
||||
Fn<void(PeerListBox*)> _init;
|
||||
bool _scrollBottomFixed = false;
|
||||
bool _addedTopScrollAboveSearch = false;
|
||||
int _addedTopScrollSkip = 0;
|
||||
|
||||
};
|
||||
|
|
|
@ -179,6 +179,7 @@ void FillUpgradeToPremiumCover(
|
|||
CreateUserpicsWithMoreBadge(
|
||||
container,
|
||||
rpl::single(std::move(userpicPeers)),
|
||||
st::boostReplaceUserpicsRow,
|
||||
kUserpicsLimit),
|
||||
st::inviteForbiddenUserpicsPadding)
|
||||
)->entity()->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
@ -808,7 +809,7 @@ void AddParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {
|
|||
updateTitle();
|
||||
} else if (const auto channel = _peer ? _peer->asChannel() : nullptr) {
|
||||
if (!_peer->isMegagroup()) {
|
||||
showBox(Box<MaxInviteBox>(_peer->asChannel()));
|
||||
showBox(Box<MaxInviteBox>(channel));
|
||||
}
|
||||
} else if (count >= serverConfig.chatSizeMax
|
||||
&& count < serverConfig.megagroupSizeMax) {
|
||||
|
|
|
@ -453,7 +453,7 @@ void ChoosePeerBoxController::rowClicked(not_null<PeerListRow*> row) {
|
|||
const auto onstack = callback;
|
||||
onstack({ peer });
|
||||
};
|
||||
if (const auto user = peer->asUser()) {
|
||||
if (peer->isUser()) {
|
||||
done();
|
||||
} else {
|
||||
delegate()->peerListUiShow()->showBox(
|
||||
|
|
|
@ -689,7 +689,7 @@ UserData *ParticipantsAdditionalData::applyAdmin(
|
|||
const auto user = _peer->owner().userLoaded(data.userId());
|
||||
if (!user) {
|
||||
return nullptr;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
} else if (_peer->isChat()) {
|
||||
// This can come from saveAdmin callback.
|
||||
_admins.emplace(user);
|
||||
return user;
|
||||
|
@ -733,7 +733,7 @@ UserData *ParticipantsAdditionalData::applyRegular(UserId userId) {
|
|||
const auto user = _peer->owner().userLoaded(userId);
|
||||
if (!user) {
|
||||
return nullptr;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
} else if (_peer->isChat()) {
|
||||
// This can come from saveAdmin or saveRestricted callback.
|
||||
_admins.erase(user);
|
||||
return user;
|
||||
|
@ -913,7 +913,7 @@ void ParticipantsBoxController::setupListChangeViewers() {
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (const auto row = delegate()->peerListFindRow(user->id.value)) {
|
||||
if (delegate()->peerListFindRow(user->id.value)) {
|
||||
delegate()->peerListPartitionRows([&](const PeerListRow &row) {
|
||||
return (row.peer() == user);
|
||||
});
|
||||
|
|
|
@ -361,7 +361,7 @@ bool ProcessCurrent(
|
|||
&& peer->asUser()->hasPersonalPhoto())
|
||||
? tr::lng_profile_photo_by_you(tr::now)
|
||||
: ((state->current.index == (state->current.count - 1))
|
||||
&& SyncUserFallbackPhotoViewer(peer->asUser()))
|
||||
&& SyncUserFallbackPhotoViewer(peer->asUser()) == state->photoId)
|
||||
? tr::lng_profile_public_photo(tr::now)
|
||||
: QString();
|
||||
state->waitingLoad = false;
|
||||
|
|
|
@ -550,13 +550,13 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
|||
rpl::variable<int> count = 0;
|
||||
bool painting = false;
|
||||
};
|
||||
const auto full = st::boostReplaceUserpic.size.height()
|
||||
const auto st = &st::boostReplaceUserpicsRow;
|
||||
const auto full = st->button.size.height()
|
||||
+ st::boostReplaceIconAdd.y()
|
||||
+ st::lineWidth;
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
|
||||
const auto raw = result.data();
|
||||
const auto &st = st::boostReplaceUserpic;
|
||||
const auto right = CreateChild<Ui::UserpicButton>(raw, to, st);
|
||||
const auto right = CreateChild<Ui::UserpicButton>(raw, to, st->button);
|
||||
const auto overlay = CreateChild<Ui::RpWidget>(raw);
|
||||
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
|
@ -564,7 +564,6 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
|||
from
|
||||
) | rpl::start_with_next([=](
|
||||
const std::vector<not_null<PeerData*>> &list) {
|
||||
const auto &st = st::boostReplaceUserpic;
|
||||
auto was = base::take(state->from);
|
||||
auto buttons = base::take(state->buttons);
|
||||
state->from.reserve(list.size());
|
||||
|
@ -578,7 +577,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
|||
state->buttons.push_back(std::move(buttons[index]));
|
||||
} else {
|
||||
state->buttons.push_back(
|
||||
std::make_unique<Ui::UserpicButton>(raw, peer, st));
|
||||
std::make_unique<Ui::UserpicButton>(raw, peer, st->button));
|
||||
const auto raw = state->buttons.back().get();
|
||||
base::install_event_filter(raw, [=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::Paint && !state->painting)
|
||||
|
@ -598,7 +597,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
|||
const auto skip = st::boostReplaceUserpicsSkip;
|
||||
const auto left = width - 2 * right->width() - skip;
|
||||
const auto shift = std::min(
|
||||
st::boostReplaceUserpicsShift,
|
||||
st->shift,
|
||||
(count > 1 ? (left / (count - 1)) : width));
|
||||
const auto total = right->width()
|
||||
+ (count ? (skip + right->width() + (count - 1) * shift) : 0);
|
||||
|
@ -630,7 +629,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
|||
|
||||
auto q = QPainter(&state->layer);
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
const auto stroke = st::boostReplaceIconOutline;
|
||||
const auto stroke = st->stroke;
|
||||
const auto half = stroke / 2.;
|
||||
auto pen = st::windowBg->p;
|
||||
pen.setWidthF(stroke * 2.);
|
||||
|
@ -684,6 +683,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsTransfer(
|
|||
object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<std::vector<not_null<PeerData*>>> peers,
|
||||
const style::UserpicsRow &st,
|
||||
int limit) {
|
||||
struct State {
|
||||
std::vector<not_null<PeerData*>> from;
|
||||
|
@ -693,9 +693,8 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
rpl::variable<int> count = 0;
|
||||
bool painting = false;
|
||||
};
|
||||
const auto full = st::boostReplaceUserpic.size.height()
|
||||
+ st::boostReplaceIconAdd.y()
|
||||
+ st::lineWidth;
|
||||
const auto full = st.button.size.height()
|
||||
+ (st.complex ? (st::boostReplaceIconAdd.y() + st::lineWidth) : 0);
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
|
||||
const auto raw = result.data();
|
||||
const auto overlay = CreateChild<Ui::RpWidget>(raw);
|
||||
|
@ -703,9 +702,8 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
const auto state = raw->lifetime().make_state<State>();
|
||||
std::move(
|
||||
peers
|
||||
) | rpl::start_with_next([=](
|
||||
) | rpl::start_with_next([=, &st](
|
||||
const std::vector<not_null<PeerData*>> &list) {
|
||||
const auto &st = st::boostReplaceUserpic;
|
||||
auto was = base::take(state->from);
|
||||
auto buttons = base::take(state->buttons);
|
||||
state->from.reserve(list.size());
|
||||
|
@ -719,7 +717,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
state->buttons.push_back(std::move(buttons[index]));
|
||||
} else {
|
||||
state->buttons.push_back(
|
||||
std::make_unique<Ui::UserpicButton>(raw, peer, st));
|
||||
std::make_unique<Ui::UserpicButton>(raw, peer, st.button));
|
||||
const auto raw = state->buttons.back().get();
|
||||
base::install_event_filter(raw, [=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::Paint && !state->painting)
|
||||
|
@ -732,16 +730,21 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
overlay->update();
|
||||
}, raw->lifetime());
|
||||
|
||||
if (const auto count = state->count.current()) {
|
||||
const auto single = st.button.size.width();
|
||||
const auto used = std::min(count, int(state->buttons.size()));
|
||||
const auto shift = st.shift;
|
||||
raw->resize(used ? (single + (used - 1) * shift) : 0, raw->height());
|
||||
}
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
state->count.value()
|
||||
) | rpl::start_with_next([=](int width, int count) {
|
||||
const auto &st = st::boostReplaceUserpic;
|
||||
const auto single = st.size.width();
|
||||
) | rpl::start_with_next([=, &st](int width, int count) {
|
||||
const auto single = st.button.size.width();
|
||||
const auto left = width - single;
|
||||
const auto used = std::min(count, int(state->buttons.size()));
|
||||
const auto shift = std::min(
|
||||
st::boostReplaceUserpicsShift,
|
||||
st.shift,
|
||||
(used > 1 ? (left / (used - 1)) : width));
|
||||
const auto total = used ? (single + (used - 1) * shift) : 0;
|
||||
auto x = (width - total) / 2;
|
||||
|
@ -755,7 +758,7 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
overlay->paintRequest(
|
||||
) | rpl::filter([=] {
|
||||
return !state->buttons.empty();
|
||||
}) | rpl::start_with_next([=] {
|
||||
}) | rpl::start_with_next([=, &st] {
|
||||
const auto outerw = overlay->width();
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
if (state->layer.size() != QSize(outerw, full) * ratio) {
|
||||
|
@ -768,31 +771,40 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
|||
|
||||
auto q = QPainter(&state->layer);
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
const auto stroke = st::boostReplaceIconOutline;
|
||||
const auto stroke = st.stroke;
|
||||
const auto half = stroke / 2.;
|
||||
auto pen = st::windowBg->p;
|
||||
auto pen = st.bg->p;
|
||||
pen.setWidthF(stroke * 2.);
|
||||
state->painting = true;
|
||||
for (const auto &button : state->buttons) {
|
||||
const auto paintOne = [&](not_null<Ui::UserpicButton*> button) {
|
||||
q.setPen(pen);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.drawEllipse(button->geometry());
|
||||
const auto position = button->pos();
|
||||
button->render(&q, position, QRegion(), QWidget::DrawChildren);
|
||||
};
|
||||
if (st.invert) {
|
||||
for (const auto &button : ranges::views::reverse(state->buttons)) {
|
||||
paintOne(button.get());
|
||||
}
|
||||
} else {
|
||||
for (const auto &button : state->buttons) {
|
||||
paintOne(button.get());
|
||||
}
|
||||
}
|
||||
state->painting = false;
|
||||
const auto last = state->buttons.back().get();
|
||||
const auto add = st::boostReplaceIconAdd;
|
||||
const auto skip = st::boostReplaceIconSkip;
|
||||
const auto w = st::boostReplaceIcon.width() + 2 * skip;
|
||||
const auto h = st::boostReplaceIcon.height() + 2 * skip;
|
||||
const auto x = last->x() + last->width() - w + add.x();
|
||||
const auto y = last->y() + last->height() - h + add.y();
|
||||
|
||||
const auto text = (state->count.current() > limit)
|
||||
? ('+' + QString::number(state->count.current() - limit))
|
||||
: QString();
|
||||
if (!text.isEmpty()) {
|
||||
if (st.complex && !text.isEmpty()) {
|
||||
const auto last = state->buttons.back().get();
|
||||
const auto add = st::boostReplaceIconAdd;
|
||||
const auto skip = st::boostReplaceIconSkip;
|
||||
const auto w = st::boostReplaceIcon.width() + 2 * skip;
|
||||
const auto h = st::boostReplaceIcon.height() + 2 * skip;
|
||||
const auto x = last->x() + last->width() - w + add.x();
|
||||
const auto y = last->y() + last->height() - h + add.y();
|
||||
const auto &font = st::semiboldFont;
|
||||
const auto width = font->width(text);
|
||||
const auto padded = std::max(w, width + 2 * font->spacew);
|
||||
|
|
|
@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace style {
|
||||
struct UserpicsRow;
|
||||
} // namespace style
|
||||
|
||||
class ChannelData;
|
||||
|
||||
namespace Main {
|
||||
|
@ -66,4 +70,5 @@ enum class UserpicsTransferType {
|
|||
[[nodiscard]] object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<std::vector<not_null<PeerData*>>> peers,
|
||||
const style::UserpicsRow &st,
|
||||
int limit);
|
||||
|
|
|
@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "base/unixtime.h"
|
||||
#include "boxes/filters/edit_filter_chats_list.h"
|
||||
#include "boxes/peers/edit_peer_color_box.h"
|
||||
#include "boxes/peers/prepare_short_info_box.h"
|
||||
#include "boxes/gift_premium_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
|
@ -29,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/data_birthday.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_credits.h"
|
||||
|
@ -69,6 +71,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/effects/path_shift_gradient.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/effects/premium_stars_colored.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/new_badges.h"
|
||||
#include "ui/painter.h"
|
||||
|
@ -91,6 +94,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_premium.h"
|
||||
|
@ -117,6 +121,13 @@ constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
|
|||
using namespace HistoryView;
|
||||
using namespace Info::PeerGifts;
|
||||
|
||||
enum class PickType {
|
||||
Activate,
|
||||
SendMessage,
|
||||
OpenProfile,
|
||||
};
|
||||
using PickCallback = Fn<void(not_null<PeerData*>, PickType)>;
|
||||
|
||||
struct PremiumGiftsDescriptor {
|
||||
std::vector<GiftTypePremium> list;
|
||||
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
|
||||
|
@ -141,6 +152,80 @@ struct GiftDetails {
|
|||
bool byStars = false;
|
||||
};
|
||||
|
||||
class PeerRow final : public PeerListRow {
|
||||
public:
|
||||
using PeerListRow::PeerListRow;
|
||||
|
||||
QSize rightActionSize() const override;
|
||||
QMargins rightActionMargins() const override;
|
||||
void rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
void rightActionAddRipple(
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) override;
|
||||
void rightActionStopLastRipple() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
|
||||
|
||||
};
|
||||
|
||||
QSize PeerRow::rightActionSize() const {
|
||||
return QSize(
|
||||
st::inviteLinkThreeDotsIcon.width(),
|
||||
st::inviteLinkThreeDotsIcon.height());
|
||||
}
|
||||
|
||||
QMargins PeerRow::rightActionMargins() const {
|
||||
return QMargins(
|
||||
0,
|
||||
(st::inviteLinkList.item.height - rightActionSize().height()) / 2,
|
||||
st::inviteLinkThreeDotsSkip,
|
||||
0);
|
||||
}
|
||||
|
||||
void PeerRow::rightActionPaint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(p, x, y, outerWidth);
|
||||
if (_actionRipple->empty()) {
|
||||
_actionRipple.reset();
|
||||
}
|
||||
}
|
||||
(actionSelected
|
||||
? st::inviteLinkThreeDotsIconOver
|
||||
: st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth);
|
||||
}
|
||||
|
||||
void PeerRow::rightActionAddRipple(QPoint point, Fn<void()> updateCallback) {
|
||||
if (!_actionRipple) {
|
||||
auto mask = Ui::RippleAnimation::EllipseMask(
|
||||
Size(st::inviteLinkThreeDotsIcon.height()));
|
||||
_actionRipple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::defaultRippleAnimation,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
}
|
||||
_actionRipple->add(point);
|
||||
}
|
||||
|
||||
void PeerRow::rightActionStopLastRipple() {
|
||||
if (_actionRipple) {
|
||||
_actionRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
class PreviewDelegate final : public DefaultElementDelegate {
|
||||
public:
|
||||
PreviewDelegate(
|
||||
|
@ -1793,7 +1878,7 @@ void SendGiftBox(
|
|||
ShowSentToast(window, details.descriptor, details);
|
||||
}
|
||||
if (const auto strong = weak.data()) {
|
||||
box->closeBox();
|
||||
strong->closeBox();
|
||||
}
|
||||
};
|
||||
SendGift(window, peer, api, details, done);
|
||||
|
@ -2191,10 +2276,10 @@ void GiftBox(
|
|||
&& uniqueDisallowed;
|
||||
|
||||
content->add(
|
||||
object_ptr<CenterWrap<>>(
|
||||
object_ptr<CenterWrap<UserpicButton>>(
|
||||
content,
|
||||
object_ptr<UserpicButton>(content, peer, stUser))
|
||||
)->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
)->entity()->setClickedCallback([=] { window->showPeerInfo(peer); });
|
||||
AddSkip(content);
|
||||
AddSkip(content);
|
||||
|
||||
|
@ -2257,24 +2342,46 @@ void GiftBox(
|
|||
}
|
||||
}
|
||||
|
||||
struct SelfOption {
|
||||
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> CreateRowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerData*> peer,
|
||||
PickCallback pick) {
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
st::popupMenuWithIcons);
|
||||
result->addAction(
|
||||
tr::lng_context_send_message(tr::now),
|
||||
[=] { pick(peer, PickType::SendMessage); },
|
||||
&st::menuIconChatBubble);
|
||||
result->addAction(
|
||||
tr::lng_context_view_profile(tr::now),
|
||||
[=] { pick(peer, PickType::OpenProfile); },
|
||||
&st::menuIconProfile);
|
||||
return result;
|
||||
}
|
||||
|
||||
struct CustomList {
|
||||
object_ptr<Ui::RpWidget> content = { nullptr };
|
||||
Fn<bool(int, int, int)> overrideKey;
|
||||
Fn<void()> activate;
|
||||
Fn<bool()> hasSelection;
|
||||
};
|
||||
|
||||
class Controller final : public ContactsBoxController {
|
||||
public:
|
||||
Controller(
|
||||
not_null<Main::Session*> session,
|
||||
Fn<void(not_null<PeerData*>)> choose);
|
||||
Controller(not_null<Main::Session*> session, PickCallback pick);
|
||||
|
||||
void noSearchSubmit();
|
||||
|
||||
bool overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) override;
|
||||
int toIndex) override final;
|
||||
|
||||
void rowRightActionClicked(not_null<PeerListRow*> row) override final;
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override final;
|
||||
|
||||
private:
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
|
@ -2283,41 +2390,71 @@ private:
|
|||
void prepareViewHook() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
const Fn<void(not_null<PeerData*>)> _choose;
|
||||
SelfOption _selfOption;
|
||||
const PickCallback _pick;
|
||||
const std::vector<UserId> _contactBirthdays;
|
||||
CustomList _selfOption;
|
||||
CustomList _birthdayOptions;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
bool _skipUpDirectionSelect = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] SelfOption MakeSelfOption(
|
||||
[[nodiscard]] CustomList MakeCustomList(
|
||||
not_null<Main::Session*> session,
|
||||
Fn<void()> activate) {
|
||||
class SelfController final : public PeerListController {
|
||||
Fn<void(not_null<PeerListController*>)> fill,
|
||||
PickCallback pick,
|
||||
rpl::producer<QString> below) {
|
||||
class CustomController final : public PeerListController {
|
||||
public:
|
||||
SelfController(
|
||||
CustomController(
|
||||
not_null<Main::Session*> session,
|
||||
Fn<void()> activate)
|
||||
Fn<void(not_null<PeerListController*>)> fill,
|
||||
PickCallback pick)
|
||||
: _session(session)
|
||||
, _activate(std::move(activate)) {
|
||||
, _pick(std::move(pick))
|
||||
, _fill(std::move(fill)) {
|
||||
}
|
||||
|
||||
void prepare() override {
|
||||
auto row = std::make_unique<PeerListRow>(_session->user());
|
||||
row->setCustomStatus(tr::lng_gift_self_status(tr::now));
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
delegate()->peerListRefreshRows();
|
||||
if (_fill) {
|
||||
_fill(this);
|
||||
}
|
||||
}
|
||||
void loadMoreRows() override {
|
||||
}
|
||||
void rowClicked(not_null<PeerListRow*> row) override {
|
||||
_activate();
|
||||
_pick(row->peer(), PickType::Activate);
|
||||
}
|
||||
Main::Session &session() const override {
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void rowRightActionClicked(not_null<PeerListRow*> row) override {
|
||||
delegate()->peerListShowRowMenu(row, true);
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override {
|
||||
auto result = CreateRowContextMenu(parent, row->peer(), _pick);
|
||||
|
||||
if (result) {
|
||||
base::take(_menu);
|
||||
|
||||
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<Main::Session*> _session;
|
||||
Fn<void()> _activate;
|
||||
PickCallback _pick;
|
||||
Fn<void(not_null<PeerListController*>)> _fill;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
};
|
||||
|
||||
|
@ -2329,9 +2466,12 @@ private:
|
|||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime().make_state<
|
||||
SelfController
|
||||
>(session, activate);
|
||||
const auto controller
|
||||
= container->lifetime().make_state<CustomController>(
|
||||
session,
|
||||
fill,
|
||||
pick);
|
||||
|
||||
controller->setStyleOverrides(&st::peerListSingleRow);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
|
@ -2339,10 +2479,12 @@ private:
|
|||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
|
||||
Ui::AddSkip(container);
|
||||
container->add(CreatePeerListSectionSubtitle(
|
||||
container,
|
||||
tr::lng_contacts_header()));
|
||||
if (below) {
|
||||
Ui::AddSkip(container);
|
||||
container->add(CreatePeerListSectionSubtitle(
|
||||
container,
|
||||
std::move(below)));
|
||||
}
|
||||
|
||||
const auto overrideKey = [=](int direction, int from, int to) {
|
||||
if (!content->isVisible()) {
|
||||
|
@ -2368,77 +2510,242 @@ private:
|
|||
}
|
||||
return false;
|
||||
};
|
||||
const auto hasSelection = [=] {
|
||||
return content->isVisible() && content->hasSelection();
|
||||
};
|
||||
|
||||
return {
|
||||
.content = std::move(result),
|
||||
.overrideKey = overrideKey,
|
||||
.activate = activate,
|
||||
.activate = [=] {
|
||||
if (content->hasSelection()) {
|
||||
pick(
|
||||
content->rowAt(content->selectedIndex())->peer(),
|
||||
PickType::Activate);
|
||||
}
|
||||
},
|
||||
.hasSelection = hasSelection,
|
||||
};
|
||||
}
|
||||
|
||||
Controller::Controller(
|
||||
not_null<Main::Session*> session,
|
||||
Fn<void(not_null<PeerData*>)> choose)
|
||||
Controller::Controller(not_null<Main::Session*> session, PickCallback pick)
|
||||
: ContactsBoxController(session)
|
||||
, _choose(std::move(choose))
|
||||
, _selfOption(MakeSelfOption(session, [=] { _choose(session->user()); })) {
|
||||
, _pick(std::move(pick))
|
||||
, _contactBirthdays(
|
||||
session->data().knownContactBirthdays().value_or(std::vector<UserId>{}))
|
||||
, _selfOption(
|
||||
MakeCustomList(
|
||||
session,
|
||||
[=](not_null<PeerListController*> controller) {
|
||||
auto row = std::make_unique<PeerListRow>(session->user());
|
||||
row->setCustomStatus(tr::lng_gift_self_status(tr::now));
|
||||
controller->delegate()->peerListAppendRow(std::move(row));
|
||||
controller->delegate()->peerListRefreshRows();
|
||||
},
|
||||
_pick,
|
||||
_contactBirthdays.empty()
|
||||
? tr::lng_contacts_header()
|
||||
: tr::lng_gift_subtitle_birthdays()))
|
||||
, _birthdayOptions(
|
||||
MakeCustomList(
|
||||
session,
|
||||
[=](not_null<PeerListController*> controller) {
|
||||
const auto status = [=](const Data::Birthday &date) {
|
||||
if (Data::IsBirthdayToday(date)) {
|
||||
return tr::lng_gift_list_birthday_status_today(
|
||||
tr::now,
|
||||
lt_emoji,
|
||||
Data::BirthdayCake());
|
||||
}
|
||||
const auto yesterday = QDate::currentDate().addDays(-1);
|
||||
const auto tomorrow = QDate::currentDate().addDays(1);
|
||||
if (date.day() == yesterday.day()
|
||||
&& date.month() == yesterday.month()) {
|
||||
return tr::lng_gift_list_birthday_status_yesterday(
|
||||
tr::now);
|
||||
} else if (date.day() == tomorrow.day()
|
||||
&& date.month() == tomorrow.month()) {
|
||||
return tr::lng_gift_list_birthday_status_tomorrow(
|
||||
tr::now);
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
|
||||
auto usersWithBirthdays = ranges::views::all(
|
||||
_contactBirthdays
|
||||
) | ranges::views::transform([&](UserId userId) {
|
||||
return session->data().user(userId);
|
||||
}) | ranges::to_vector;
|
||||
|
||||
ranges::sort(usersWithBirthdays, [](UserData *a, UserData *b) {
|
||||
const auto aBirthday = a->birthday();
|
||||
const auto bBirthday = b->birthday();
|
||||
const auto aIsToday = Data::IsBirthdayToday(aBirthday);
|
||||
const auto bIsToday = Data::IsBirthdayToday(bBirthday);
|
||||
if (aIsToday != bIsToday) {
|
||||
return aIsToday > bIsToday;
|
||||
}
|
||||
if (aBirthday.month() != bBirthday.month()) {
|
||||
return aBirthday.month() < bBirthday.month();
|
||||
}
|
||||
return aBirthday.day() < bBirthday.day();
|
||||
});
|
||||
|
||||
for (const auto user : usersWithBirthdays) {
|
||||
auto row = std::make_unique<PeerRow>(user);
|
||||
if (auto s = status(user->birthday()); !s.isEmpty()) {
|
||||
row->setCustomStatus(std::move(s));
|
||||
}
|
||||
controller->delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
|
||||
controller->delegate()->peerListRefreshRows();
|
||||
},
|
||||
_pick,
|
||||
_contactBirthdays.empty()
|
||||
? rpl::producer<QString>(nullptr)
|
||||
: tr::lng_contacts_header())) {
|
||||
setStyleOverrides(&st::peerListSmallSkips);
|
||||
}
|
||||
|
||||
void Controller::rowRightActionClicked(not_null<PeerListRow*> row) {
|
||||
delegate()->peerListShowRowMenu(row, true);
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> Controller::rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
auto result = CreateRowContextMenu(parent, row->peer(), _pick);
|
||||
|
||||
if (result) {
|
||||
// First clear _menu value, so that we don't check row positions yet.
|
||||
base::take(_menu);
|
||||
|
||||
// Here unique_qptr is used like a shared pointer, where
|
||||
// not the last destroyed pointer destroys the object, but the first.
|
||||
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Controller::noSearchSubmit() {
|
||||
if (const auto onstack = _selfOption.activate) {
|
||||
onstack();
|
||||
}
|
||||
if (const auto onstack = _birthdayOptions.activate) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) {
|
||||
return _selfOption.overrideKey
|
||||
&& _selfOption.overrideKey(direction, fromIndex, toIndex);
|
||||
int from,
|
||||
int to) {
|
||||
if (direction == -1 && from == -1 && to == -1 && _skipUpDirectionSelect) {
|
||||
return true;
|
||||
}
|
||||
_skipUpDirectionSelect = false;
|
||||
if (direction > 0) {
|
||||
if (!_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
|
||||
return _selfOption.overrideKey(direction, from, to);
|
||||
}
|
||||
if (_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
|
||||
if (_selfOption.overrideKey(direction, from, to)) {
|
||||
return true;
|
||||
} else {
|
||||
return _birthdayOptions.overrideKey(direction, from, to);
|
||||
}
|
||||
}
|
||||
if (!_selfOption.hasSelection() && _birthdayOptions.hasSelection()) {
|
||||
if (_birthdayOptions.overrideKey(direction, from, to)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (direction < 0) {
|
||||
if (!_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
|
||||
return _birthdayOptions.overrideKey(direction, from, to);
|
||||
}
|
||||
if (!_selfOption.hasSelection() && _birthdayOptions.hasSelection()) {
|
||||
if (_birthdayOptions.overrideKey(direction, from, to)) {
|
||||
return true;
|
||||
} else if (!_birthdayOptions.hasSelection()) {
|
||||
const auto res = _selfOption.overrideKey(direction, from, to);
|
||||
_skipUpDirectionSelect = _selfOption.hasSelection();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
if (_selfOption.hasSelection() && !_birthdayOptions.hasSelection()) {
|
||||
if (_selfOption.overrideKey(direction, from, to)) {
|
||||
_skipUpDirectionSelect = _selfOption.hasSelection();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> Controller::createRow(
|
||||
not_null<UserData*> user) {
|
||||
if (const auto birthday = user->owner().knownContactBirthdays()) {
|
||||
if (ranges::contains(*birthday, peerToUser(user->id))) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (user->isSelf()
|
||||
|| user->isBot()
|
||||
|| user->isServiceUser()
|
||||
|| user->isInaccessible()) {
|
||||
return nullptr;
|
||||
}
|
||||
return ContactsBoxController::createRow(user);
|
||||
return std::make_unique<PeerRow>(user);
|
||||
}
|
||||
|
||||
void Controller::prepareViewHook() {
|
||||
delegate()->peerListSetAboveWidget(std::move(_selfOption.content));
|
||||
auto list = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
|
||||
list->add(std::move(_selfOption.content));
|
||||
list->add(std::move(_birthdayOptions.content));
|
||||
delegate()->peerListSetAboveWidget(std::move(list));
|
||||
}
|
||||
|
||||
void Controller::rowClicked(not_null<PeerListRow*> row) {
|
||||
_choose(row->peer());
|
||||
_pick(row->peer(), PickType::Activate);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ChooseStarGiftRecipient(
|
||||
not_null<Window::SessionController*> window) {
|
||||
auto controller = std::make_unique<Controller>(
|
||||
&window->session(),
|
||||
[=](not_null<PeerData*> peer) {
|
||||
ShowStarGiftBox(window, peer);
|
||||
});
|
||||
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(); });
|
||||
const auto session = &window->session();
|
||||
const auto lifetime = std::make_shared<rpl::lifetime>();
|
||||
session->data().contactBirthdays(
|
||||
) | rpl::start_with_next(crl::guard(session, [=] {
|
||||
lifetime->destroy();
|
||||
auto controller = std::make_unique<Controller>(
|
||||
session,
|
||||
[=](not_null<PeerData*> peer, PickType type) {
|
||||
if (type == PickType::Activate) {
|
||||
ShowStarGiftBox(window, peer);
|
||||
} else if (type == PickType::SendMessage) {
|
||||
using Way = Window::SectionShow::Way;
|
||||
window->showPeerHistory(peer, Way::Forward);
|
||||
} else if (type == PickType::OpenProfile) {
|
||||
window->show(PrepareShortInfoBox(peer, window));
|
||||
}
|
||||
});
|
||||
const auto controllerRaw = controller.get();
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->setTitle(tr::lng_gift_premium_or_stars());
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
box->noSearchSubmits() | rpl::start_with_next([=] {
|
||||
controllerRaw->noSearchSubmit();
|
||||
}, box->lifetime());
|
||||
};
|
||||
window->show(
|
||||
Box<PeerListBox>(std::move(controller), std::move(initBox)),
|
||||
LayerOption::KeepOther);
|
||||
box->noSearchSubmits() | rpl::start_with_next([=] {
|
||||
controllerRaw->noSearchSubmit();
|
||||
}, box->lifetime());
|
||||
};
|
||||
window->show(
|
||||
Box<PeerListBox>(std::move(controller), std::move(initBox)),
|
||||
LayerOption::KeepOther);
|
||||
}), *lifetime);
|
||||
}
|
||||
|
||||
void ShowStarGiftBox(
|
||||
|
|
|
@ -1575,7 +1575,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
|
|||
sticker->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
auto p = Painter(sticker);
|
||||
if (const auto strong = weak.data()) {
|
||||
if ([[maybe_unused]] const auto strong = weak.data()) {
|
||||
const auto paused = On(PowerSaving::kStickersPanel)
|
||||
|| show->paused(ChatHelpers::PauseReason::Layer);
|
||||
paintSticker(p, index, QPoint(), paused, crl::now());
|
||||
|
@ -1629,7 +1629,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
|
|||
Data::StickersType::Stickers);
|
||||
}, [](const auto &) {
|
||||
});
|
||||
if (const auto strong = weak.data()) {
|
||||
if ([[maybe_unused]] const auto strong = weak.data()) {
|
||||
applySet(result);
|
||||
}
|
||||
if (const auto strongBox = weakBox.data()) {
|
||||
|
|
|
@ -144,7 +144,7 @@ void UrlAuthBox::Request(
|
|||
const auto callback = [=](Result result) {
|
||||
if (result == Result::None) {
|
||||
finishWithUrl(url);
|
||||
} else if (const auto msg = session->data().message(itemId)) {
|
||||
} else if (session->data().message(itemId)) {
|
||||
const auto allowWrite = (result == Result::AuthAndAllowWrite);
|
||||
using Flag = MTPmessages_AcceptUrlAuth::Flag;
|
||||
const auto flags = (allowWrite ? Flag::f_write_allowed : Flag(0))
|
||||
|
|
|
@ -24,8 +24,8 @@ CallSignalBars {
|
|||
inactiveOpacity: double;
|
||||
}
|
||||
|
||||
callWidthMin: 300px;
|
||||
callHeightMin: 440px;
|
||||
callWidthMin: 380px;
|
||||
callHeightMin: 520px;
|
||||
callWidth: 720px;
|
||||
callHeight: 540px;
|
||||
|
||||
|
@ -37,6 +37,7 @@ CallBodyLayout {
|
|||
photoSize: pixels;
|
||||
nameTop: pixels;
|
||||
statusTop: pixels;
|
||||
participantsTop: pixels;
|
||||
muteStroke: pixels;
|
||||
muteSize: pixels;
|
||||
mutePosition: point;
|
||||
|
@ -48,6 +49,7 @@ callBodyLayout: CallBodyLayout {
|
|||
photoSize: 160px;
|
||||
nameTop: 221px;
|
||||
statusTop: 254px;
|
||||
participantsTop: 294px;
|
||||
muteStroke: 3px;
|
||||
muteSize: 36px;
|
||||
mutePosition: point(142px, 135px);
|
||||
|
@ -58,6 +60,7 @@ callBodyWithPreview: CallBodyLayout {
|
|||
photoSize: 100px;
|
||||
nameTop: 132px;
|
||||
statusTop: 163px;
|
||||
participantsTop: 193px;
|
||||
muteStroke: 3px;
|
||||
muteSize: 0px;
|
||||
mutePosition: point(90px, 84px);
|
||||
|
@ -132,7 +135,6 @@ callHangup: CallButton(callAnswer) {
|
|||
}
|
||||
bg: callHangupBg;
|
||||
outerBg: callHangupBg;
|
||||
label: callButtonLabel;
|
||||
}
|
||||
callCancel: CallButton(callAnswer) {
|
||||
button: IconButton(callButton) {
|
||||
|
@ -143,7 +145,6 @@ callCancel: CallButton(callAnswer) {
|
|||
}
|
||||
bg: callIconBgActive;
|
||||
outerBg: callIconBgActive;
|
||||
label: callButtonLabel;
|
||||
}
|
||||
callMicrophoneMute: CallButton(callAnswer) {
|
||||
button: IconButton(callButton) {
|
||||
|
@ -154,7 +155,6 @@ callMicrophoneMute: CallButton(callAnswer) {
|
|||
}
|
||||
bg: callIconBg;
|
||||
outerBg: callMuteRipple;
|
||||
label: callButtonLabel;
|
||||
cornerButtonPosition: point(40px, 4px);
|
||||
cornerButtonBorder: 2px;
|
||||
}
|
||||
|
@ -183,6 +183,17 @@ callCameraUnmute: CallButton(callMicrophoneUnmute) {
|
|||
}
|
||||
}
|
||||
}
|
||||
callAddPeople: CallButton(callAnswer) {
|
||||
button: IconButton(callButton) {
|
||||
icon: icon {{ "calls/calls_add_people", callIconFg }};
|
||||
iconPosition: point(-1px, 22px);
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: callMuteRipple;
|
||||
}
|
||||
}
|
||||
bg: callIconBg;
|
||||
outerBg: callMuteRipple;
|
||||
}
|
||||
callCornerButtonInner: IconButton {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
@ -521,7 +532,7 @@ callErrorToast: Toast(defaultToast) {
|
|||
}
|
||||
|
||||
groupCallWidth: 380px;
|
||||
groupCallHeight: 580px;
|
||||
groupCallHeight: 520px;
|
||||
groupCallWidthRtmp: 720px;
|
||||
groupCallWidthRtmpMin: 240px;
|
||||
groupCallHeightRtmp: 580px;
|
||||
|
@ -567,6 +578,13 @@ groupCallPopupMenu: PopupMenu(defaultPopupMenu) {
|
|||
menu: groupCallMenu;
|
||||
animation: groupCallPanelAnimation;
|
||||
}
|
||||
groupCallPopupMenuWithIcons: PopupMenu(popupMenuWithIcons) {
|
||||
shadow: groupCallMenuShadow;
|
||||
menu: Menu(groupCallMenu, menuWithIcons) {
|
||||
arrow: icon {{ "menu/submenu_arrow", groupCallMemberNotJoinedStatus }};
|
||||
}
|
||||
animation: groupCallPanelAnimation;
|
||||
}
|
||||
groupCallPopupMenuWithVolume: PopupMenu(groupCallPopupMenu) {
|
||||
scrollPadding: margins(0px, 3px, 0px, 8px);
|
||||
menu: Menu(groupCallMenu) {
|
||||
|
@ -617,6 +635,23 @@ callDeviceSelectionMenu: PopupMenu(groupCallPopupMenu) {
|
|||
}
|
||||
}
|
||||
|
||||
createCallListButton: OutlineButton(defaultPeerListButton) {
|
||||
font: normalFont;
|
||||
padding: margins(11px, 5px, 11px, 5px);
|
||||
}
|
||||
createCallListItem: PeerListItem(defaultPeerListItem) {
|
||||
button: createCallListButton;
|
||||
height: 52px;
|
||||
photoPosition: point(12px, 6px);
|
||||
namePosition: point(63px, 7px);
|
||||
statusPosition: point(63px, 26px);
|
||||
photoSize: 40px;
|
||||
}
|
||||
createCallList: PeerList(defaultPeerList) {
|
||||
item: createCallListItem;
|
||||
padding: margins(0px, 6px, 0px, 6px);
|
||||
}
|
||||
|
||||
groupCallRecordingTimerPadding: margins(0px, 4px, 0px, 4px);
|
||||
groupCallRecordingTimerFont: font(12px);
|
||||
|
||||
|
@ -640,26 +675,18 @@ groupCallMembersListCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {
|
|||
selectFg: groupCallActiveFg;
|
||||
check: groupCallMembersListCheck;
|
||||
}
|
||||
groupCallMembersListItem: PeerListItem(defaultPeerListItem) {
|
||||
button: OutlineButton(defaultPeerListButton) {
|
||||
groupCallMembersListItem: PeerListItem(createCallListItem) {
|
||||
button: OutlineButton(createCallListButton) {
|
||||
textBg: groupCallMembersBg;
|
||||
textBgOver: groupCallMembersBgOver;
|
||||
|
||||
textFg: groupCallMemberInactiveStatus;
|
||||
textFgOver: groupCallMemberInactiveStatus;
|
||||
|
||||
font: normalFont;
|
||||
padding: margins(11px, 5px, 11px, 5px);
|
||||
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
disabledCheckFg: groupCallMemberNotJoinedStatus;
|
||||
checkbox: groupCallMembersListCheckbox;
|
||||
height: 52px;
|
||||
photoPosition: point(12px, 6px);
|
||||
namePosition: point(63px, 7px);
|
||||
statusPosition: point(63px, 26px);
|
||||
photoSize: 40px;
|
||||
nameFg: groupCallMembersFg;
|
||||
nameFgChecked: groupCallMembersFg;
|
||||
statusFg: groupCallMemberInactiveStatus;
|
||||
|
@ -804,6 +831,7 @@ groupCallAddMember: SettingsButton(defaultSettingsButton) {
|
|||
ripple: groupCallRipple;
|
||||
}
|
||||
groupCallAddMemberIcon: icon {{ "info/info_add_member", groupCallMemberInactiveIcon, point(0px, 3px) }};
|
||||
groupCallShareLinkIcon: icon {{ "menu/links_profile", groupCallMemberInactiveIcon, point(4px, 3px) }};
|
||||
groupCallSubtitleLabel: FlatLabel(defaultFlatLabel) {
|
||||
maxHeight: 18px;
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
|
@ -874,6 +902,8 @@ groupCallMemberColoredCrossLine: CrossLineAnimation(groupCallMemberInactiveCross
|
|||
fg: groupCallMemberMutedIcon;
|
||||
icon: icon {{ "calls/group_calls_unmuted", groupCallMemberActiveIcon }};
|
||||
}
|
||||
groupCallMemberCalling: icon {{ "calls/call_answer", groupCallMemberInactiveIcon }};
|
||||
groupCallMemberCallingPosition: point(0px, 8px);
|
||||
groupCallMemberInvited: icon {{ "calls/group_calls_invited", groupCallMemberInactiveIcon }};
|
||||
groupCallMemberInvitedPosition: point(2px, 12px);
|
||||
groupCallMemberRaisedHand: icon {{ "calls/group_calls_raised_hand", groupCallMemberInactiveStatus }};
|
||||
|
@ -917,7 +947,6 @@ groupCallHangup: CallButton(callHangup) {
|
|||
button: groupCallHangupInner;
|
||||
bg: groupCallLeaveBg;
|
||||
outerBg: groupCallLeaveBg;
|
||||
label: callButtonLabel;
|
||||
}
|
||||
groupCallSettingsSmall: CallButton(groupCallSettings) {
|
||||
button: IconButton(groupCallSettingsInner) {
|
||||
|
@ -1309,6 +1338,7 @@ groupCallNarrowRaisedHand: icon {{ "calls/video_mini_speak", groupCallMemberInac
|
|||
groupCallNarrowCameraIcon: icon {{ "calls/video_mini_video", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowScreenIcon: icon {{ "calls/video_mini_screencast", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowInvitedIcon: icon {{ "calls/video_mini_invited", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowCallingIcon: icon {{ "calls/video_mini_invited", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowIconPosition: point(-4px, 2px);
|
||||
groupCallNarrowIconSkip: 15px;
|
||||
groupCallOutline: 2px;
|
||||
|
@ -1483,4 +1513,174 @@ groupCallCalendarColors: CalendarColors {
|
|||
|
||||
titleTextColor: groupCallMembersFg;
|
||||
}
|
||||
//
|
||||
|
||||
createCallInviteLink: SettingsButton(defaultSettingsButton) {
|
||||
textFg: windowActiveTextFg;
|
||||
textFgOver: windowActiveTextFg;
|
||||
textBg: windowBg;
|
||||
textBgOver: windowBgOver;
|
||||
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(14px semibold);
|
||||
}
|
||||
|
||||
height: 20px;
|
||||
padding: margins(74px, 8px, 8px, 9px);
|
||||
}
|
||||
createCallInviteLinkIcon: icon {{ "info/edit/group_manage_links", windowActiveTextFg }};
|
||||
createCallInviteLinkIconPosition: point(23px, 2px);
|
||||
createCallVideo: IconButton {
|
||||
width: 36px;
|
||||
height: 52px;
|
||||
|
||||
icon: icon {{ "info/info_media_video", menuIconFg }};
|
||||
iconOver: icon {{ "info/info_media_video", menuIconFgOver }};
|
||||
iconPosition: point(-1px, -1px);
|
||||
|
||||
ripple: defaultRippleAnimation;
|
||||
rippleAreaPosition: point(0px, 8px);
|
||||
rippleAreaSize: 36px;
|
||||
}
|
||||
createCallVideoActive: icon {{ "info/info_media_video", windowActiveTextFg }};
|
||||
createCallVideoMargins: margins(0px, 0px, 10px, 0px);
|
||||
createCallAudio: IconButton(createCallVideo) {
|
||||
icon: icon {{ "menu/phone", menuIconFg }};
|
||||
iconOver: icon {{ "menu/phone", menuIconFgOver }};
|
||||
}
|
||||
createCallAudioActive: icon {{ "menu/phone", windowActiveTextFg }};
|
||||
createCallAudioMargins: margins(0px, 0px, 4px, 0px);
|
||||
|
||||
confcallLinkButton: RoundButton(defaultActiveButton) {
|
||||
height: 42px;
|
||||
textTop: 12px;
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
confcallLinkBoxInitial: Box(defaultBox) {
|
||||
buttonPadding: margins(12px, 11px, 24px, 96px);
|
||||
buttonHeight: 42px;
|
||||
button: confcallLinkButton;
|
||||
shadowIgnoreTopSkip: true;
|
||||
}
|
||||
confcallLinkBox: Box(confcallLinkBoxInitial) {
|
||||
buttonPadding: margins(12px, 11px, 24px, 24px);
|
||||
}
|
||||
confcallLinkCopyButton: RoundButton(confcallLinkButton) {
|
||||
icon: icon {{ "info/edit/links_copy", activeButtonFg }};
|
||||
iconOver: icon {{ "info/edit/links_copy", activeButtonFgOver }};
|
||||
iconPosition: point(-1px, 5px);
|
||||
}
|
||||
confcallLinkShareButton: RoundButton(confcallLinkCopyButton) {
|
||||
icon: icon {{ "info/edit/links_share", activeButtonFg }};
|
||||
iconOver: icon {{ "info/edit/links_share", activeButtonFgOver }};
|
||||
}
|
||||
confcallLinkHeaderIconPadding: margins(0px, 32px, 0px, 10px);
|
||||
confcallLinkTitlePadding: margins(0px, 0px, 0px, 12px);
|
||||
confcallLinkCenteredText: FlatLabel(defaultFlatLabel) {
|
||||
align: align(top);
|
||||
minWidth: 40px;
|
||||
}
|
||||
confcallLinkFooterOr: FlatLabel(confcallLinkCenteredText) {
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
confcallLinkFooterOrTop: 12px;
|
||||
confcallLinkFooterOrSkip: 8px;
|
||||
confcallLinkFooterOrLineTop: 9px;
|
||||
confcallJoinBox: Box(confcallLinkBox) {
|
||||
buttonPadding: margins(24px, 24px, 24px, 24px);
|
||||
}
|
||||
confcallJoinLogo: icon {{ "calls/group_call_logo", windowFgActive }};
|
||||
confcallJoinLogoPadding: margins(8px, 8px, 8px, 8px);
|
||||
confcallJoinSepPadding: margins(0px, 8px, 0px, 8px);
|
||||
confcallJoinUserpics: UserpicsRow {
|
||||
button: UserpicButton(defaultUserpicButton) {
|
||||
size: size(36px, 36px);
|
||||
photoSize: 36px;
|
||||
}
|
||||
bg: boxBg;
|
||||
shift: 16px;
|
||||
stroke: 2px;
|
||||
invert: true;
|
||||
}
|
||||
confcallJoinUserpicsPadding: margins(0px, 0px, 0px, 16px);
|
||||
confcallInviteUserpicsBg: groupCallMembersBg;
|
||||
confcallInviteUserpics: UserpicsRow {
|
||||
button: UserpicButton(defaultUserpicButton) {
|
||||
size: size(24px, 24px);
|
||||
photoSize: 24px;
|
||||
}
|
||||
bg: confcallInviteUserpicsBg;
|
||||
shift: 10px;
|
||||
stroke: 2px;
|
||||
invert: true;
|
||||
}
|
||||
confcallInviteParticipants: FlatLabel(defaultFlatLabel) {
|
||||
textFg: callNameFg;
|
||||
}
|
||||
confcallInviteParticipantsPadding: margins(8px, 3px, 12px, 2px);
|
||||
confcallInviteVideo: IconButton(createCallVideo) {
|
||||
icon: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }};
|
||||
iconOver: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }};
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
confcallInviteVideoActive: icon {{ "info/info_media_video", groupCallActiveFg }};
|
||||
confcallInviteAudio: IconButton(confcallInviteVideo) {
|
||||
icon: icon {{ "menu/phone", groupCallMemberInactiveIcon }};
|
||||
iconOver: icon {{ "menu/phone", groupCallMemberInactiveIcon }};
|
||||
}
|
||||
confcallInviteAudioActive: icon {{ "menu/phone", groupCallActiveFg }};
|
||||
|
||||
groupCallLinkBox: Box(confcallLinkBox) {
|
||||
bg: groupCallMembersBg;
|
||||
title: FlatLabel(boxTitle) {
|
||||
textFg: groupCallMembersFg;
|
||||
}
|
||||
titleAdditionalFg: groupCallMemberNotJoinedStatus;
|
||||
}
|
||||
groupCallLinkCenteredText: FlatLabel(confcallLinkCenteredText) {
|
||||
textFg: groupCallMembersFg;
|
||||
}
|
||||
groupCallLinkPreview: InputField(defaultInputField) {
|
||||
textBg: groupCallMembersBgOver;
|
||||
textFg: groupCallMembersFg;
|
||||
textMargins: margins(12px, 8px, 30px, 5px);
|
||||
style: defaultTextStyle;
|
||||
heightMin: 35px;
|
||||
}
|
||||
groupCallInviteLink: SettingsButton(createCallInviteLink) {
|
||||
textFg: mediaviewTextLinkFg;
|
||||
textFgOver: mediaviewTextLinkFg;
|
||||
textBg: groupCallMembersBg;
|
||||
textBgOver: groupCallMembersBgOver;
|
||||
|
||||
padding: margins(63px, 8px, 8px, 9px);
|
||||
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
groupCallInviteLinkIcon: icon {{ "info/edit/group_manage_links", mediaviewTextLinkFg }};
|
||||
|
||||
confcallLinkMenu: IconButton(boxTitleClose) {
|
||||
icon: icon {{ "title_menu_dots", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }};
|
||||
}
|
||||
groupCallLinkMenu: IconButton(confcallLinkMenu) {
|
||||
icon: icon {{ "title_menu_dots", groupCallMemberInactiveIcon }};
|
||||
iconOver: icon {{ "title_menu_dots", groupCallMemberInactiveIcon }};
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: groupCallMembersBgOver;
|
||||
}
|
||||
}
|
||||
|
||||
confcallFingerprintBottomSkip: 8px;
|
||||
confcallFingerprintMargins: margins(8px, 5px, 8px, 5px);
|
||||
confcallFingerprintTextMargins: margins(3px, 3px, 3px, 0px);
|
||||
confcallFingerprintText: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMembersFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(10px semibold);
|
||||
}
|
||||
}
|
||||
confcallFingerprintSkip: 2px;
|
||||
confcallFingerprintTooltipLabel: defaultImportantTooltipLabel;
|
||||
confcallFingerprintTooltip: defaultImportantTooltip;
|
||||
confcallFingerprintTooltipSkip: 12px;
|
||||
confcallFingerprintTooltipMaxWidth: 220px;
|
||||
|
|
|
@ -9,17 +9,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "core/application.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_invite_controller.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "mainwidget.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
|
@ -32,6 +41,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "base/unixtime.h"
|
||||
#include "api/api_updates.h"
|
||||
#include "apiwrap.h"
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "settings/settings_calls.h"
|
||||
#include "styles/style_info.h" // infoTopBarMenu
|
||||
#include "styles/style_layers.h" // st::boxLabel.
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
@ -92,7 +104,7 @@ GroupCallRow::GroupCallRow(not_null<PeerData*> peer)
|
|||
: PeerListRow(peer)
|
||||
, _st(st::callGroupCall) {
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
const auto status = (channel->isMegagroup()
|
||||
const auto status = (!channel->isMegagroup()
|
||||
? (channel->isPublic()
|
||||
? tr::lng_create_public_channel_title
|
||||
: tr::lng_create_private_channel_title)
|
||||
|
@ -150,7 +162,7 @@ void GroupCallRow::rightActionStopLastRipple() {
|
|||
|
||||
namespace GroupCalls {
|
||||
|
||||
ListController::ListController(not_null<Window::SessionController*> window)
|
||||
ListController::ListController(not_null<::Window::SessionController*> window)
|
||||
: _window(window) {
|
||||
setStyleOverrides(&st::peerListSingleRow);
|
||||
}
|
||||
|
@ -227,7 +239,7 @@ void ListController::rowClicked(not_null<PeerListRow*> row) {
|
|||
crl::on_main(window, [=, peer = row->peer()] {
|
||||
window->showPeerHistory(
|
||||
peer,
|
||||
Window::SectionShow::Way::ClearStack);
|
||||
::Window::SectionShow::Way::ClearStack);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -430,9 +442,9 @@ BoxController::Row::Type BoxController::Row::ComputeType(
|
|||
return Type::Out;
|
||||
} else if (auto media = item->media()) {
|
||||
if (const auto call = media->call()) {
|
||||
const auto reason = call->finishReason;
|
||||
if (reason == Data::Call::FinishReason::Busy
|
||||
|| reason == Data::Call::FinishReason::Missed) {
|
||||
using State = Data::CallState;
|
||||
const auto state = call->state;
|
||||
if (state == State::Busy || state == State::Missed) {
|
||||
return Type::Missed;
|
||||
}
|
||||
}
|
||||
|
@ -470,7 +482,7 @@ void BoxController::Row::rightActionStopLastRipple() {
|
|||
}
|
||||
}
|
||||
|
||||
BoxController::BoxController(not_null<Window::SessionController*> window)
|
||||
BoxController::BoxController(not_null<::Window::SessionController*> window)
|
||||
: _window(window)
|
||||
, _api(&_window->session().mtp()) {
|
||||
}
|
||||
|
@ -591,7 +603,7 @@ void BoxController::rowClicked(not_null<PeerListRow*> row) {
|
|||
crl::on_main(window, [=, peer = row->peer()] {
|
||||
window->showPeerHistory(
|
||||
peer,
|
||||
Window::SectionShow::Way::ClearStack,
|
||||
::Window::SectionShow::Way::ClearStack,
|
||||
itemId);
|
||||
});
|
||||
}
|
||||
|
@ -611,7 +623,7 @@ void BoxController::receivedCalls(const QVector<MTPMessage> &result) {
|
|||
for (const auto &message : result) {
|
||||
const auto msgId = IdFromMessage(message);
|
||||
const auto peerId = PeerFromMessage(message);
|
||||
if (const auto peer = session().data().peerLoaded(peerId)) {
|
||||
if (session().data().peerLoaded(peerId)) {
|
||||
const auto item = session().data().addNewMessage(
|
||||
message,
|
||||
MessageFlags(),
|
||||
|
@ -698,7 +710,7 @@ std::unique_ptr<PeerListRow> BoxController::createRow(
|
|||
|
||||
void ClearCallsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window) {
|
||||
not_null<::Window::SessionController*> window) {
|
||||
const auto weak = Ui::MakeWeak(box);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
|
@ -756,4 +768,133 @@ void ClearCallsBox(
|
|||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateCallButton(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<::Window::SessionController*> controller,
|
||||
Fn<void()> done) {
|
||||
const auto result = container->add(object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
tr::lng_confcall_create_call(),
|
||||
st::inviteViaLinkButton), QMargins());
|
||||
Ui::AddSkip(container);
|
||||
Ui::AddDividerText(
|
||||
container,
|
||||
tr::lng_confcall_create_call_description(
|
||||
lt_count,
|
||||
rpl::single(controller->session().appConfig().confcallSizeLimit()
|
||||
* 1.),
|
||||
Ui::Text::WithEntities));
|
||||
|
||||
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
|
||||
result,
|
||||
st::inviteViaLinkIcon,
|
||||
QPoint());
|
||||
result->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
icon->moveToLeft(
|
||||
st::inviteViaLinkIconPosition.x(),
|
||||
(height - st::inviteViaLinkIcon.height()) / 2);
|
||||
}, icon->lifetime());
|
||||
|
||||
result->setClickedCallback([=] {
|
||||
controller->show(Group::PrepareCreateCallBox(controller, done));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ShowCallsBox(not_null<::Window::SessionController*> window) {
|
||||
struct State {
|
||||
State(not_null<::Window::SessionController*> window)
|
||||
: callsController(window)
|
||||
, groupCallsController(window) {
|
||||
}
|
||||
Calls::BoxController callsController;
|
||||
PeerListContentDelegateSimple callsDelegate;
|
||||
|
||||
Calls::GroupCalls::ListController groupCallsController;
|
||||
PeerListContentDelegateSimple groupCallsDelegate;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
};
|
||||
|
||||
window->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
const auto state = box->lifetime().make_state<State>(window);
|
||||
|
||||
const auto groupCalls = box->addRow(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
box,
|
||||
object_ptr<Ui::VerticalLayout>(box)),
|
||||
{});
|
||||
groupCalls->hide(anim::type::instant);
|
||||
groupCalls->toggleOn(state->groupCallsController.shownValue());
|
||||
|
||||
Ui::AddSubsectionTitle(
|
||||
groupCalls->entity(),
|
||||
tr::lng_call_box_groupcalls_subtitle());
|
||||
state->groupCallsDelegate.setContent(groupCalls->entity()->add(
|
||||
object_ptr<PeerListContent>(box, &state->groupCallsController),
|
||||
{}));
|
||||
state->groupCallsController.setDelegate(&state->groupCallsDelegate);
|
||||
Ui::AddSkip(groupCalls->entity());
|
||||
Ui::AddDivider(groupCalls->entity());
|
||||
Ui::AddSkip(groupCalls->entity());
|
||||
|
||||
const auto button = AddCreateCallButton(
|
||||
box->verticalLayout(),
|
||||
window,
|
||||
crl::guard(box, [=] { box->closeBox(); }));
|
||||
button->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::Enter);
|
||||
}) | rpl::start_with_next([=] {
|
||||
state->callsDelegate.peerListMouseLeftGeometry();
|
||||
}, button->lifetime());
|
||||
|
||||
const auto content = box->addRow(
|
||||
object_ptr<PeerListContent>(box, &state->callsController),
|
||||
{});
|
||||
state->callsDelegate.setContent(content);
|
||||
state->callsController.setDelegate(&state->callsDelegate);
|
||||
|
||||
box->setWidth(state->callsController.contentWidth());
|
||||
state->callsController.boxHeightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
box->setMinHeight(height);
|
||||
}, box->lifetime());
|
||||
box->setTitle(tr::lng_call_box_title());
|
||||
box->addButton(tr::lng_close(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
const auto menuButton = box->addTopButton(st::infoTopBarMenu);
|
||||
menuButton->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
menuButton,
|
||||
st::popupMenuWithIcons);
|
||||
const auto showSettings = [=] {
|
||||
window->showSettings(
|
||||
Settings::Calls::Id(),
|
||||
::Window::SectionShow(anim::type::instant));
|
||||
};
|
||||
const auto clearAll = crl::guard(box, [=] {
|
||||
box->uiShow()->showBox(Box(Calls::ClearCallsBox, window));
|
||||
});
|
||||
state->menu->addAction(
|
||||
tr::lng_settings_section_call_settings(tr::now),
|
||||
showSettings,
|
||||
&st::menuIconSettings);
|
||||
if (state->callsDelegate.peerListFullRowsCount() > 0) {
|
||||
Ui::Menu::CreateAddActionCallback(state->menu)({
|
||||
.text = tr::lng_call_box_clear_all(tr::now),
|
||||
.handler = clearAll,
|
||||
.icon = &st::menuIconDeleteAttention,
|
||||
.isAttention = true,
|
||||
});
|
||||
}
|
||||
state->menu->popup(QCursor::pos());
|
||||
return true;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace GroupCalls {
|
|||
|
||||
class ListController : public PeerListController {
|
||||
public:
|
||||
explicit ListController(not_null<Window::SessionController*> window);
|
||||
explicit ListController(not_null<::Window::SessionController*> window);
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> shownValue() const;
|
||||
|
||||
|
@ -30,7 +30,7 @@ public:
|
|||
void rowRightActionClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
private:
|
||||
const not_null<Window::SessionController*> _window;
|
||||
const not_null<::Window::SessionController*> _window;
|
||||
base::flat_map<PeerId, not_null<PeerListRow*>> _groupCalls;
|
||||
rpl::variable<int> _fullCount;
|
||||
|
||||
|
@ -40,7 +40,7 @@ private:
|
|||
|
||||
class BoxController : public PeerListController {
|
||||
public:
|
||||
explicit BoxController(not_null<Window::SessionController*> window);
|
||||
explicit BoxController(not_null<::Window::SessionController*> window);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void prepare() override;
|
||||
|
@ -68,7 +68,7 @@ private:
|
|||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<HistoryItem*> item) const;
|
||||
|
||||
const not_null<Window::SessionController*> _window;
|
||||
const not_null<::Window::SessionController*> _window;
|
||||
MTP::Sender _api;
|
||||
|
||||
MsgId _offsetId = 0;
|
||||
|
@ -79,6 +79,8 @@ private:
|
|||
|
||||
void ClearCallsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window);
|
||||
not_null<::Window::SessionController*> window);
|
||||
|
||||
void ShowCallsBox(not_null<::Window::SessionController*> window);
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -12,10 +12,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/random.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "calls/calls_panel.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
|
@ -39,8 +41,6 @@ namespace tgcalls {
|
|||
class InstanceImpl;
|
||||
class InstanceV2Impl;
|
||||
class InstanceV2ReferenceImpl;
|
||||
class InstanceImplLegacy;
|
||||
void SetLegacyGlobalServerConfig(const std::string &serverConfig);
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Calls {
|
||||
|
@ -55,7 +55,6 @@ const auto kDefaultVersion = "2.4.4"_q;
|
|||
const auto Register = tgcalls::Register<tgcalls::InstanceImpl>();
|
||||
const auto RegisterV2 = tgcalls::Register<tgcalls::InstanceV2Impl>();
|
||||
const auto RegV2Ref = tgcalls::Register<tgcalls::InstanceV2ReferenceImpl>();
|
||||
const auto RegisterLegacy = tgcalls::Register<tgcalls::InstanceImplLegacy>();
|
||||
|
||||
[[nodiscard]] base::flat_set<int64> CollectEndpointIds(
|
||||
const QVector<MTPPhoneConnection> &list) {
|
||||
|
@ -246,7 +245,52 @@ Call::Call(
|
|||
setupOutgoingVideo();
|
||||
}
|
||||
|
||||
Call::Call(
|
||||
not_null<Delegate*> delegate,
|
||||
not_null<UserData*> user,
|
||||
CallId conferenceId,
|
||||
MsgId conferenceInviteMsgId,
|
||||
std::vector<not_null<PeerData*>> conferenceParticipants,
|
||||
bool video)
|
||||
: _delegate(delegate)
|
||||
, _user(user)
|
||||
, _api(&_user->session().mtp())
|
||||
, _type(Type::Incoming)
|
||||
, _state(State::WaitingIncoming)
|
||||
, _discardByTimeoutTimer([=] { hangup(); })
|
||||
, _playbackDeviceId(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Playback,
|
||||
Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callPlaybackDeviceIdValue(),
|
||||
Core::App().settings().playbackDeviceIdValue()))
|
||||
, _captureDeviceId(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Capture,
|
||||
Webrtc::DeviceIdValueWithFallback(
|
||||
Core::App().settings().callCaptureDeviceIdValue(),
|
||||
Core::App().settings().captureDeviceIdValue()))
|
||||
, _cameraDeviceId(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Camera,
|
||||
Core::App().settings().cameraDeviceIdValue())
|
||||
, _id(base::RandomValue<CallId>())
|
||||
, _conferenceId(conferenceId)
|
||||
, _conferenceInviteMsgId(conferenceInviteMsgId)
|
||||
, _conferenceParticipants(std::move(conferenceParticipants))
|
||||
, _videoIncoming(
|
||||
std::make_unique<Webrtc::VideoTrack>(
|
||||
StartVideoState(video)))
|
||||
, _videoOutgoing(
|
||||
std::make_unique<Webrtc::VideoTrack>(
|
||||
StartVideoState(video))) {
|
||||
startWaitingTrack();
|
||||
setupOutgoingVideo();
|
||||
}
|
||||
|
||||
void Call::generateModExpFirst(bytes::const_span randomSeed) {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
auto first = MTP::CreateModExp(_dhConfig.g, _dhConfig.p, randomSeed);
|
||||
if (first.modexp.empty()) {
|
||||
LOG(("Call Error: Could not compute mod-exp first."));
|
||||
|
@ -272,6 +316,8 @@ bool Call::isIncomingWaiting() const {
|
|||
}
|
||||
|
||||
void Call::start(bytes::const_span random) {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
// Save config here, because it is possible that it changes between
|
||||
// different usages inside the same call.
|
||||
_dhConfig = _delegate->getDhConfig();
|
||||
|
@ -296,6 +342,7 @@ void Call::startOutgoing() {
|
|||
Expects(_type == Type::Outgoing);
|
||||
Expects(_state.current() == State::Requesting);
|
||||
Expects(_gaHash.size() == kSha256Size);
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
const auto flags = _videoCapture
|
||||
? MTPphone_RequestCall::Flag::f_video
|
||||
|
@ -303,7 +350,6 @@ void Call::startOutgoing() {
|
|||
_api.request(MTPphone_RequestCall(
|
||||
MTP_flags(flags),
|
||||
_user->inputUser,
|
||||
MTPInputGroupCall(),
|
||||
MTP_int(base::RandomValue<int32>()),
|
||||
MTP_bytes(_gaHash),
|
||||
MTP_phoneCallProtocol(
|
||||
|
@ -350,6 +396,7 @@ void Call::startOutgoing() {
|
|||
void Call::startIncoming() {
|
||||
Expects(_type == Type::Incoming);
|
||||
Expects(_state.current() == State::Starting);
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
_api.request(MTPphone_ReceivedCall(
|
||||
MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash))
|
||||
|
@ -363,6 +410,8 @@ void Call::startIncoming() {
|
|||
}
|
||||
|
||||
void Call::applyUserConfirmation() {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
if (_state.current() == State::WaitingUserConfirmation) {
|
||||
setState(State::Requesting);
|
||||
}
|
||||
|
@ -375,9 +424,51 @@ void Call::answer() {
|
|||
}), video);
|
||||
}
|
||||
|
||||
StartConferenceInfo Call::migrateConferenceInfo(StartConferenceInfo extend) {
|
||||
extend.migrating = true;
|
||||
extend.muted = muted();
|
||||
extend.videoCapture = isSharingVideo() ? _videoCapture : nullptr;
|
||||
extend.videoCaptureScreenId = screenSharingDeviceId();
|
||||
return extend;
|
||||
}
|
||||
|
||||
void Call::acceptConferenceInvite() {
|
||||
Expects(conferenceInvite());
|
||||
|
||||
if (_state.current() != State::WaitingIncoming) {
|
||||
return;
|
||||
}
|
||||
setState(State::ExchangingKeys);
|
||||
const auto limit = 5;
|
||||
const auto messageId = _conferenceInviteMsgId;
|
||||
_api.request(MTPphone_GetGroupCall(
|
||||
MTP_inputGroupCallInviteMessage(MTP_int(messageId.bare)),
|
||||
MTP_int(limit)
|
||||
)).done([=](const MTPphone_GroupCall &result) {
|
||||
result.data().vcall().match([&](const auto &data) {
|
||||
auto call = _user->owner().sharedConferenceCall(
|
||||
data.vid().v,
|
||||
data.vaccess_hash().v);
|
||||
call->processFullCall(result);
|
||||
Core::App().calls().startOrJoinConferenceCall(
|
||||
migrateConferenceInfo({
|
||||
.call = std::move(call),
|
||||
.joinMessageId = messageId,
|
||||
}));
|
||||
});
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
handleRequestError(error.type());
|
||||
}).send();
|
||||
}
|
||||
|
||||
void Call::actuallyAnswer() {
|
||||
Expects(_type == Type::Incoming);
|
||||
|
||||
if (conferenceInvite()) {
|
||||
acceptConferenceInvite();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto state = _state.current();
|
||||
if (state != State::Starting && state != State::WaitingIncoming) {
|
||||
if (state != State::ExchangingKeys
|
||||
|
@ -435,6 +526,8 @@ void Call::setMuted(bool mute) {
|
|||
}
|
||||
|
||||
void Call::setupMediaDevices() {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
_playbackDeviceId.changes() | rpl::filter([=] {
|
||||
return _instance && _setDeviceIdCallback;
|
||||
}) | rpl::start_with_next([=](const Webrtc::DeviceResolvedId &deviceId) {
|
||||
|
@ -472,7 +565,8 @@ void Call::setupOutgoingVideo() {
|
|||
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
|
||||
} else if (_state.current() != State::Established
|
||||
&& (state != Webrtc::VideoState::Inactive)
|
||||
&& (started == Webrtc::VideoState::Inactive)) {
|
||||
&& (started == Webrtc::VideoState::Inactive)
|
||||
&& !conferenceInvite()) {
|
||||
_errors.fire({ ErrorType::NotStartedCall });
|
||||
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
|
||||
} else if (state != Webrtc::VideoState::Inactive
|
||||
|
@ -528,24 +622,30 @@ crl::time Call::getDurationMs() const {
|
|||
return _startTime ? (crl::now() - _startTime) : 0;
|
||||
}
|
||||
|
||||
void Call::hangup() {
|
||||
void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) {
|
||||
const auto state = _state.current();
|
||||
if (state == State::Busy) {
|
||||
if (state == State::Busy
|
||||
|| state == State::MigrationHangingUp) {
|
||||
_delegate->callFinished(this);
|
||||
} else {
|
||||
const auto missed = (state == State::Ringing
|
||||
|| (state == State::Waiting && _type == Type::Outgoing));
|
||||
const auto declined = isIncomingWaiting();
|
||||
const auto reason = missed
|
||||
const auto reason = !migrateSlug.isEmpty()
|
||||
? MTP_phoneCallDiscardReasonMigrateConferenceCall(
|
||||
MTP_string(migrateSlug))
|
||||
: missed
|
||||
? MTP_phoneCallDiscardReasonMissed()
|
||||
: declined
|
||||
? MTP_phoneCallDiscardReasonBusy()
|
||||
: MTP_phoneCallDiscardReasonHangup();
|
||||
finish(FinishType::Ended, reason);
|
||||
finish(FinishType::Ended, reason, migrateCall);
|
||||
}
|
||||
}
|
||||
|
||||
void Call::redial() {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
if (_state.current() != State::Busy) {
|
||||
return;
|
||||
}
|
||||
|
@ -575,6 +675,8 @@ void Call::startWaitingTrack() {
|
|||
}
|
||||
|
||||
void Call::sendSignalingData(const QByteArray &data) {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
_api.request(MTPphone_SendSignalingData(
|
||||
MTP_inputPhoneCall(
|
||||
MTP_long(_id),
|
||||
|
@ -706,7 +808,7 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
|
|||
}
|
||||
if (false && data.is_need_rating() && _id && _accessHash) {
|
||||
const auto window = Core::App().windowFor(
|
||||
Window::SeparateId(_user));
|
||||
::Window::SeparateId(_user));
|
||||
const auto session = &_user->session();
|
||||
const auto callId = _id;
|
||||
const auto callAccessHash = _accessHash;
|
||||
|
@ -741,7 +843,10 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
|
|||
&& reason->type() == mtpc_phoneCallDiscardReasonDisconnect) {
|
||||
LOG(("Call Info: Discarded with DISCONNECT reason."));
|
||||
}
|
||||
if (reason && reason->type() == mtpc_phoneCallDiscardReasonBusy) {
|
||||
if (reason && reason->type() == mtpc_phoneCallDiscardReasonMigrateConferenceCall) {
|
||||
const auto slug = qs(reason->c_phoneCallDiscardReasonMigrateConferenceCall().vslug());
|
||||
finishByMigration(slug);
|
||||
} else if (reason && reason->type() == mtpc_phoneCallDiscardReasonBusy) {
|
||||
setState(State::Busy);
|
||||
} else if (_type == Type::Outgoing
|
||||
|| _state.current() == State::HangingUp) {
|
||||
|
@ -769,6 +874,35 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
|
|||
Unexpected("phoneCall type inside an existing call handleUpdate()");
|
||||
}
|
||||
|
||||
void Call::finishByMigration(const QString &slug) {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
if (_state.current() == State::MigrationHangingUp) {
|
||||
return;
|
||||
}
|
||||
setState(State::MigrationHangingUp);
|
||||
const auto limit = 5;
|
||||
const auto session = &_user->session();
|
||||
session->api().request(MTPphone_GetGroupCall(
|
||||
MTP_inputGroupCallSlug(MTP_string(slug)),
|
||||
MTP_int(limit)
|
||||
)).done([=](const MTPphone_GroupCall &result) {
|
||||
result.data().vcall().match([&](const auto &data) {
|
||||
const auto call = session->data().sharedConferenceCall(
|
||||
data.vid().v,
|
||||
data.vaccess_hash().v);
|
||||
call->processFullCall(result);
|
||||
Core::App().calls().startOrJoinConferenceCall(
|
||||
migrateConferenceInfo({
|
||||
.call = call,
|
||||
.linkSlug = slug,
|
||||
}));
|
||||
});
|
||||
}).fail(crl::guard(this, [=] {
|
||||
setState(State::Failed);
|
||||
})).send();
|
||||
}
|
||||
|
||||
void Call::updateRemoteMediaState(
|
||||
tgcalls::AudioState audio,
|
||||
tgcalls::VideoState video) {
|
||||
|
@ -809,6 +943,7 @@ bool Call::handleSignalingData(
|
|||
|
||||
void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
|
||||
Expects(_type == Type::Outgoing);
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
if (_state.current() == State::ExchangingKeys
|
||||
|| _instance) {
|
||||
|
@ -861,6 +996,7 @@ void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) {
|
|||
|
||||
void Call::startConfirmedCall(const MTPDphoneCall &call) {
|
||||
Expects(_type == Type::Incoming);
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
const auto firstBytes = bytes::make_span(call.vg_a_or_b().v);
|
||||
if (_gaHash != openssl::Sha256(firstBytes)) {
|
||||
|
@ -887,11 +1023,15 @@ void Call::startConfirmedCall(const MTPDphoneCall &call) {
|
|||
}
|
||||
|
||||
void Call::createAndStartController(const MTPDphoneCall &call) {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
_discardByTimeoutTimer.cancel();
|
||||
if (!checkCallFields(call) || _authKey.size() != kAuthKeySize) {
|
||||
return;
|
||||
}
|
||||
|
||||
_conferenceSupported = call.is_conference_supported();
|
||||
|
||||
const auto &protocol = call.vprotocol().c_phoneCallProtocol();
|
||||
const auto &serverConfig = _user->session().serverConfig();
|
||||
|
||||
|
@ -1060,6 +1200,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
|
|||
const auto track = (state != State::FailedHangingUp)
|
||||
&& (state != State::Failed)
|
||||
&& (state != State::HangingUp)
|
||||
&& (state != State::MigrationHangingUp)
|
||||
&& (state != State::Ended)
|
||||
&& (state != State::EndedByOtherDevice)
|
||||
&& (state != State::Busy);
|
||||
|
@ -1083,6 +1224,8 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
|
|||
}
|
||||
|
||||
void Call::handleControllerStateChange(tgcalls::State state) {
|
||||
Expects(!conferenceInvite());
|
||||
|
||||
switch (state) {
|
||||
case tgcalls::State::WaitInit: {
|
||||
DEBUG_LOG(("Call Info: State changed to WaitingInit."));
|
||||
|
@ -1176,6 +1319,11 @@ void Call::setState(State state) {
|
|||
&& state != State::Failed) {
|
||||
return;
|
||||
}
|
||||
if (was == State::MigrationHangingUp
|
||||
&& state != State::Ended
|
||||
&& state != State::Failed) {
|
||||
return;
|
||||
}
|
||||
if (was != state) {
|
||||
_state = state;
|
||||
|
||||
|
@ -1311,6 +1459,11 @@ void Call::toggleScreenSharing(std::optional<QString> uniqueId) {
|
|||
_videoOutgoing->setState(Webrtc::VideoState::Active);
|
||||
}
|
||||
|
||||
auto Call::peekVideoCapture() const
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> {
|
||||
return _videoCapture;
|
||||
}
|
||||
|
||||
auto Call::playbackDeviceIdValue() const
|
||||
-> rpl::producer<Webrtc::DeviceResolvedId> {
|
||||
return _playbackDeviceId.value();
|
||||
|
@ -1324,7 +1477,10 @@ rpl::producer<Webrtc::DeviceResolvedId> Call::cameraDeviceIdValue() const {
|
|||
return _cameraDeviceId.value();
|
||||
}
|
||||
|
||||
void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
|
||||
void Call::finish(
|
||||
FinishType type,
|
||||
const MTPPhoneCallDiscardReason &reason,
|
||||
Data::GroupCall *migrateCall) {
|
||||
Expects(type != FinishType::None);
|
||||
|
||||
setSignalBarCount(kSignalBarFinished);
|
||||
|
@ -1349,8 +1505,15 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
|
|||
|| state == State::Ended
|
||||
|| state == State::Failed) {
|
||||
return;
|
||||
}
|
||||
if (!_id) {
|
||||
} else if (conferenceInvite()) {
|
||||
if (migrateCall) {
|
||||
_delegate->callFinished(this);
|
||||
} else {
|
||||
Core::App().calls().declineIncomingConferenceInvites(_conferenceId);
|
||||
setState(finalState);
|
||||
}
|
||||
return;
|
||||
} else if (!_id) {
|
||||
setState(finalState);
|
||||
return;
|
||||
}
|
||||
|
@ -1372,6 +1535,13 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
|
|||
|
||||
// We want to discard request still being sent and processed even if
|
||||
// the call is already destroyed.
|
||||
if (migrateCall) {
|
||||
_user->owner().registerInvitedToCallUser(
|
||||
migrateCall->id(),
|
||||
migrateCall,
|
||||
_user,
|
||||
true);
|
||||
}
|
||||
const auto session = &_user->session();
|
||||
const auto weak = base::make_weak(this);
|
||||
session->api().request(MTPphone_DiscardCall( // We send 'discard' here.
|
||||
|
@ -1413,10 +1583,10 @@ void Call::handleRequestError(const QString &error) {
|
|||
? Lang::Hard::CallErrorIncompatible().replace(
|
||||
"{user}",
|
||||
_user->name())
|
||||
: QString();
|
||||
: error;
|
||||
if (!inform.isEmpty()) {
|
||||
if (const auto window = Core::App().windowFor(
|
||||
Window::SeparateId(_user))) {
|
||||
::Window::SeparateId(_user))) {
|
||||
window->show(Ui::MakeInformBox(inform));
|
||||
} else {
|
||||
Ui::show(Ui::MakeInformBox(inform));
|
||||
|
@ -1435,7 +1605,7 @@ void Call::handleControllerError(const QString &error) {
|
|||
: QString();
|
||||
if (!inform.isEmpty()) {
|
||||
if (const auto window = Core::App().windowFor(
|
||||
Window::SeparateId(_user))) {
|
||||
::Window::SeparateId(_user))) {
|
||||
window->show(Ui::MakeInformBox(inform));
|
||||
} else {
|
||||
Ui::show(Ui::MakeInformBox(inform));
|
||||
|
@ -1464,7 +1634,6 @@ Call::~Call() {
|
|||
}
|
||||
|
||||
void UpdateConfig(const std::string &data) {
|
||||
tgcalls::SetLegacyGlobalServerConfig(data);
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "mtproto/mtproto_auth_key.h"
|
||||
#include "webrtc/webrtc_device_resolver.h"
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Media {
|
||||
namespace Audio {
|
||||
class Track;
|
||||
|
@ -36,6 +40,8 @@ struct DeviceResolvedId;
|
|||
|
||||
namespace Calls {
|
||||
|
||||
struct StartConferenceInfo;
|
||||
|
||||
struct DhConfig {
|
||||
int32 version = 0;
|
||||
int32 g = 0;
|
||||
|
@ -98,6 +104,13 @@ public:
|
|||
not_null<UserData*> user,
|
||||
Type type,
|
||||
bool video);
|
||||
Call(
|
||||
not_null<Delegate*> delegate,
|
||||
not_null<UserData*> user,
|
||||
CallId conferenceId,
|
||||
MsgId conferenceInviteMsgId,
|
||||
std::vector<not_null<PeerData*>> conferenceParticipants,
|
||||
bool video);
|
||||
|
||||
[[nodiscard]] Type type() const {
|
||||
return _type;
|
||||
|
@ -108,6 +121,19 @@ public:
|
|||
[[nodiscard]] CallId id() const {
|
||||
return _id;
|
||||
}
|
||||
[[nodiscard]] bool conferenceInvite() const {
|
||||
return _conferenceId != 0;
|
||||
}
|
||||
[[nodiscard]] CallId conferenceId() const {
|
||||
return _conferenceId;
|
||||
}
|
||||
[[nodiscard]] MsgId conferenceInviteMsgId() const {
|
||||
return _conferenceInviteMsgId;
|
||||
}
|
||||
[[nodiscard]] auto conferenceParticipants() const
|
||||
-> const std::vector<not_null<PeerData*>> & {
|
||||
return _conferenceParticipants;
|
||||
}
|
||||
[[nodiscard]] bool isIncomingWaiting() const;
|
||||
|
||||
void start(bytes::const_span random);
|
||||
|
@ -122,6 +148,7 @@ public:
|
|||
FailedHangingUp,
|
||||
Failed,
|
||||
HangingUp,
|
||||
MigrationHangingUp,
|
||||
Ended,
|
||||
EndedByOtherDevice,
|
||||
ExchangingKeys,
|
||||
|
@ -143,6 +170,10 @@ public:
|
|||
return _errors.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> confereceSupportedValue() const {
|
||||
return _conferenceSupported.value();
|
||||
}
|
||||
|
||||
enum class RemoteAudioState {
|
||||
Muted,
|
||||
Active,
|
||||
|
@ -198,7 +229,9 @@ public:
|
|||
|
||||
void applyUserConfirmation();
|
||||
void answer();
|
||||
void hangup();
|
||||
void hangup(
|
||||
Data::GroupCall *migrateCall = nullptr,
|
||||
const QString &migrateSlug = QString());
|
||||
void redial();
|
||||
|
||||
bool isKeyShaForFingerprintReady() const;
|
||||
|
@ -220,6 +253,8 @@ public:
|
|||
[[nodiscard]] QString screenSharingDeviceId() const;
|
||||
void toggleCameraSharing(bool enabled);
|
||||
void toggleScreenSharing(std::optional<QString> uniqueId);
|
||||
[[nodiscard]] auto peekVideoCapture() const
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
|
||||
|
||||
[[nodiscard]] auto playbackDeviceIdValue() const
|
||||
-> rpl::producer<Webrtc::DeviceResolvedId>;
|
||||
|
@ -246,7 +281,9 @@ private:
|
|||
void finish(
|
||||
FinishType type,
|
||||
const MTPPhoneCallDiscardReason &reason
|
||||
= MTP_phoneCallDiscardReasonDisconnect());
|
||||
= MTP_phoneCallDiscardReasonDisconnect(),
|
||||
Data::GroupCall *migrateCall = nullptr);
|
||||
void finishByMigration(const QString &slug);
|
||||
void startOutgoing();
|
||||
void startIncoming();
|
||||
void startWaitingTrack();
|
||||
|
@ -263,6 +300,7 @@ private:
|
|||
bool checkCallFields(const MTPDphoneCallAccepted &call);
|
||||
|
||||
void actuallyAnswer();
|
||||
void acceptConferenceInvite();
|
||||
void confirmAcceptedCall(const MTPDphoneCallAccepted &call);
|
||||
void startConfirmedCall(const MTPDphoneCall &call);
|
||||
void setState(State state);
|
||||
|
@ -280,11 +318,15 @@ private:
|
|||
tgcalls::AudioState audio,
|
||||
tgcalls::VideoState video);
|
||||
|
||||
[[nodiscard]] StartConferenceInfo migrateConferenceInfo(
|
||||
StartConferenceInfo extend);
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
const not_null<UserData*> _user;
|
||||
MTP::Sender _api;
|
||||
Type _type = Type::Outgoing;
|
||||
rpl::variable<State> _state = State::Starting;
|
||||
rpl::variable<bool> _conferenceSupported = false;
|
||||
rpl::variable<RemoteAudioState> _remoteAudioState
|
||||
= RemoteAudioState::Active;
|
||||
rpl::variable<Webrtc::VideoState> _remoteVideoState;
|
||||
|
@ -316,6 +358,10 @@ private:
|
|||
uint64 _accessHash = 0;
|
||||
uint64 _keyFingerprint = 0;
|
||||
|
||||
CallId _conferenceId = 0;
|
||||
MsgId _conferenceInviteMsgId = 0;
|
||||
std::vector<not_null<PeerData*>> _conferenceParticipants;
|
||||
|
||||
std::unique_ptr<tgcalls::Instance> _instance;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _videoCapture;
|
||||
QString _videoCaptureDeviceId;
|
||||
|
|
|
@ -7,20 +7,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "calls/calls_emoji_fingerprint.h"
|
||||
|
||||
#include "base/random.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_signal_bars.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "data/data_user.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTooltipShowTimeoutMs = 350;
|
||||
constexpr auto kTooltipShowTimeoutMs = crl::time(500);
|
||||
constexpr auto kCarouselOneDuration = crl::time(100);
|
||||
constexpr auto kStartTimeShift = crl::time(50);
|
||||
constexpr auto kEmojiInFingerprint = 4;
|
||||
constexpr auto kEmojiInCarousel = 10;
|
||||
|
||||
const ushort Data[] = {
|
||||
0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21,
|
||||
|
@ -109,8 +118,11 @@ const ushort Offsets[] = {
|
|||
620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641,
|
||||
642, 643, 644, 646, 648, 650, 652, 654, 656, 658 };
|
||||
|
||||
constexpr auto kEmojiCount = (base::array_size(Offsets) - 1);
|
||||
|
||||
uint64 ComputeEmojiIndex(bytes::const_span bytes) {
|
||||
Expects(bytes.size() == 8);
|
||||
|
||||
return ((gsl::to_integer<uint64>(bytes[0]) & 0x7F) << 56)
|
||||
| (gsl::to_integer<uint64>(bytes[1]) << 48)
|
||||
| (gsl::to_integer<uint64>(bytes[2]) << 40)
|
||||
|
@ -121,40 +133,41 @@ uint64 ComputeEmojiIndex(bytes::const_span bytes) {
|
|||
| (gsl::to_integer<uint64>(bytes[7]));
|
||||
}
|
||||
|
||||
[[nodiscard]] EmojiPtr EmojiByIndex(int index) {
|
||||
Expects(index >= 0 && index < kEmojiCount);
|
||||
|
||||
const auto offset = Offsets[index];
|
||||
const auto size = Offsets[index + 1] - offset;
|
||||
const auto string = QString::fromRawData(
|
||||
reinterpret_cast<const QChar*>(Data + offset),
|
||||
size);
|
||||
return Ui::Emoji::Find(string);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<EmojiPtr> ComputeEmojiFingerprint(not_null<Call*> call) {
|
||||
auto result = std::vector<EmojiPtr>();
|
||||
constexpr auto EmojiCount = (base::array_size(Offsets) - 1);
|
||||
for (auto index = 0; index != EmojiCount; ++index) {
|
||||
auto offset = Offsets[index];
|
||||
auto size = Offsets[index + 1] - offset;
|
||||
auto string = QString::fromRawData(
|
||||
reinterpret_cast<const QChar*>(Data + offset),
|
||||
size);
|
||||
auto emoji = Ui::Emoji::Find(string);
|
||||
Assert(emoji != nullptr);
|
||||
if (!call->isKeyShaForFingerprintReady()) {
|
||||
return {};
|
||||
}
|
||||
if (call->isKeyShaForFingerprintReady()) {
|
||||
auto sha256 = call->getKeyShaForFingerprint();
|
||||
constexpr auto kPartSize = 8;
|
||||
for (auto partOffset = 0; partOffset != sha256.size(); partOffset += kPartSize) {
|
||||
auto value = ComputeEmojiIndex(gsl::make_span(sha256).subspan(partOffset, kPartSize));
|
||||
auto index = value % EmojiCount;
|
||||
auto offset = Offsets[index];
|
||||
auto size = Offsets[index + 1] - offset;
|
||||
auto string = QString::fromRawData(
|
||||
reinterpret_cast<const QChar*>(Data + offset),
|
||||
size);
|
||||
auto emoji = Ui::Emoji::Find(string);
|
||||
Assert(emoji != nullptr);
|
||||
result.push_back(emoji);
|
||||
}
|
||||
return ComputeEmojiFingerprint(call->getKeyShaForFingerprint());
|
||||
}
|
||||
|
||||
std::vector<EmojiPtr> ComputeEmojiFingerprint(
|
||||
bytes::const_span fingerprint) {
|
||||
auto result = std::vector<EmojiPtr>();
|
||||
constexpr auto kPartSize = 8;
|
||||
for (auto partOffset = 0
|
||||
; partOffset != fingerprint.size()
|
||||
; partOffset += kPartSize) {
|
||||
const auto value = ComputeEmojiIndex(
|
||||
fingerprint.subspan(partOffset, kPartSize));
|
||||
result.push_back(EmojiByIndex(value % kEmojiCount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
||||
base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Call*> call) {
|
||||
class EmojiTooltipShower final : public Ui::AbstractTooltipShower {
|
||||
|
@ -180,8 +193,8 @@ object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
|||
|
||||
};
|
||||
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto raw = result.data();
|
||||
auto result = base::make_unique_q<Ui::RpWidget>(parent);
|
||||
const auto raw = result.get();
|
||||
|
||||
// Emoji tooltip.
|
||||
const auto shower = raw->lifetime().make_state<EmojiTooltipShower>(
|
||||
|
@ -295,4 +308,516 @@ object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
|||
return result;
|
||||
}
|
||||
|
||||
FingerprintBadge SetupFingerprintBadge(
|
||||
rpl::lifetime &on,
|
||||
rpl::producer<QByteArray> fingerprint) {
|
||||
struct State {
|
||||
FingerprintBadgeState data;
|
||||
Ui::Animations::Basic animation;
|
||||
Fn<void(crl::time)> update;
|
||||
rpl::event_stream<> repaints;
|
||||
};
|
||||
const auto state = on.make_state<State>();
|
||||
|
||||
state->data.speed = 1. / kCarouselOneDuration;
|
||||
state->update = [=](crl::time now) {
|
||||
// speed-up-duration = 2 * one / speed.
|
||||
const auto one = 1.;
|
||||
const auto speedUpDuration = 2 * kCarouselOneDuration;
|
||||
const auto speed0 = one / kCarouselOneDuration;
|
||||
|
||||
auto updated = false;
|
||||
auto animating = false;
|
||||
for (auto &entry : state->data.entries) {
|
||||
if (!entry.time) {
|
||||
continue;
|
||||
}
|
||||
animating = true;
|
||||
if (entry.time >= now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updated = true;
|
||||
const auto elapsed = (now - entry.time) * 1.;
|
||||
entry.time = now;
|
||||
|
||||
Assert(!entry.emoji || entry.sliding.size() > 1);
|
||||
const auto slideCount = entry.emoji
|
||||
? (int(entry.sliding.size()) - 1) * one
|
||||
: (kEmojiInCarousel + (elapsed / kCarouselOneDuration));
|
||||
const auto finalPosition = slideCount * one;
|
||||
const auto distance = finalPosition - entry.position;
|
||||
|
||||
const auto accelerate0 = speed0 - entry.speed;
|
||||
const auto decelerate0 = speed0;
|
||||
const auto acceleration0 = speed0 / speedUpDuration;
|
||||
const auto taccelerate0 = accelerate0 / acceleration0;
|
||||
const auto tdecelerate0 = decelerate0 / acceleration0;
|
||||
const auto paccelerate0 = entry.speed * taccelerate0
|
||||
+ acceleration0 * taccelerate0 * taccelerate0 / 2.;
|
||||
const auto pdecelerate0 = 0
|
||||
+ acceleration0 * tdecelerate0 * tdecelerate0 / 2.;
|
||||
const auto ttozero = entry.speed / acceleration0;
|
||||
if (paccelerate0 + pdecelerate0 <= distance) {
|
||||
// We have time to accelerate to speed0,
|
||||
// maybe go some time on speed0 and then decelerate to 0.
|
||||
const auto uaccelerate0 = std::min(taccelerate0, elapsed);
|
||||
const auto left = distance - paccelerate0 - pdecelerate0;
|
||||
const auto tconstant = left / speed0;
|
||||
const auto uconstant = std::min(
|
||||
tconstant,
|
||||
elapsed - uaccelerate0);
|
||||
const auto udecelerate0 = std::min(
|
||||
tdecelerate0,
|
||||
elapsed - uaccelerate0 - uconstant);
|
||||
if (udecelerate0 >= tdecelerate0) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
entry.position += entry.speed * uaccelerate0
|
||||
+ acceleration0 * uaccelerate0 * uaccelerate0 / 2.
|
||||
+ speed0 * uconstant
|
||||
+ speed0 * udecelerate0
|
||||
- acceleration0 * udecelerate0 * udecelerate0 / 2.;
|
||||
entry.speed += acceleration0
|
||||
* (uaccelerate0 - udecelerate0);
|
||||
}
|
||||
} else if (acceleration0 * ttozero * ttozero / 2 <= distance) {
|
||||
// We have time to accelerate at least for some time >= 0,
|
||||
// and then decelerate to 0 to make it to final position.
|
||||
//
|
||||
// peak = entry.speed + acceleration0 * t
|
||||
// tdecelerate = peak / acceleration0
|
||||
// distance = entry.speed * t
|
||||
// + acceleration0 * t * t / 2
|
||||
// + acceleration0 * tdecelerate * tdecelerate / 2
|
||||
const auto det = entry.speed * entry.speed / 2
|
||||
+ distance * acceleration0;
|
||||
const auto t = std::max(
|
||||
(sqrt(det) - entry.speed) / acceleration0,
|
||||
0.);
|
||||
|
||||
const auto taccelerate = t;
|
||||
const auto uaccelerate = std::min(taccelerate, elapsed);
|
||||
const auto tdecelerate = t + (entry.speed / acceleration0);
|
||||
const auto udecelerate = std::min(
|
||||
tdecelerate,
|
||||
elapsed - uaccelerate);
|
||||
if (udecelerate >= tdecelerate) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
const auto topspeed = entry.speed
|
||||
+ acceleration0 * taccelerate;
|
||||
entry.position += entry.speed * uaccelerate
|
||||
+ acceleration0 * uaccelerate * uaccelerate / 2.
|
||||
+ topspeed * udecelerate
|
||||
- acceleration0 * udecelerate * udecelerate / 2.;
|
||||
entry.speed += acceleration0
|
||||
* (uaccelerate - udecelerate);
|
||||
}
|
||||
} else {
|
||||
// We just need to decelerate to 0,
|
||||
// faster than acceleration0.
|
||||
Assert(entry.speed > 0);
|
||||
const auto tdecelerate = 2 * distance / entry.speed;
|
||||
const auto udecelerate = std::min(tdecelerate, elapsed);
|
||||
if (udecelerate >= tdecelerate) {
|
||||
Assert(entry.emoji != nullptr);
|
||||
entry = { .emoji = entry.emoji };
|
||||
} else {
|
||||
const auto a = entry.speed / tdecelerate;
|
||||
entry.position += entry.speed * udecelerate
|
||||
- a * udecelerate * udecelerate / 2;
|
||||
entry.speed -= a * udecelerate;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.position >= kEmojiInCarousel) {
|
||||
entry.position -= qFloor(entry.position / kEmojiInCarousel)
|
||||
* kEmojiInCarousel;
|
||||
}
|
||||
while (entry.position >= 1.) {
|
||||
Assert(!entry.sliding.empty());
|
||||
entry.position -= 1.;
|
||||
entry.sliding.erase(begin(entry.sliding));
|
||||
if (entry.emoji && entry.sliding.size() < 2) {
|
||||
entry = { .emoji = entry.emoji };
|
||||
break;
|
||||
} else if (entry.sliding.empty()) {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
}
|
||||
if (!entry.emoji
|
||||
&& entry.position > 0.
|
||||
&& entry.sliding.size() < 2) {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
}
|
||||
if (!animating) {
|
||||
state->animation.stop();
|
||||
} else if (updated) {
|
||||
state->repaints.fire({});
|
||||
}
|
||||
};
|
||||
state->animation.init(state->update);
|
||||
state->data.entries.resize(kEmojiInFingerprint);
|
||||
|
||||
const auto fillCarousel = [=](
|
||||
int index,
|
||||
base::BufferedRandom<uint32> &buffered) {
|
||||
auto &entry = state->data.entries[index];
|
||||
auto indices = std::vector<int>();
|
||||
indices.reserve(kEmojiInCarousel);
|
||||
auto count = kEmojiCount;
|
||||
for (auto i = 0; i != kEmojiInCarousel; ++i, --count) {
|
||||
auto index = base::RandomIndex(count, buffered);
|
||||
for (const auto &already : indices) {
|
||||
if (index >= already) {
|
||||
++index;
|
||||
}
|
||||
}
|
||||
indices.push_back(index);
|
||||
}
|
||||
|
||||
entry.carousel.clear();
|
||||
entry.carousel.reserve(kEmojiInCarousel);
|
||||
for (const auto index : indices) {
|
||||
entry.carousel.push_back(EmojiByIndex(index));
|
||||
}
|
||||
};
|
||||
|
||||
const auto startTo = [=](
|
||||
int index,
|
||||
EmojiPtr emoji,
|
||||
crl::time now,
|
||||
base::BufferedRandom<uint32> &buffered) {
|
||||
auto &entry = state->data.entries[index];
|
||||
if ((entry.emoji == emoji) && (emoji || entry.time)) {
|
||||
return;
|
||||
} else if (!entry.time) {
|
||||
Assert(entry.sliding.empty());
|
||||
|
||||
if (entry.emoji) {
|
||||
entry.sliding.push_back(entry.emoji);
|
||||
} else if (emoji) {
|
||||
// Just initialize if we get emoji right from the start.
|
||||
entry.emoji = emoji;
|
||||
return;
|
||||
}
|
||||
entry.time = now + index * kStartTimeShift;
|
||||
|
||||
fillCarousel(index, buffered);
|
||||
}
|
||||
entry.emoji = emoji;
|
||||
if (entry.emoji) {
|
||||
entry.sliding.push_back(entry.emoji);
|
||||
} else {
|
||||
const auto index = (entry.added++) % kEmojiInCarousel;
|
||||
entry.sliding.push_back(entry.carousel[index]);
|
||||
}
|
||||
};
|
||||
|
||||
std::move(
|
||||
fingerprint
|
||||
) | rpl::start_with_next([=](const QByteArray &fingerprint) {
|
||||
auto buffered = base::BufferedRandom<uint32>(
|
||||
kEmojiInCarousel * kEmojiInFingerprint);
|
||||
const auto now = crl::now();
|
||||
const auto emoji = (fingerprint.size() >= 32)
|
||||
? ComputeEmojiFingerprint(
|
||||
bytes::make_span(fingerprint).subspan(0, 32))
|
||||
: std::vector<EmojiPtr>();
|
||||
state->update(now);
|
||||
|
||||
if (emoji.size() == kEmojiInFingerprint) {
|
||||
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
|
||||
startTo(i, emoji[i], now, buffered);
|
||||
}
|
||||
} else {
|
||||
for (auto i = 0; i != kEmojiInFingerprint; ++i) {
|
||||
startTo(i, nullptr, now, buffered);
|
||||
}
|
||||
}
|
||||
if (!state->animation.animating()) {
|
||||
state->animation.start();
|
||||
}
|
||||
}, on);
|
||||
|
||||
return { .state = &state->data, .repaints = state->repaints.events() };
|
||||
}
|
||||
|
||||
void SetupFingerprintTooltip(not_null<Ui::RpWidget*> widget) {
|
||||
struct State {
|
||||
std::unique_ptr<Ui::ImportantTooltip> tooltip;
|
||||
Fn<void()> updateGeometry;
|
||||
Fn<void(bool)> toggleTooltip;
|
||||
};
|
||||
const auto state = widget->lifetime().make_state<State>();
|
||||
state->updateGeometry = [=] {
|
||||
if (!state->tooltip.get()) {
|
||||
return;
|
||||
}
|
||||
const auto geometry = Ui::MapFrom(
|
||||
widget->window(),
|
||||
widget,
|
||||
widget->rect());
|
||||
if (geometry.isEmpty()) {
|
||||
state->toggleTooltip(false);
|
||||
return;
|
||||
}
|
||||
const auto weak = QPointer<QWidget>(state->tooltip.get());
|
||||
const auto countPosition = [=](QSize size) {
|
||||
const auto result = geometry.bottomLeft()
|
||||
+ QPoint(
|
||||
geometry.width() / 2,
|
||||
st::confcallFingerprintTooltipSkip)
|
||||
- QPoint(size.width() / 2, 0);
|
||||
return result;
|
||||
};
|
||||
state->tooltip.get()->pointAt(
|
||||
geometry,
|
||||
RectPart::Bottom,
|
||||
countPosition);
|
||||
};
|
||||
state->toggleTooltip = [=](bool show) {
|
||||
if (const auto was = state->tooltip.release()) {
|
||||
was->toggleAnimated(false);
|
||||
}
|
||||
if (!show) {
|
||||
return;
|
||||
}
|
||||
const auto text = tr::lng_confcall_e2e_about(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
state->tooltip = std::make_unique<Ui::ImportantTooltip>(
|
||||
widget->window(),
|
||||
Ui::MakeNiceTooltipLabel(
|
||||
widget,
|
||||
rpl::single(text),
|
||||
st::confcallFingerprintTooltipMaxWidth,
|
||||
st::confcallFingerprintTooltipLabel),
|
||||
st::confcallFingerprintTooltip);
|
||||
const auto raw = state->tooltip.get();
|
||||
const auto weak = QPointer<QWidget>(raw);
|
||||
const auto destroy = [=] {
|
||||
delete weak.data();
|
||||
};
|
||||
raw->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
raw->setHiddenCallback(destroy);
|
||||
state->updateGeometry();
|
||||
raw->toggleAnimated(true);
|
||||
};
|
||||
|
||||
widget->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Enter) {
|
||||
state->toggleTooltip(true);
|
||||
} else if (type == QEvent::Leave) {
|
||||
state->toggleTooltip(false);
|
||||
}
|
||||
}, widget->lifetime());
|
||||
}
|
||||
|
||||
QImage MakeVerticalShadow(int height) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto result = QImage(
|
||||
QSize(1, height) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
auto p = QPainter(&result);
|
||||
auto g = QLinearGradient(0, 0, 0, height);
|
||||
auto color = st::groupCallMembersBg->c;
|
||||
auto trans = color;
|
||||
trans.setAlpha(0);
|
||||
g.setStops({
|
||||
{ 0.0, color },
|
||||
{ 0.4, trans },
|
||||
{ 0.6, trans },
|
||||
{ 1.0, color },
|
||||
});
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.fillRect(0, 0, 1, height, g);
|
||||
p.end();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SetupFingerprintBadgeWidget(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<const FingerprintBadgeState*> state,
|
||||
rpl::producer<> repaints) {
|
||||
auto &lifetime = widget->lifetime();
|
||||
|
||||
const auto button = Ui::CreateChild<Ui::RpWidget>(widget);
|
||||
button->show();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
button,
|
||||
QString(),
|
||||
st::confcallFingerprintText);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
label->show();
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto esize = Ui::Emoji::GetSizeNormal();
|
||||
const auto size = esize / ratio;
|
||||
widget->widthValue() | rpl::start_with_next([=](int width) {
|
||||
static_assert(!(kEmojiInFingerprint % 2));
|
||||
|
||||
const auto available = width
|
||||
- st::confcallFingerprintMargins.left()
|
||||
- st::confcallFingerprintMargins.right()
|
||||
- (kEmojiInFingerprint * size)
|
||||
- (kEmojiInFingerprint - 2) * st::confcallFingerprintSkip
|
||||
- st::confcallFingerprintTextMargins.left()
|
||||
- st::confcallFingerprintTextMargins.right();
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
label->setText(tr::lng_confcall_e2e_badge(tr::now));
|
||||
if (label->textMaxWidth() > available) {
|
||||
label->setText(tr::lng_confcall_e2e_badge_small(tr::now));
|
||||
}
|
||||
const auto use = std::min(available, label->textMaxWidth());
|
||||
label->resizeToWidth(use);
|
||||
|
||||
const auto ontheleft = kEmojiInFingerprint / 2;
|
||||
const auto ontheside = ontheleft * size
|
||||
+ (ontheleft - 1) * st::confcallFingerprintSkip;
|
||||
const auto text = QRect(
|
||||
(width - use) / 2,
|
||||
(st::confcallFingerprintMargins.top()
|
||||
+ st::confcallFingerprintTextMargins.top()),
|
||||
use,
|
||||
label->height());
|
||||
const auto textOuter = text.marginsAdded(
|
||||
st::confcallFingerprintTextMargins);
|
||||
const auto withEmoji = QRect(
|
||||
textOuter.x() - ontheside,
|
||||
textOuter.y(),
|
||||
textOuter.width() + ontheside * 2,
|
||||
size);
|
||||
const auto outer = withEmoji.marginsAdded(
|
||||
st::confcallFingerprintMargins);
|
||||
|
||||
button->setGeometry(outer);
|
||||
label->moveToLeft(text.x() - outer.x(), text.y() - outer.y(), width);
|
||||
|
||||
widget->resize(
|
||||
width,
|
||||
button->height() + st::confcallFingerprintBottomSkip);
|
||||
}, lifetime);
|
||||
|
||||
const auto cache = lifetime.make_state<FingerprintBadgeCache>();
|
||||
button->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(button);
|
||||
|
||||
const auto outer = button->rect();
|
||||
const auto radius = outer.height() / 2.;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::groupCallMembersBg);
|
||||
p.drawRoundedRect(outer, radius, radius);
|
||||
p.setClipRect(outer);
|
||||
|
||||
const auto withEmoji = outer.marginsRemoved(
|
||||
st::confcallFingerprintMargins);
|
||||
p.translate(withEmoji.topLeft());
|
||||
|
||||
const auto text = label->geometry();
|
||||
const auto textOuter = text.marginsAdded(
|
||||
st::confcallFingerprintTextMargins);
|
||||
const auto count = int(state->entries.size());
|
||||
cache->entries.resize(count);
|
||||
cache->shadow = MakeVerticalShadow(outer.height());
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
const auto &entry = state->entries[i];
|
||||
auto &cached = cache->entries[i];
|
||||
const auto shadowed = entry.speed / state->speed;
|
||||
PaintFingerprintEntry(p, entry, cached, esize);
|
||||
if (shadowed > 0.) {
|
||||
p.setOpacity(shadowed);
|
||||
p.drawImage(
|
||||
QRect(0, -st::confcallFingerprintMargins.top(), size, outer.height()),
|
||||
cache->shadow);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (i + 1 == count / 2) {
|
||||
p.translate(size + textOuter.width(), 0);
|
||||
} else {
|
||||
p.translate(size + st::confcallFingerprintSkip, 0);
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
std::move(repaints) | rpl::start_with_next([=] {
|
||||
button->update();
|
||||
}, lifetime);
|
||||
|
||||
SetupFingerprintTooltip(button);
|
||||
}
|
||||
|
||||
void PaintFingerprintEntry(
|
||||
QPainter &p,
|
||||
const FingerprintBadgeState::Entry &entry,
|
||||
FingerprintBadgeCache::Entry &cache,
|
||||
int esize) {
|
||||
const auto stationary = !entry.time;
|
||||
if (stationary) {
|
||||
Ui::Emoji::Draw(p, entry.emoji, esize, 0, 0);
|
||||
return;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = esize / ratio;
|
||||
const auto add = 4;
|
||||
const auto height = size + 2 * add;
|
||||
const auto validateCache = [&](int index, EmojiPtr e) {
|
||||
if (cache.emoji.size() <= index) {
|
||||
cache.emoji.reserve(entry.carousel.size() + 2);
|
||||
cache.emoji.resize(index + 1);
|
||||
}
|
||||
auto &emoji = cache.emoji[index];
|
||||
if (emoji.ptr != e) {
|
||||
emoji.ptr = e;
|
||||
emoji.image = QImage(
|
||||
QSize(size, height) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
emoji.image.setDevicePixelRatio(ratio);
|
||||
emoji.image.fill(Qt::transparent);
|
||||
auto q = QPainter(&emoji.image);
|
||||
Ui::Emoji::Draw(q, e, esize, 0, add);
|
||||
q.end();
|
||||
|
||||
//emoji.image = Images::Blur(
|
||||
// std::move(emoji.image),
|
||||
// false,
|
||||
// Qt::Vertical);
|
||||
}
|
||||
return &emoji;
|
||||
};
|
||||
auto shift = entry.position * height - add;
|
||||
p.translate(0, shift);
|
||||
for (const auto &e : entry.sliding) {
|
||||
const auto index = [&] {
|
||||
const auto i = ranges::find(entry.carousel, e);
|
||||
if (i != end(entry.carousel)) {
|
||||
return int(i - begin(entry.carousel));
|
||||
}
|
||||
return int(entry.carousel.size())
|
||||
+ ((e == entry.sliding.back()) ? 1 : 0);
|
||||
}();
|
||||
const auto entry = validateCache(index, e);
|
||||
p.drawImage(0, 0, entry->image);
|
||||
p.translate(0, -height);
|
||||
shift -= height;
|
||||
}
|
||||
p.translate(0, -shift);
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
|
@ -19,9 +19,54 @@ class Call;
|
|||
|
||||
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
|
||||
not_null<Call*> call);
|
||||
[[nodiscard]] std::vector<EmojiPtr> ComputeEmojiFingerprint(
|
||||
bytes::const_span fingerprint);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
||||
[[nodiscard]] base::unique_qptr<Ui::RpWidget> CreateFingerprintAndSignalBars(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Call*> call);
|
||||
|
||||
struct FingerprintBadgeState {
|
||||
struct Entry {
|
||||
EmojiPtr emoji = nullptr;
|
||||
std::vector<EmojiPtr> sliding;
|
||||
std::vector<EmojiPtr> carousel;
|
||||
crl::time time = 0;
|
||||
float64 speed = 0.;
|
||||
float64 position = 0.;
|
||||
int added = 0;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
float64 speed = 1.;
|
||||
};
|
||||
struct FingerprintBadge {
|
||||
not_null<const FingerprintBadgeState*> state;
|
||||
rpl::producer<> repaints;
|
||||
};
|
||||
FingerprintBadge SetupFingerprintBadge(
|
||||
rpl::lifetime &on,
|
||||
rpl::producer<QByteArray> fingerprint);
|
||||
|
||||
void SetupFingerprintBadgeWidget(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<const FingerprintBadgeState*> state,
|
||||
rpl::producer<> repaints);
|
||||
|
||||
struct FingerprintBadgeCache {
|
||||
struct Emoji {
|
||||
EmojiPtr ptr = nullptr;
|
||||
QImage image;
|
||||
};
|
||||
struct Entry {
|
||||
std::vector<Emoji> emoji;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
QImage shadow;
|
||||
};
|
||||
void PaintFingerprintEntry(
|
||||
QPainter &p,
|
||||
const FingerprintBadgeState::Entry &entry,
|
||||
FingerprintBadgeCache::Entry &cache,
|
||||
int esize);
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -12,9 +12,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_rtmp.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "mtproto/mtproto_dh_utils.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "main/session/session_show.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_account.h"
|
||||
#include "apiwrap.h"
|
||||
|
@ -26,8 +30,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "calls/calls_panel.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_session.h"
|
||||
#include "media/audio/media_audio_track.h"
|
||||
#include "platform/platform_specific.h"
|
||||
|
@ -103,6 +109,8 @@ void Instance::Delegate::callFailed(not_null<Call*> call) {
|
|||
}
|
||||
|
||||
void Instance::Delegate::callRedial(not_null<Call*> call) {
|
||||
Expects(!call->conferenceInvite());
|
||||
|
||||
if (_instance->_currentCall.get() == call) {
|
||||
_instance->refreshDhConfig();
|
||||
}
|
||||
|
@ -230,6 +238,78 @@ void Instance::startOrJoinGroupCall(
|
|||
});
|
||||
}
|
||||
|
||||
void Instance::startOrJoinConferenceCall(StartConferenceInfo args) {
|
||||
Expects(args.call || args.show);
|
||||
|
||||
const auto migrationInfo = (args.migrating
|
||||
&& args.call
|
||||
&& _currentCallPanel)
|
||||
? _currentCallPanel->migrationInfo()
|
||||
: ConferencePanelMigration();
|
||||
if (!args.migrating) {
|
||||
destroyCurrentCall();
|
||||
}
|
||||
|
||||
const auto session = args.show
|
||||
? &args.show->session()
|
||||
: &args.call->session();
|
||||
auto call = std::make_unique<GroupCall>(_delegate.get(), args);
|
||||
const auto raw = call.get();
|
||||
|
||||
session->account().sessionChanges(
|
||||
) | rpl::start_with_next([=] {
|
||||
destroyGroupCall(raw);
|
||||
}, raw->lifetime());
|
||||
|
||||
if (args.call) {
|
||||
_currentGroupCallPanel = std::make_unique<Group::Panel>(
|
||||
raw,
|
||||
migrationInfo);
|
||||
_currentGroupCall = std::move(call);
|
||||
_currentGroupCallChanges.fire_copy(raw);
|
||||
finishConferenceInvitations(args);
|
||||
if (args.migrating) {
|
||||
destroyCurrentCall(args.call.get(), args.linkSlug);
|
||||
}
|
||||
} else {
|
||||
if (const auto was = base::take(_startingGroupCall)) {
|
||||
destroyGroupCall(was.get());
|
||||
}
|
||||
_startingGroupCall = std::move(call);
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::startedConferenceReady(
|
||||
not_null<GroupCall*> call,
|
||||
StartConferenceInfo args) {
|
||||
if (_startingGroupCall.get() != call) {
|
||||
return;
|
||||
}
|
||||
const auto migrationInfo = _currentCallPanel
|
||||
? _currentCallPanel->migrationInfo()
|
||||
: ConferencePanelMigration();
|
||||
_currentGroupCallPanel = std::make_unique<Group::Panel>(
|
||||
call,
|
||||
migrationInfo);
|
||||
_currentGroupCall = std::move(_startingGroupCall);
|
||||
_currentGroupCallChanges.fire_copy(call);
|
||||
const auto real = call->conferenceCall().get();
|
||||
const auto link = real->conferenceInviteLink();
|
||||
const auto slug = Group::ExtractConferenceSlug(link);
|
||||
finishConferenceInvitations(args);
|
||||
destroyCurrentCall(real, slug);
|
||||
}
|
||||
|
||||
void Instance::finishConferenceInvitations(const StartConferenceInfo &args) {
|
||||
Expects(_currentGroupCallPanel != nullptr);
|
||||
|
||||
if (!args.invite.empty()) {
|
||||
_currentGroupCallPanel->migrationInviteUsers(std::move(args.invite));
|
||||
} else if (args.sharingLink) {
|
||||
_currentGroupCallPanel->migrationShowShareLink();
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::confirmLeaveCurrent(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
|
@ -310,7 +390,11 @@ void Instance::playSoundOnce(const QString &key) {
|
|||
|
||||
void Instance::destroyCall(not_null<Call*> call) {
|
||||
if (_currentCall.get() == call) {
|
||||
_currentCallPanel->closeBeforeDestroy();
|
||||
const auto groupCallWindow = _currentGroupCallPanel
|
||||
? _currentGroupCallPanel->window().get()
|
||||
: nullptr;
|
||||
const auto reused = (_currentCallPanel->window() == groupCallWindow);
|
||||
_currentCallPanel->closeBeforeDestroy(reused);
|
||||
_currentCallPanel = nullptr;
|
||||
|
||||
auto taken = base::take(_currentCall);
|
||||
|
@ -326,7 +410,7 @@ void Instance::destroyCall(not_null<Call*> call) {
|
|||
|
||||
void Instance::createCall(
|
||||
not_null<UserData*> user,
|
||||
Call::Type type,
|
||||
CallType type,
|
||||
bool isVideo) {
|
||||
struct Performer final {
|
||||
explicit Performer(Fn<void(bool, bool, const Performer &)> callback)
|
||||
|
@ -385,6 +469,8 @@ void Instance::destroyGroupCall(not_null<GroupCall*> call) {
|
|||
LOG(("Calls::Instance doesn't prevent quit any more."));
|
||||
}
|
||||
Core::App().quitPreventFinished();
|
||||
} else if (_startingGroupCall.get() == call) {
|
||||
base::take(_startingGroupCall);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -411,6 +497,7 @@ void Instance::createGroupCall(
|
|||
|
||||
void Instance::refreshDhConfig() {
|
||||
Expects(_currentCall != nullptr);
|
||||
Expects(!_currentCall->conferenceInvite());
|
||||
|
||||
const auto weak = base::make_weak(_currentCall);
|
||||
_currentCall->user()->session().api().request(MTPmessages_GetDhConfig(
|
||||
|
@ -505,6 +592,8 @@ void Instance::handleUpdate(
|
|||
handleGroupCallUpdate(session, update);
|
||||
}, [&](const MTPDupdateGroupCallParticipants &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [&](const MTPDupdateGroupCallChainBlocks &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [](const auto &) {
|
||||
Unexpected("Update type in Calls::Instance::handleUpdate.");
|
||||
});
|
||||
|
@ -612,12 +701,14 @@ void Instance::handleCallUpdate(
|
|||
void Instance::handleGroupCallUpdate(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdate &update) {
|
||||
if (_currentGroupCall
|
||||
&& (&_currentGroupCall->peer()->session() == session)) {
|
||||
const auto groupCall = _currentGroupCall
|
||||
? _currentGroupCall.get()
|
||||
: _startingGroupCall.get();
|
||||
if (groupCall && (&groupCall->peer()->session() == session)) {
|
||||
update.match([&](const MTPDupdateGroupCall &data) {
|
||||
_currentGroupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
groupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
}, [&](const MTPDupdateGroupCallConnection &data) {
|
||||
_currentGroupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
groupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
}, [](const auto &) {
|
||||
});
|
||||
}
|
||||
|
@ -632,11 +723,24 @@ void Instance::handleGroupCallUpdate(
|
|||
}, [](const MTPDupdateGroupCallParticipants &data) {
|
||||
return data.vcall().match([&](const MTPDinputGroupCall &data) {
|
||||
return data.vid().v;
|
||||
}, [](const auto &) -> CallId {
|
||||
Unexpected("slug/msg in Instance::handleGroupCallUpdate");
|
||||
});
|
||||
}, [](const MTPDupdateGroupCallChainBlocks &data) {
|
||||
return data.vcall().match([&](const MTPDinputGroupCall &data) {
|
||||
return data.vid().v;
|
||||
}, [](const auto &) -> CallId {
|
||||
Unexpected("slug/msg in Instance::handleGroupCallUpdate");
|
||||
});
|
||||
}, [](const auto &) -> CallId {
|
||||
Unexpected("Type in Instance::handleGroupCallUpdate.");
|
||||
});
|
||||
if (const auto existing = session->data().groupCall(callId)) {
|
||||
if (update.type() == mtpc_updateGroupCallChainBlocks) {
|
||||
const auto existing = session->data().groupCall(callId);
|
||||
if (existing && groupCall && groupCall->lookupReal() == existing) {
|
||||
groupCall->handleUpdate(update);
|
||||
}
|
||||
} else if (const auto existing = session->data().groupCall(callId)) {
|
||||
existing->enqueueUpdate(update);
|
||||
} else {
|
||||
applyGroupCallUpdateChecked(session, update);
|
||||
|
@ -646,9 +750,11 @@ void Instance::handleGroupCallUpdate(
|
|||
void Instance::applyGroupCallUpdateChecked(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdate &update) {
|
||||
if (_currentGroupCall
|
||||
&& (&_currentGroupCall->peer()->session() == session)) {
|
||||
_currentGroupCall->handleUpdate(update);
|
||||
const auto groupCall = _currentGroupCall
|
||||
? _currentGroupCall.get()
|
||||
: _startingGroupCall.get();
|
||||
if (groupCall && (&groupCall->peer()->session() == session)) {
|
||||
groupCall->handleUpdate(update);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -683,19 +789,24 @@ bool Instance::inGroupCall() const {
|
|||
&& (state != GroupCall::State::Failed);
|
||||
}
|
||||
|
||||
void Instance::destroyCurrentCall() {
|
||||
void Instance::destroyCurrentCall(
|
||||
Data::GroupCall *migrateCall,
|
||||
const QString &migrateSlug) {
|
||||
if (const auto current = currentCall()) {
|
||||
current->hangup();
|
||||
current->hangup(migrateCall, migrateSlug);
|
||||
if (const auto still = currentCall()) {
|
||||
destroyCall(still);
|
||||
}
|
||||
}
|
||||
if (const auto current = currentGroupCall()) {
|
||||
current->hangup();
|
||||
if (const auto still = currentGroupCall()) {
|
||||
destroyGroupCall(still);
|
||||
if (!migrateCall || current->lookupReal() != migrateCall) {
|
||||
current->hangup();
|
||||
if (const auto still = currentGroupCall()) {
|
||||
destroyGroupCall(still);
|
||||
}
|
||||
}
|
||||
}
|
||||
base::take(_startingGroupCall);
|
||||
}
|
||||
|
||||
bool Instance::hasVisiblePanel(Main::Session *session) const {
|
||||
|
@ -849,4 +960,188 @@ std::shared_ptr<tgcalls::VideoCaptureInterface> Instance::getVideoCapture(
|
|||
return result;
|
||||
}
|
||||
|
||||
const ConferenceInvites &Instance::conferenceInvites(
|
||||
CallId conferenceId) const {
|
||||
static const auto kEmpty = ConferenceInvites();
|
||||
const auto i = _conferenceInvites.find(conferenceId);
|
||||
return (i != end(_conferenceInvites)) ? i->second : kEmpty;
|
||||
}
|
||||
|
||||
void Instance::registerConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
MsgId messageId,
|
||||
bool incoming) {
|
||||
auto &info = _conferenceInvites[conferenceId].users[user];
|
||||
(incoming ? info.incoming : info.outgoing).emplace(messageId);
|
||||
}
|
||||
|
||||
void Instance::unregisterConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
MsgId messageId,
|
||||
bool incoming,
|
||||
bool onlyStopCalling) {
|
||||
const auto i = _conferenceInvites.find(conferenceId);
|
||||
if (i == end(_conferenceInvites)) {
|
||||
return;
|
||||
}
|
||||
const auto j = i->second.users.find(user);
|
||||
if (j == end(i->second.users)) {
|
||||
return;
|
||||
}
|
||||
auto &info = j->second;
|
||||
if (!(incoming ? info.incoming : info.outgoing).remove(messageId)) {
|
||||
return;
|
||||
}
|
||||
if (!incoming) {
|
||||
user->owner().unregisterInvitedToCallUser(
|
||||
conferenceId,
|
||||
user,
|
||||
onlyStopCalling);
|
||||
}
|
||||
if (info.incoming.empty() && info.outgoing.empty()) {
|
||||
i->second.users.erase(j);
|
||||
if (i->second.users.empty()) {
|
||||
_conferenceInvites.erase(i);
|
||||
}
|
||||
}
|
||||
if (_currentCall
|
||||
&& _currentCall->user() == user
|
||||
&& _currentCall->conferenceInviteMsgId() == messageId
|
||||
&& _currentCall->state() == Call::State::WaitingIncoming) {
|
||||
destroyCurrentCall();
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::declineIncomingConferenceInvites(CallId conferenceId) {
|
||||
const auto i = _conferenceInvites.find(conferenceId);
|
||||
if (i == end(_conferenceInvites)) {
|
||||
return;
|
||||
}
|
||||
for (auto j = begin(i->second.users); j != end(i->second.users);) {
|
||||
const auto api = &j->first->session().api();
|
||||
for (const auto &messageId : base::take(j->second.incoming)) {
|
||||
api->request(MTPphone_DeclineConferenceCallInvite(
|
||||
MTP_int(messageId.bare)
|
||||
)).send();
|
||||
}
|
||||
if (j->second.outgoing.empty()) {
|
||||
j = i->second.users.erase(j);
|
||||
} else {
|
||||
++j;
|
||||
}
|
||||
}
|
||||
if (i->second.users.empty()) {
|
||||
_conferenceInvites.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::declineOutgoingConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
bool discard) {
|
||||
const auto i = _conferenceInvites.find(conferenceId);
|
||||
if (i == end(_conferenceInvites)) {
|
||||
return;
|
||||
}
|
||||
const auto j = i->second.users.find(user);
|
||||
if (j == end(i->second.users)) {
|
||||
return;
|
||||
}
|
||||
const auto api = &user->session().api();
|
||||
auto ids = base::take(j->second.outgoing);
|
||||
auto inputs = QVector<MTPint>();
|
||||
for (const auto &messageId : ids) {
|
||||
if (discard) {
|
||||
inputs.push_back(MTP_int(messageId.bare));
|
||||
} else {
|
||||
api->request(MTPphone_DeclineConferenceCallInvite(
|
||||
MTP_int(messageId.bare)
|
||||
)).send();
|
||||
}
|
||||
}
|
||||
if (!inputs.empty()) {
|
||||
user->owner().histories().deleteMessages(
|
||||
user->owner().history(user),
|
||||
std::move(inputs),
|
||||
true);
|
||||
for (const auto &messageId : ids) {
|
||||
if (const auto item = user->owner().message(user, messageId)) {
|
||||
item->destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (j->second.incoming.empty()) {
|
||||
i->second.users.erase(j);
|
||||
if (i->second.users.empty()) {
|
||||
_conferenceInvites.erase(i);
|
||||
}
|
||||
}
|
||||
user->owner().unregisterInvitedToCallUser(conferenceId, user, !discard);
|
||||
}
|
||||
|
||||
void Instance::showConferenceInvite(
|
||||
not_null<UserData*> user,
|
||||
MsgId conferenceInviteMsgId) {
|
||||
const auto item = user->owner().message(user, conferenceInviteMsgId);
|
||||
const auto media = item ? item->media() : nullptr;
|
||||
const auto call = media ? media->call() : nullptr;
|
||||
const auto conferenceId = call ? call->conferenceId : 0;
|
||||
const auto video = call->video;
|
||||
if (!conferenceId
|
||||
|| call->state != Data::CallState::Invitation
|
||||
|| user->isSelf()
|
||||
|| user->session().appConfig().callsDisabledForSession()) {
|
||||
return;
|
||||
} else if (_currentCall
|
||||
&& _currentCall->conferenceId() == conferenceId) {
|
||||
return;
|
||||
} else if (inGroupCall()
|
||||
&& _currentGroupCall->conference()
|
||||
&& _currentGroupCall->conferenceCall()->id() == conferenceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto conferenceParticipants = call->otherParticipants;
|
||||
if (!ranges::contains(conferenceParticipants, user)) {
|
||||
conferenceParticipants.push_back(user);
|
||||
}
|
||||
|
||||
const auto &config = user->session().serverConfig();
|
||||
if (inCall() || inGroupCall()) {
|
||||
declineIncomingConferenceInvites(conferenceId);
|
||||
} else if (item->date() + (config.callRingTimeoutMs / 1000)
|
||||
< base::unixtime::now()) {
|
||||
declineIncomingConferenceInvites(conferenceId);
|
||||
LOG(("Ignoring too old conference call invitation."));
|
||||
} else {
|
||||
const auto delegate = _delegate.get();
|
||||
auto call = std::make_unique<Call>(
|
||||
delegate,
|
||||
user,
|
||||
conferenceId,
|
||||
conferenceInviteMsgId,
|
||||
std::move(conferenceParticipants),
|
||||
video);
|
||||
const auto raw = call.get();
|
||||
|
||||
user->session().account().sessionChanges(
|
||||
) | rpl::start_with_next([=] {
|
||||
destroyCall(raw);
|
||||
}, raw->lifetime());
|
||||
|
||||
if (_currentCall) {
|
||||
_currentCallPanel->replaceCall(raw);
|
||||
std::swap(_currentCall, call);
|
||||
call->hangup();
|
||||
} else {
|
||||
_currentCallPanel = std::make_unique<Panel>(raw);
|
||||
_currentCall = std::move(call);
|
||||
}
|
||||
_currentCallChanges.fire_copy(raw);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -13,6 +13,10 @@ namespace crl {
|
|||
class semaphore;
|
||||
} // namespace crl
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Platform {
|
||||
enum class PermissionType;
|
||||
} // namespace Platform
|
||||
|
@ -31,6 +35,7 @@ class Show;
|
|||
|
||||
namespace Calls::Group {
|
||||
struct JoinInfo;
|
||||
struct ConferenceInfo;
|
||||
class Panel;
|
||||
class ChooseJoinAsProcess;
|
||||
class StartRtmpProcess;
|
||||
|
@ -47,6 +52,8 @@ enum class CallType;
|
|||
class GroupCall;
|
||||
class Panel;
|
||||
struct DhConfig;
|
||||
struct InviteRequest;
|
||||
struct StartConferenceInfo;
|
||||
|
||||
struct StartGroupCallArgs {
|
||||
enum class JoinConfirm {
|
||||
|
@ -59,6 +66,15 @@ struct StartGroupCallArgs {
|
|||
bool scheduleNeeded = false;
|
||||
};
|
||||
|
||||
struct ConferenceInviteMessages {
|
||||
base::flat_set<MsgId> incoming;
|
||||
base::flat_set<MsgId> outgoing;
|
||||
};
|
||||
|
||||
struct ConferenceInvites {
|
||||
base::flat_map<not_null<UserData*>, ConferenceInviteMessages> users;
|
||||
};
|
||||
|
||||
class Instance final : public base::has_weak_ptr {
|
||||
public:
|
||||
Instance();
|
||||
|
@ -69,6 +85,10 @@ public:
|
|||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
StartGroupCallArgs args);
|
||||
void startOrJoinConferenceCall(StartConferenceInfo args);
|
||||
void startedConferenceReady(
|
||||
not_null<GroupCall*> call,
|
||||
StartConferenceInfo args);
|
||||
void showStartWithRtmp(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<PeerData*> peer);
|
||||
|
@ -103,6 +123,28 @@ public:
|
|||
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
|
||||
void requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);
|
||||
|
||||
[[nodiscard]] const ConferenceInvites &conferenceInvites(
|
||||
CallId conferenceId) const;
|
||||
void registerConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
MsgId messageId,
|
||||
bool incoming);
|
||||
void unregisterConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
MsgId messageId,
|
||||
bool incoming,
|
||||
bool onlyStopCalling = false);
|
||||
void showConferenceInvite(
|
||||
not_null<UserData*> user,
|
||||
MsgId conferenceInviteMsgId);
|
||||
void declineIncomingConferenceInvites(CallId conferenceId);
|
||||
void declineOutgoingConferenceInvite(
|
||||
CallId conferenceId,
|
||||
not_null<UserData*> user,
|
||||
bool discard = false);
|
||||
|
||||
[[nodiscard]] FnMut<void()> addAsyncWaiter();
|
||||
|
||||
[[nodiscard]] bool isSharingScreen() const;
|
||||
|
@ -117,6 +159,7 @@ private:
|
|||
|
||||
void createCall(not_null<UserData*> user, CallType type, bool isVideo);
|
||||
void destroyCall(not_null<Call*> call);
|
||||
void finishConferenceInvitations(const StartConferenceInfo &args);
|
||||
|
||||
void createGroupCall(
|
||||
Group::JoinInfo info,
|
||||
|
@ -136,7 +179,9 @@ private:
|
|||
void refreshServerConfig(not_null<Main::Session*> session);
|
||||
bytes::const_span updateDhConfig(const MTPmessages_DhConfig &data);
|
||||
|
||||
void destroyCurrentCall();
|
||||
void destroyCurrentCall(
|
||||
Data::GroupCall *migrateCall = nullptr,
|
||||
const QString &migrateSlug = QString());
|
||||
void handleCallUpdate(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPPhoneCall &call);
|
||||
|
@ -159,6 +204,7 @@ private:
|
|||
std::unique_ptr<Panel> _currentCallPanel;
|
||||
|
||||
std::unique_ptr<GroupCall> _currentGroupCall;
|
||||
std::unique_ptr<GroupCall> _startingGroupCall;
|
||||
rpl::event_stream<GroupCall*> _currentGroupCallChanges;
|
||||
std::unique_ptr<Group::Panel> _currentGroupCallPanel;
|
||||
|
||||
|
@ -167,6 +213,8 @@ private:
|
|||
const std::unique_ptr<Group::ChooseJoinAsProcess> _chooseJoinAs;
|
||||
const std::unique_ptr<Group::StartRtmpProcess> _startWithRtmp;
|
||||
|
||||
base::flat_map<CallId, ConferenceInvites> _conferenceInvites;
|
||||
|
||||
base::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;
|
||||
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "calls/calls_panel.h"
|
||||
|
||||
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
|
@ -15,12 +16,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_cloud_file.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_invite_controller.h"
|
||||
#include "calls/ui/calls_device_menu.h"
|
||||
#include "calls/calls_emoji_fingerprint.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "calls/calls_signal_bars.h"
|
||||
#include "calls/calls_userpic.h"
|
||||
#include "calls/calls_video_bubble.h"
|
||||
#include "calls/calls_video_incoming.h"
|
||||
#include "calls/calls_window.h"
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "ui/widgets/call_button.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
@ -45,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/integration.h"
|
||||
#include "core/application.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/session/session_show.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "platform/platform_specific.h"
|
||||
|
@ -97,46 +102,56 @@ constexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000);
|
|||
Panel::Panel(not_null<Call*> call)
|
||||
: _call(call)
|
||||
, _user(call->user())
|
||||
, _layerBg(std::make_unique<Ui::LayerManager>(widget()))
|
||||
#ifndef Q_OS_MAC
|
||||
, _controls(Ui::Platform::SetupSeparateTitleControls(
|
||||
window(),
|
||||
st::callTitle,
|
||||
[=](bool maximized) { toggleFullScreen(maximized); }))
|
||||
#endif // !Q_OS_MAC
|
||||
, _window(std::make_shared<Window>())
|
||||
, _bodySt(&st::callBodyLayout)
|
||||
, _answerHangupRedial(widget(), st::callAnswer, &st::callHangup)
|
||||
, _decline(widget(), object_ptr<Ui::CallButton>(widget(), st::callHangup))
|
||||
, _cancel(widget(), object_ptr<Ui::CallButton>(widget(), st::callCancel))
|
||||
, _answerHangupRedial(
|
||||
std::in_place,
|
||||
widget(),
|
||||
st::callAnswer,
|
||||
&st::callHangup)
|
||||
, _decline(
|
||||
std::in_place,
|
||||
widget(),
|
||||
object_ptr<Ui::CallButton>(widget(), st::callHangup))
|
||||
, _cancel(
|
||||
std::in_place,
|
||||
widget(),
|
||||
object_ptr<Ui::CallButton>(widget(), st::callCancel))
|
||||
, _screencast(
|
||||
std::in_place,
|
||||
widget(),
|
||||
object_ptr<Ui::CallButton>(
|
||||
widget(),
|
||||
st::callScreencastOn,
|
||||
&st::callScreencastOff))
|
||||
, _camera(widget(), st::callCameraMute, &st::callCameraUnmute)
|
||||
, _camera(std::in_place, widget(), st::callCameraMute, &st::callCameraUnmute)
|
||||
, _mute(
|
||||
std::in_place,
|
||||
widget(),
|
||||
object_ptr<Ui::CallButton>(
|
||||
widget(),
|
||||
st::callMicrophoneMute,
|
||||
&st::callMicrophoneUnmute))
|
||||
, _name(widget(), st::callName)
|
||||
, _status(widget(), st::callStatus)
|
||||
, _addPeople(
|
||||
std::in_place,
|
||||
widget(),
|
||||
object_ptr<Ui::CallButton>(widget(), st::callAddPeople))
|
||||
, _name(std::in_place, widget(), st::callName)
|
||||
, _status(std::in_place, widget(), st::callStatus)
|
||||
, _hideControlsTimer([=] { requestControlsHidden(true); })
|
||||
, _controlsShownForceTimer([=] { controlsShownForce(false); }) {
|
||||
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
|
||||
_layerBg->setHideByBackgroundClick(true);
|
||||
|
||||
_decline->setDuration(st::callPanelDuration);
|
||||
_decline->entity()->setText(tr::lng_call_decline());
|
||||
_cancel->setDuration(st::callPanelDuration);
|
||||
_cancel->entity()->setText(tr::lng_call_cancel());
|
||||
_screencast->setDuration(st::callPanelDuration);
|
||||
_addPeople->setDuration(st::callPanelDuration);
|
||||
_addPeople->entity()->setText(tr::lng_call_add_people());
|
||||
|
||||
initWindow();
|
||||
initWidget();
|
||||
initControls();
|
||||
initConferenceInvite();
|
||||
initLayout();
|
||||
initMediaDeviceToggles();
|
||||
showAndActivate();
|
||||
|
@ -153,6 +168,18 @@ bool Panel::isActive() const {
|
|||
return window()->isActiveWindow() && isVisible();
|
||||
}
|
||||
|
||||
ConferencePanelMigration Panel::migrationInfo() const {
|
||||
return ConferencePanelMigration{ .window = _window };
|
||||
}
|
||||
|
||||
std::shared_ptr<Main::SessionShow> Panel::sessionShow() {
|
||||
return Main::MakeSessionShow(uiShow(), &_user->session());
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::Show> Panel::uiShow() {
|
||||
return _window->uiShow();
|
||||
}
|
||||
|
||||
void Panel::showAndActivate() {
|
||||
if (window()->isHidden()) {
|
||||
window()->show();
|
||||
|
@ -215,20 +242,16 @@ void Panel::initWindow() {
|
|||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
}, lifetime());
|
||||
|
||||
const auto guard = base::make_weak(this);
|
||||
window()->setBodyTitleArea([=](QPoint widgetPoint) {
|
||||
using Flag = Ui::WindowTitleHitTestFlag;
|
||||
if (!widget()->rect().contains(widgetPoint)) {
|
||||
if (!guard
|
||||
|| !widget()->rect().contains(widgetPoint)
|
||||
|| _window->controlsHasHitTest(widgetPoint)) {
|
||||
return Flag::None | Flag(0);
|
||||
}
|
||||
#ifndef Q_OS_MAC
|
||||
using Result = Ui::Platform::HitTestResult;
|
||||
const auto windowPoint = widget()->mapTo(window(), widgetPoint);
|
||||
if (_controls->controls.hitTest(windowPoint) != Result::None) {
|
||||
return Flag::None | Flag(0);
|
||||
}
|
||||
#endif // !Q_OS_MAC
|
||||
const auto buttonWidth = st::callCancel.button.width;
|
||||
const auto buttonsWidth = buttonWidth * 4;
|
||||
const auto inControls = (_fingerprint
|
||||
|
@ -243,12 +266,15 @@ void Panel::initWindow() {
|
|||
if (inControls) {
|
||||
return Flag::None | Flag(0);
|
||||
}
|
||||
const auto shown = _layerBg->topShownLayer();
|
||||
const auto shown = _window->topShownLayer();
|
||||
return (!shown || !shown->geometry().contains(widgetPoint))
|
||||
? (Flag::Move | Flag::Menu | Flag::FullScreen)
|
||||
: Flag::None;
|
||||
});
|
||||
|
||||
_window->maximizeRequests() | rpl::start_with_next([=](bool maximized) {
|
||||
toggleFullScreen(maximized);
|
||||
}, lifetime());
|
||||
// Don't do that, it looks awful :(
|
||||
//#ifdef Q_OS_WIN
|
||||
// // On Windows we replace snap-to-top maximizing with fullscreen.
|
||||
|
@ -282,12 +308,12 @@ void Panel::initWidget() {
|
|||
widget()->paintRequest(
|
||||
) | rpl::start_with_next([=](QRect clip) {
|
||||
paint(clip);
|
||||
}, widget()->lifetime());
|
||||
}, lifetime());
|
||||
|
||||
widget()->sizeValue(
|
||||
) | rpl::skip(1) | rpl::start_with_next([=] {
|
||||
updateControlsGeometry();
|
||||
}, widget()->lifetime());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void Panel::initControls() {
|
||||
|
@ -303,7 +329,7 @@ void Panel::initControls() {
|
|||
return;
|
||||
} else if (!env->desktopCaptureAllowed()) {
|
||||
if (auto box = Group::ScreenSharingPrivacyRequestBox()) {
|
||||
_layerBg->showBox(std::move(box));
|
||||
uiShow()->showBox(std::move(box));
|
||||
}
|
||||
} else if (const auto source = env->uniqueDesktopCaptureSource()) {
|
||||
if (!chooseSourceActiveDeviceId().isEmpty()) {
|
||||
|
@ -318,9 +344,42 @@ void Panel::initControls() {
|
|||
_camera->setClickedCallback([=] {
|
||||
if (!_call) {
|
||||
return;
|
||||
} else {
|
||||
_call->toggleCameraSharing(!_call->isSharingCamera());
|
||||
}
|
||||
_call->toggleCameraSharing(!_call->isSharingCamera());
|
||||
});
|
||||
_addPeople->entity()->setClickedCallback([=] {
|
||||
if (!_call || _call->state() != Call::State::Established) {
|
||||
uiShow()->showToast(tr::lng_call_error_add_not_started(tr::now));
|
||||
return;
|
||||
}
|
||||
const auto call = _call;
|
||||
const auto creating = std::make_shared<bool>();
|
||||
const auto create = [=](std::vector<InviteRequest> users) {
|
||||
if (*creating) {
|
||||
return;
|
||||
}
|
||||
*creating = true;
|
||||
const auto sharingLink = users.empty();
|
||||
Core::App().calls().startOrJoinConferenceCall({
|
||||
.show = sessionShow(),
|
||||
.invite = std::move(users),
|
||||
.sharingLink = sharingLink,
|
||||
.migrating = true,
|
||||
.muted = call->muted(),
|
||||
.videoCapture = (call->isSharingVideo()
|
||||
? call->peekVideoCapture()
|
||||
: nullptr),
|
||||
.videoCaptureScreenId = call->screenSharingDeviceId(),
|
||||
});
|
||||
};
|
||||
const auto invite = crl::guard(call, [=](
|
||||
std::vector<InviteRequest> users) {
|
||||
create(std::move(users));
|
||||
});
|
||||
const auto share = crl::guard(call, [=] {
|
||||
create({});
|
||||
});
|
||||
uiShow()->showBox(Group::PrepareInviteBox(call, invite, share));
|
||||
});
|
||||
|
||||
_updateDurationTimer.setCallback([this] {
|
||||
|
@ -367,6 +426,65 @@ void Panel::initControls() {
|
|||
_screencast->finishAnimating();
|
||||
}
|
||||
|
||||
void Panel::initConferenceInvite() {
|
||||
const auto &participants = _call->conferenceParticipants();
|
||||
const auto count = int(participants.size());
|
||||
if (count < 2) {
|
||||
return;
|
||||
}
|
||||
_conferenceParticipants = base::make_unique_q<Ui::RpWidget>(widget());
|
||||
_conferenceParticipants->show();
|
||||
const auto raw = _conferenceParticipants.get();
|
||||
|
||||
auto peers = std::vector<not_null<PeerData*>>();
|
||||
for (const auto &peer : participants) {
|
||||
if (peer == _user && count > 3) {
|
||||
continue;
|
||||
}
|
||||
peers.push_back(peer);
|
||||
if (peers.size() == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto userpics = CreateUserpicsWithMoreBadge(
|
||||
raw,
|
||||
rpl::single(peers),
|
||||
st::confcallInviteUserpics,
|
||||
peers.size()).release();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
tr::lng_group_call_members(tr::now, lt_count, count),
|
||||
st::confcallInviteParticipants);
|
||||
const auto padding = st::confcallInviteParticipantsPadding;
|
||||
const auto add = padding.bottom();
|
||||
const auto width = add
|
||||
+ userpics->width()
|
||||
+ padding.left()
|
||||
+ label->width()
|
||||
+ padding.right();
|
||||
const auto height = add + userpics->height() + add;
|
||||
|
||||
_status->geometryValue() | rpl::start_with_next([=] {
|
||||
const auto top = _bodyTop + _bodySt->participantsTop;
|
||||
const auto left = (widget()->width() - width) / 2;
|
||||
raw->setGeometry(left, top, width, height);
|
||||
userpics->move(add, add);
|
||||
label->move(add + userpics->width() + padding.left(), padding.top());
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto radius = raw->height() / 2.;
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::confcallInviteUserpicsBg);
|
||||
p.drawRoundedRect(raw->rect(), radius, radius);
|
||||
}, raw->lifetime());
|
||||
}
|
||||
|
||||
void Panel::setIncomingSize(QSize size) {
|
||||
if (_incomingFrameSize == size) {
|
||||
return;
|
||||
|
@ -444,15 +562,24 @@ void Panel::reinitWithCall(Call *call) {
|
|||
updateControlsShown();
|
||||
});
|
||||
if (!_call) {
|
||||
_fingerprint.destroy();
|
||||
_fingerprint = nullptr;
|
||||
_incoming = nullptr;
|
||||
_outgoingVideoBubble = nullptr;
|
||||
_powerSaveBlocker = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
_user = _call->user();
|
||||
|
||||
_call->confereceSupportedValue(
|
||||
) | rpl::start_with_next([=](bool supported) {
|
||||
_conferenceSupported = supported;
|
||||
_addPeople->toggle(_conferenceSupported
|
||||
&& (_call->state() != State::WaitingUserConfirmation),
|
||||
window()->isHidden() ? anim::type::instant : anim::type::normal);
|
||||
|
||||
updateHangupGeometry();
|
||||
}, _callLifetime);
|
||||
|
||||
auto remoteMuted = _call->remoteAudioStateValue(
|
||||
) | rpl::map(rpl::mappers::_1 == Call::RemoteAudioState::Muted);
|
||||
rpl::duplicate(
|
||||
|
@ -461,7 +588,7 @@ void Panel::reinitWithCall(Call *call) {
|
|||
if (muted) {
|
||||
createRemoteAudioMute();
|
||||
} else {
|
||||
_remoteAudioMute.destroy();
|
||||
_remoteAudioMute = nullptr;
|
||||
showRemoteLowBattery();
|
||||
}
|
||||
}, _callLifetime);
|
||||
|
@ -470,7 +597,7 @@ void Panel::reinitWithCall(Call *call) {
|
|||
if (state == Call::RemoteBatteryState::Low) {
|
||||
createRemoteLowBattery();
|
||||
} else {
|
||||
_remoteLowBattery.destroy();
|
||||
_remoteLowBattery = nullptr;
|
||||
}
|
||||
}, _callLifetime);
|
||||
_userpic = std::make_unique<Userpic>(
|
||||
|
@ -483,7 +610,7 @@ void Panel::reinitWithCall(Call *call) {
|
|||
_incoming = std::make_unique<Incoming>(
|
||||
widget(),
|
||||
_call->videoIncoming(),
|
||||
_window.backend());
|
||||
_window->backend());
|
||||
_incoming->widget()->hide();
|
||||
|
||||
_incoming->rp()->shownValue() | rpl::start_with_next([=] {
|
||||
|
@ -605,6 +732,7 @@ void Panel::reinitWithCall(Call *call) {
|
|||
&& state != State::EndedByOtherDevice
|
||||
&& state != State::Failed
|
||||
&& state != State::FailedHangingUp
|
||||
&& state != State::MigrationHangingUp
|
||||
&& state != State::HangingUp) {
|
||||
refreshOutgoingPreviewInBody(state);
|
||||
}
|
||||
|
@ -630,10 +758,7 @@ void Panel::reinitWithCall(Call *call) {
|
|||
}
|
||||
Unexpected("Error type in _call->errors().");
|
||||
}();
|
||||
Ui::Toast::Show(widget(), Ui::Toast::Config{
|
||||
.text = { text },
|
||||
.st = &st::callErrorToast,
|
||||
});
|
||||
uiShow()->showToast(text);
|
||||
}, _callLifetime);
|
||||
|
||||
_name->setText(_user->name());
|
||||
|
@ -647,17 +772,13 @@ void Panel::reinitWithCall(Call *call) {
|
|||
_startVideo->raise();
|
||||
}
|
||||
_mute->raise();
|
||||
|
||||
_powerSaveBlocker = std::make_unique<base::PowerSaveBlocker>(
|
||||
base::PowerSaveBlockType::PreventDisplaySleep,
|
||||
u"Video call is active"_q,
|
||||
window()->windowHandle());
|
||||
_addPeople->raise();
|
||||
|
||||
_incoming->widget()->lower();
|
||||
}
|
||||
|
||||
void Panel::createRemoteAudioMute() {
|
||||
_remoteAudioMute.create(
|
||||
_remoteAudioMute = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
widget(),
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
widget(),
|
||||
|
@ -694,7 +815,7 @@ void Panel::createRemoteAudioMute() {
|
|||
}
|
||||
|
||||
void Panel::createRemoteLowBattery() {
|
||||
_remoteLowBattery.create(
|
||||
_remoteLowBattery = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
widget(),
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
widget(),
|
||||
|
@ -710,7 +831,7 @@ void Panel::createRemoteLowBattery() {
|
|||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_remoteLowBattery.destroy();
|
||||
_remoteLowBattery = nullptr;
|
||||
createRemoteLowBattery();
|
||||
}, _remoteLowBattery->lifetime());
|
||||
|
||||
|
@ -778,11 +899,9 @@ void Panel::initLayout() {
|
|||
}) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
|
||||
_name->setText(_call->user()->name());
|
||||
updateControlsGeometry();
|
||||
}, widget()->lifetime());
|
||||
}, lifetime());
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
_controls->wrap.raise();
|
||||
#endif // !Q_OS_MAC
|
||||
_window->raiseControls();
|
||||
}
|
||||
|
||||
void Panel::showControls() {
|
||||
|
@ -792,6 +911,7 @@ void Panel::showControls() {
|
|||
_decline->setVisible(_decline->toggled());
|
||||
_cancel->setVisible(_cancel->toggled());
|
||||
_screencast->setVisible(_screencast->toggled());
|
||||
_addPeople->setVisible(_addPeople->toggled());
|
||||
|
||||
const auto shown = !_incomingFrameSize.isEmpty();
|
||||
_incoming->widget()->setVisible(shown);
|
||||
|
@ -804,13 +924,16 @@ void Panel::showControls() {
|
|||
showRemoteLowBattery();
|
||||
}
|
||||
|
||||
void Panel::closeBeforeDestroy() {
|
||||
window()->close();
|
||||
void Panel::closeBeforeDestroy(bool windowIsReused) {
|
||||
if (!windowIsReused) {
|
||||
window()->close();
|
||||
}
|
||||
reinitWithCall(nullptr);
|
||||
_lifetime.destroy();
|
||||
}
|
||||
|
||||
rpl::lifetime &Panel::lifetime() {
|
||||
return window()->lifetime();
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
void Panel::initGeometry() {
|
||||
|
@ -948,7 +1071,7 @@ void Panel::updateControlsGeometry() {
|
|||
_controlsShown ? 1. : 0.);
|
||||
if (_fingerprint) {
|
||||
#ifndef Q_OS_MAC
|
||||
const auto controlsGeometry = _controls->controls.geometry();
|
||||
const auto controlsGeometry = _window->controlsGeometry();
|
||||
const auto halfWidth = widget()->width() / 2;
|
||||
const auto minLeft = (controlsGeometry.center().x() < halfWidth)
|
||||
? (controlsGeometry.width() + st::callFingerprintTop)
|
||||
|
@ -994,7 +1117,11 @@ void Panel::updateControlsGeometry() {
|
|||
std::min(
|
||||
bodyPreviewSizeMax.height(),
|
||||
st::callOutgoingPreviewMax.height()));
|
||||
const auto contentHeight = _bodySt->height
|
||||
const auto bodyContentHeight = _bodySt->height
|
||||
+ (_conferenceParticipants
|
||||
? (_bodySt->participantsTop - _bodySt->statusTop)
|
||||
: 0);
|
||||
const auto contentHeight = bodyContentHeight
|
||||
+ (_outgoingPreviewInBody ? bodyPreviewSize.height() : 0);
|
||||
const auto remainingHeight = available - contentHeight;
|
||||
const auto skipHeight = remainingHeight
|
||||
|
@ -1006,7 +1133,7 @@ void Panel::updateControlsGeometry() {
|
|||
widget()->height(),
|
||||
_buttonsTopShown,
|
||||
shown);
|
||||
const auto previewTop = _bodyTop + _bodySt->height + skipHeight;
|
||||
const auto previewTop = _bodyTop + bodyContentHeight + skipHeight;
|
||||
|
||||
_userpic->setGeometry(
|
||||
(widget()->width() - _bodySt->photoSize) / 2,
|
||||
|
@ -1067,8 +1194,11 @@ void Panel::updateOutgoingVideoBubbleGeometry() {
|
|||
}
|
||||
|
||||
void Panel::updateHangupGeometry() {
|
||||
const auto isBusy = (_call
|
||||
&& _call->state() == State::Busy);
|
||||
const auto isWaitingUser = (_call
|
||||
&& _call->state() == State::WaitingUserConfirmation);
|
||||
const auto incomingWaiting = _call && _call->isIncomingWaiting();
|
||||
const auto hangupProgress = isWaitingUser
|
||||
? 0.
|
||||
: _hangupShownProgress.value(_hangupShown ? 1. : 0.);
|
||||
|
@ -1077,11 +1207,9 @@ void Panel::updateHangupGeometry() {
|
|||
// Screencast - Camera - Cancel/Decline - Answer/Hangup/Redial - Mute.
|
||||
const auto buttonWidth = st::callCancel.button.width;
|
||||
const auto cancelWidth = buttonWidth * (1. - hangupProgress);
|
||||
const auto cancelLeft = (isWaitingUser)
|
||||
? ((widget()->width() - buttonWidth) / 2)
|
||||
: (_mute->animating())
|
||||
? ((widget()->width() - cancelWidth) / 2)
|
||||
: ((widget()->width() / 2) - cancelWidth);
|
||||
const auto cancelLeft = (widget()->width() - buttonWidth) / 2
|
||||
- ((isBusy || incomingWaiting) ? buttonWidth : 0)
|
||||
+ ((isWaitingUser || _conferenceSupported) ? 0 : (buttonWidth / 2));
|
||||
|
||||
_cancel->moveToLeft(cancelLeft, _buttonsTop);
|
||||
_decline->moveToLeft(cancelLeft, _buttonsTop);
|
||||
|
@ -1089,6 +1217,7 @@ void Panel::updateHangupGeometry() {
|
|||
_screencast->moveToLeft(_camera->x() - buttonWidth, _buttonsTop);
|
||||
_answerHangupRedial->moveToLeft(cancelLeft + cancelWidth, _buttonsTop);
|
||||
_mute->moveToLeft(_answerHangupRedial->x() + buttonWidth, _buttonsTop);
|
||||
_addPeople->moveToLeft(_mute->x() + buttonWidth, _buttonsTop);
|
||||
if (_startVideo) {
|
||||
_startVideo->moveToLeft(_camera->x(), _camera->y());
|
||||
}
|
||||
|
@ -1118,7 +1247,9 @@ void Panel::paint(QRect clip) {
|
|||
bool Panel::handleClose() const {
|
||||
if (_call) {
|
||||
if (_call->state() == Call::State::WaitingUserConfirmation
|
||||
|| _call->state() == Call::State::Busy) {
|
||||
|| _call->state() == Call::State::Busy
|
||||
|| _call->state() == Call::State::Starting
|
||||
|| _call->state() == Call::State::WaitingIncoming) {
|
||||
_call->hangup();
|
||||
} else {
|
||||
window()->hide();
|
||||
|
@ -1129,11 +1260,15 @@ bool Panel::handleClose() const {
|
|||
}
|
||||
|
||||
not_null<Ui::RpWindow*> Panel::window() const {
|
||||
return _window.window();
|
||||
return _window->window();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> Panel::widget() const {
|
||||
return _window.widget();
|
||||
return _window->widget();
|
||||
}
|
||||
|
||||
not_null<UserData*> Panel::user() const {
|
||||
return _user;
|
||||
}
|
||||
|
||||
void Panel::stateChanged(State state) {
|
||||
|
@ -1141,16 +1276,16 @@ void Panel::stateChanged(State state) {
|
|||
|
||||
updateStatusText(state);
|
||||
|
||||
const auto isBusy = (state == State::Busy);
|
||||
const auto isWaitingUser = (state == State::WaitingUserConfirmation);
|
||||
_window->togglePowerSaveBlocker(!isBusy && !isWaitingUser);
|
||||
|
||||
if ((state != State::HangingUp)
|
||||
&& (state != State::MigrationHangingUp)
|
||||
&& (state != State::Ended)
|
||||
&& (state != State::EndedByOtherDevice)
|
||||
&& (state != State::FailedHangingUp)
|
||||
&& (state != State::Failed)) {
|
||||
const auto isBusy = (state == State::Busy);
|
||||
const auto isWaitingUser = (state == State::WaitingUserConfirmation);
|
||||
if (isBusy) {
|
||||
_powerSaveBlocker = nullptr;
|
||||
}
|
||||
if (_startVideo && !isWaitingUser) {
|
||||
_startVideo = nullptr;
|
||||
} else if (!_startVideo && isWaitingUser) {
|
||||
|
@ -1165,12 +1300,11 @@ void Panel::stateChanged(State state) {
|
|||
}
|
||||
_camera->setVisible(!_startVideo);
|
||||
|
||||
const auto windowHidden = window()->isHidden();
|
||||
const auto toggleButton = [&](auto &&button, bool visible) {
|
||||
button->toggle(
|
||||
visible,
|
||||
window()->isHidden()
|
||||
? anim::type::instant
|
||||
: anim::type::normal);
|
||||
(windowHidden ? anim::type::instant : anim::type::normal));
|
||||
};
|
||||
const auto incomingWaiting = _call->isIncomingWaiting();
|
||||
if (incomingWaiting) {
|
||||
|
@ -1182,6 +1316,7 @@ void Panel::stateChanged(State state) {
|
|||
toggleButton(
|
||||
_screencast,
|
||||
!(isBusy || isWaitingUser || incomingWaiting));
|
||||
toggleButton(_addPeople, !isWaitingUser && _conferenceSupported);
|
||||
const auto hangupShown = !_decline->toggled()
|
||||
&& !_cancel->toggled();
|
||||
if (_hangupShown != hangupShown) {
|
||||
|
@ -1205,7 +1340,7 @@ void Panel::stateChanged(State state) {
|
|||
refreshAnswerHangupRedialLabel();
|
||||
}
|
||||
if (!_call->isKeyShaForFingerprintReady()) {
|
||||
_fingerprint.destroy();
|
||||
_fingerprint = nullptr;
|
||||
} else if (!_fingerprint) {
|
||||
_fingerprint = CreateFingerprintAndSignalBars(widget(), _call);
|
||||
updateControlsGeometry();
|
||||
|
@ -1232,7 +1367,8 @@ void Panel::updateStatusText(State state) {
|
|||
switch (state) {
|
||||
case State::Starting:
|
||||
case State::WaitingInit:
|
||||
case State::WaitingInitAck: return tr::lng_call_status_connecting(tr::now);
|
||||
case State::WaitingInitAck:
|
||||
case State::MigrationHangingUp: return tr::lng_call_status_connecting(tr::now);
|
||||
case State::Established: {
|
||||
if (_call) {
|
||||
auto durationMs = _call->getDurationMs();
|
||||
|
@ -1250,7 +1386,10 @@ void Panel::updateStatusText(State state) {
|
|||
case State::ExchangingKeys: return tr::lng_call_status_exchanging(tr::now);
|
||||
case State::Waiting: return tr::lng_call_status_waiting(tr::now);
|
||||
case State::Requesting: return tr::lng_call_status_requesting(tr::now);
|
||||
case State::WaitingIncoming: return tr::lng_call_status_incoming(tr::now);
|
||||
case State::WaitingIncoming:
|
||||
return (_call->conferenceInvite()
|
||||
? tr::lng_call_status_group_invite(tr::now)
|
||||
: tr::lng_call_status_incoming(tr::now));
|
||||
case State::Ringing: return tr::lng_call_status_ringing(tr::now);
|
||||
case State::Busy: return tr::lng_call_status_busy(tr::now);
|
||||
case State::WaitingUserConfirmation: return tr::lng_call_status_sure(tr::now);
|
||||
|
|
|
@ -7,14 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/gl/gl_window.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class Image;
|
||||
|
@ -27,7 +22,16 @@ namespace Data {
|
|||
class PhotoMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class LayerWidget;
|
||||
enum class LayerOption;
|
||||
using LayerOptions = base::flags<LayerOption>;
|
||||
class IconButton;
|
||||
class CallButton;
|
||||
class LayerManager;
|
||||
|
@ -38,14 +42,17 @@ template <typename Widget>
|
|||
class PaddingWrap;
|
||||
class RpWindow;
|
||||
class PopupMenu;
|
||||
namespace GL {
|
||||
enum class Backend;
|
||||
} // namespace GL
|
||||
namespace Platform {
|
||||
struct SeparateTitleControls;
|
||||
} // namespace Platform
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Toast {
|
||||
class Instance;
|
||||
struct Config;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace Ui::Platform {
|
||||
struct SeparateTitleControls;
|
||||
} // namespace Ui::Platform
|
||||
|
||||
namespace style {
|
||||
struct CallSignalBars;
|
||||
struct CallBodyLayout;
|
||||
|
@ -53,23 +60,32 @@ struct CallBodyLayout;
|
|||
|
||||
namespace Calls {
|
||||
|
||||
class Window;
|
||||
class Userpic;
|
||||
class SignalBars;
|
||||
class VideoBubble;
|
||||
struct DeviceSelection;
|
||||
struct ConferencePanelMigration;
|
||||
|
||||
class Panel final : private Group::Ui::DesktopCapture::ChooseSourceDelegate {
|
||||
class Panel final
|
||||
: public base::has_weak_ptr
|
||||
, private Group::Ui::DesktopCapture::ChooseSourceDelegate {
|
||||
public:
|
||||
Panel(not_null<Call*> call);
|
||||
~Panel();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
[[nodiscard]] not_null<UserData*> user() const;
|
||||
[[nodiscard]] bool isVisible() const;
|
||||
[[nodiscard]] bool isActive() const;
|
||||
|
||||
[[nodiscard]] ConferencePanelMigration migrationInfo() const;
|
||||
|
||||
void showAndActivate();
|
||||
void minimize();
|
||||
void toggleFullScreen();
|
||||
void replaceCall(not_null<Call*> call);
|
||||
void closeBeforeDestroy();
|
||||
void closeBeforeDestroy(bool windowIsReused = false);
|
||||
|
||||
QWidget *chooseSourceParent() override;
|
||||
QString chooseSourceActiveDeviceId() override;
|
||||
|
@ -83,6 +99,11 @@ public:
|
|||
|
||||
[[nodiscard]] rpl::producer<bool> startOutgoingRequests() const;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Main::SessionShow> sessionShow();
|
||||
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
|
@ -96,14 +117,12 @@ private:
|
|||
StartCall,
|
||||
};
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
|
||||
void paint(QRect clip);
|
||||
|
||||
void initWindow();
|
||||
void initWidget();
|
||||
void initControls();
|
||||
void initConferenceInvite();
|
||||
void reinitWithCall(Call *call);
|
||||
void initLayout();
|
||||
void initMediaDeviceToggles();
|
||||
|
@ -142,40 +161,35 @@ private:
|
|||
Call *_call = nullptr;
|
||||
not_null<UserData*> _user;
|
||||
|
||||
Ui::GL::Window _window;
|
||||
const std::unique_ptr<Ui::LayerManager> _layerBg;
|
||||
std::shared_ptr<Window> _window;
|
||||
std::unique_ptr<Incoming> _incoming;
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
|
||||
#endif // !Q_OS_MAC
|
||||
|
||||
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
|
||||
|
||||
QSize _incomingFrameSize;
|
||||
|
||||
rpl::lifetime _callLifetime;
|
||||
|
||||
not_null<const style::CallBodyLayout*> _bodySt;
|
||||
object_ptr<Ui::CallButton> _answerHangupRedial;
|
||||
object_ptr<Ui::FadeWrap<Ui::CallButton>> _decline;
|
||||
object_ptr<Ui::FadeWrap<Ui::CallButton>> _cancel;
|
||||
base::unique_qptr<Ui::CallButton> _answerHangupRedial;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _decline;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _cancel;
|
||||
bool _hangupShown = false;
|
||||
bool _conferenceSupported = false;
|
||||
bool _outgoingPreviewInBody = false;
|
||||
std::optional<AnswerHangupRedialState> _answerHangupRedialState;
|
||||
Ui::Animations::Simple _hangupShownProgress;
|
||||
object_ptr<Ui::FadeWrap<Ui::CallButton>> _screencast;
|
||||
object_ptr<Ui::CallButton> _camera;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _screencast;
|
||||
base::unique_qptr<Ui::CallButton> _camera;
|
||||
Ui::CallButton *_cameraDeviceToggle = nullptr;
|
||||
base::unique_qptr<Ui::CallButton> _startVideo;
|
||||
object_ptr<Ui::FadeWrap<Ui::CallButton>> _mute;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _mute;
|
||||
Ui::CallButton *_audioDeviceToggle = nullptr;
|
||||
object_ptr<Ui::FlatLabel> _name;
|
||||
object_ptr<Ui::FlatLabel> _status;
|
||||
object_ptr<Ui::RpWidget> _fingerprint = { nullptr };
|
||||
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute = { nullptr };
|
||||
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteLowBattery
|
||||
= { nullptr };
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::CallButton>> _addPeople;
|
||||
base::unique_qptr<Ui::FlatLabel> _name;
|
||||
base::unique_qptr<Ui::FlatLabel> _status;
|
||||
base::unique_qptr<Ui::RpWidget> _conferenceParticipants;
|
||||
base::unique_qptr<Ui::RpWidget> _fingerprint;
|
||||
base::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute;
|
||||
base::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteLowBattery;
|
||||
std::unique_ptr<Userpic> _userpic;
|
||||
std::unique_ptr<VideoBubble> _outgoingVideoBubble;
|
||||
QPixmap _bottomShadow;
|
||||
|
@ -200,6 +214,8 @@ private:
|
|||
|
||||
rpl::event_stream<bool> _startOutgoingRequests;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -228,14 +228,14 @@ private:
|
|||
|
||||
TopBar::TopBar(
|
||||
QWidget *parent,
|
||||
const base::weak_ptr<Call> &call,
|
||||
Call *call,
|
||||
std::shared_ptr<Ui::Show> show)
|
||||
: TopBar(parent, show, call, nullptr) {
|
||||
}
|
||||
|
||||
TopBar::TopBar(
|
||||
QWidget *parent,
|
||||
const base::weak_ptr<GroupCall> &call,
|
||||
GroupCall *call,
|
||||
std::shared_ptr<Ui::Show> show)
|
||||
: TopBar(parent, show, nullptr, call) {
|
||||
}
|
||||
|
@ -243,8 +243,8 @@ TopBar::TopBar(
|
|||
TopBar::TopBar(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
const base::weak_ptr<Call> &call,
|
||||
const base::weak_ptr<GroupCall> &groupCall)
|
||||
Call *call,
|
||||
GroupCall *groupCall)
|
||||
: RpWidget(parent)
|
||||
, _call(call)
|
||||
, _groupCall(groupCall)
|
||||
|
@ -424,7 +424,7 @@ void TopBar::initControls() {
|
|||
if (const auto call = _call.get()) {
|
||||
call->hangup();
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
if (!group->peer()->canManageGroupCall()) {
|
||||
if (!group->canManage()) {
|
||||
group->hangup();
|
||||
} else {
|
||||
_show->showBox(
|
||||
|
|
|
@ -42,11 +42,11 @@ class TopBar : public Ui::RpWidget {
|
|||
public:
|
||||
TopBar(
|
||||
QWidget *parent,
|
||||
const base::weak_ptr<Call> &call,
|
||||
Call *call,
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
TopBar(
|
||||
QWidget *parent,
|
||||
const base::weak_ptr<GroupCall> &call,
|
||||
GroupCall *call,
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
~TopBar();
|
||||
|
||||
|
@ -64,8 +64,8 @@ private:
|
|||
TopBar(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
const base::weak_ptr<Call> &call,
|
||||
const base::weak_ptr<GroupCall> &groupCall);
|
||||
Call *call,
|
||||
GroupCall *groupCall);
|
||||
|
||||
void initControls();
|
||||
void setupInitialBrush();
|
||||
|
|
250
Telegram/SourceFiles/calls/calls_window.cpp
Normal 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
|
112
Telegram/SourceFiles/calls/calls_window.h
Normal 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
|
|
@ -23,6 +23,7 @@ struct GroupLevelsUpdate;
|
|||
struct GroupNetworkState;
|
||||
struct GroupParticipantDescription;
|
||||
class VideoCaptureInterface;
|
||||
enum class VideoCodecName;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace base {
|
||||
|
@ -42,6 +43,11 @@ struct GroupCallParticipant;
|
|||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace TdE2E {
|
||||
class Call;
|
||||
class EncryptDecrypt;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
|
@ -49,12 +55,17 @@ struct MuteRequest;
|
|||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
struct JoinInfo;
|
||||
struct ConferenceInfo;
|
||||
struct RejoinEvent;
|
||||
struct RtmpInfo;
|
||||
enum class VideoQuality;
|
||||
enum class Error;
|
||||
} // namespace Group
|
||||
|
||||
struct InviteRequest;
|
||||
struct InviteResult;
|
||||
struct StartConferenceInfo;
|
||||
|
||||
enum class MuteState {
|
||||
Active,
|
||||
PushToTalk,
|
||||
|
@ -217,6 +228,7 @@ public:
|
|||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo info,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
GroupCall(not_null<Delegate*> delegate, StartConferenceInfo info);
|
||||
~GroupCall();
|
||||
|
||||
[[nodiscard]] CallId id() const {
|
||||
|
@ -237,6 +249,7 @@ public:
|
|||
}
|
||||
[[nodiscard]] bool scheduleStartSubscribed() const;
|
||||
[[nodiscard]] bool rtmp() const;
|
||||
[[nodiscard]] bool conference() const;
|
||||
[[nodiscard]] bool listenersHidden() const;
|
||||
[[nodiscard]] bool emptyRtmp() const;
|
||||
[[nodiscard]] rpl::producer<bool> emptyRtmpValue() const;
|
||||
|
@ -247,14 +260,19 @@ public:
|
|||
void setRtmpInfo(const Group::RtmpInfo &value);
|
||||
|
||||
[[nodiscard]] Data::GroupCall *lookupReal() const;
|
||||
[[nodiscard]] std::shared_ptr<Data::GroupCall> conferenceCall() const;
|
||||
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
|
||||
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
|
||||
|
||||
void applyInputCall(const MTPInputGroupCall &inputCall);
|
||||
void startConference();
|
||||
void start(TimeId scheduleDate, bool rtmp);
|
||||
void hangup();
|
||||
void discard();
|
||||
void rejoinAs(Group::JoinInfo info);
|
||||
void rejoinWithHash(const QString &hash);
|
||||
void join(const MTPInputGroupCall &inputCall);
|
||||
void initialJoin();
|
||||
void initialJoinRequested();
|
||||
void handleUpdate(const MTPUpdate &update);
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
|
||||
void handlePossibleCreateOrJoinResponse(
|
||||
|
@ -271,10 +289,21 @@ public:
|
|||
void startScheduledNow();
|
||||
void toggleScheduleStartSubscribed(bool subscribed);
|
||||
void setNoiseSuppression(bool enabled);
|
||||
void removeConferenceParticipants(
|
||||
const base::flat_set<UserId> userIds,
|
||||
bool removingStale = false);
|
||||
|
||||
bool emitShareScreenError();
|
||||
bool emitShareCameraError();
|
||||
|
||||
void joinDone(
|
||||
int64 serverTimeMs,
|
||||
const MTPUpdates &result,
|
||||
MuteState wasMuteState,
|
||||
bool wasVideoStopped,
|
||||
bool justCreated = false);
|
||||
void joinFail(const QString &error);
|
||||
|
||||
[[nodiscard]] rpl::producer<Group::Error> errors() const {
|
||||
return _errors.events();
|
||||
}
|
||||
|
@ -404,8 +433,10 @@ public:
|
|||
|
||||
void toggleMute(const Group::MuteRequest &data);
|
||||
void changeVolume(const Group::VolumeRequest &data);
|
||||
std::variant<int, not_null<UserData*>> inviteUsers(
|
||||
const std::vector<not_null<UserData*>> &users);
|
||||
|
||||
void inviteUsers(
|
||||
const std::vector<InviteRequest> &requests,
|
||||
Fn<void(InviteResult)> done);
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
|
||||
void applyGlobalShortcutChanges();
|
||||
|
@ -426,6 +457,7 @@ private:
|
|||
struct SinkPointer;
|
||||
|
||||
static constexpr uint32 kDisabledSsrc = uint32(-1);
|
||||
static constexpr int kSubChainsCount = 2;
|
||||
|
||||
struct LoadingPart {
|
||||
std::shared_ptr<LoadPartTask> task;
|
||||
|
@ -454,9 +486,14 @@ private:
|
|||
Joining,
|
||||
Leaving,
|
||||
};
|
||||
struct JoinPayload {
|
||||
uint32 ssrc = 0;
|
||||
QByteArray json;
|
||||
};
|
||||
struct JoinState {
|
||||
uint32 ssrc = 0;
|
||||
JoinAction action = JoinAction::None;
|
||||
JoinPayload payload;
|
||||
bool nextActionPending = false;
|
||||
|
||||
void finish(uint32 updatedSsrc = 0) {
|
||||
|
@ -464,11 +501,26 @@ private:
|
|||
ssrc = updatedSsrc;
|
||||
}
|
||||
};
|
||||
struct SubChainPending {
|
||||
QVector<MTPbytes> blocks;
|
||||
int next = 0;
|
||||
};
|
||||
struct SubChainState {
|
||||
std::vector<SubChainPending> pending;
|
||||
mtpRequestId requestId = 0;
|
||||
bool inShortPoll = false;
|
||||
};
|
||||
|
||||
friend inline constexpr bool is_flag_type(SendUpdateType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GroupCall(
|
||||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo join,
|
||||
StartConferenceInfo conference,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
|
||||
void broadcastPartStart(std::shared_ptr<LoadPartTask> task);
|
||||
void broadcastPartCancel(not_null<LoadPartTask*> task);
|
||||
void mediaChannelDescriptionsStart(
|
||||
|
@ -490,6 +542,13 @@ private:
|
|||
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
|
||||
void handleUpdate(const MTPDupdateGroupCall &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallChainBlocks &data);
|
||||
void applySubChainUpdate(
|
||||
int subchain,
|
||||
const QVector<MTPbytes> &blocks,
|
||||
int next);
|
||||
[[nodiscard]] auto lookupVideoCodecPreferences() const
|
||||
-> std::vector<tgcalls::VideoCodecName>;
|
||||
bool tryCreateController();
|
||||
void destroyController();
|
||||
bool tryCreateScreencast();
|
||||
|
@ -508,6 +567,7 @@ private:
|
|||
const std::optional<Data::GroupCallParticipant> &was,
|
||||
const Data::GroupCallParticipant &now);
|
||||
void applyMeInCallLocally();
|
||||
void startRejoin();
|
||||
void rejoin();
|
||||
void leave();
|
||||
void rejoin(not_null<PeerData*> as);
|
||||
|
@ -518,6 +578,10 @@ private:
|
|||
void rejoinPresentation();
|
||||
void leavePresentation();
|
||||
void checkNextJoinAction();
|
||||
void sendJoinRequest();
|
||||
void refreshLastBlockAndJoin();
|
||||
void requestSubchainBlocks(int subchain, int height);
|
||||
void sendOutboundBlock(QByteArray block);
|
||||
|
||||
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
|
||||
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
|
@ -558,6 +622,9 @@ private:
|
|||
|
||||
void setupMediaDevices();
|
||||
void setupOutgoingVideo();
|
||||
void initConferenceE2E();
|
||||
void setupConferenceCall();
|
||||
void trackParticipantsWithAccess();
|
||||
void setScreenEndpoint(std::string endpoint);
|
||||
void setCameraEndpoint(std::string endpoint);
|
||||
void addVideoOutput(const std::string &endpoint, SinkPointer sink);
|
||||
|
@ -570,11 +637,25 @@ private:
|
|||
void markTrackPaused(const VideoEndpoint &endpoint, bool paused);
|
||||
void markTrackShown(const VideoEndpoint &endpoint, bool shown);
|
||||
|
||||
void processConferenceStart(StartConferenceInfo conference);
|
||||
void inviteToConference(
|
||||
InviteRequest request,
|
||||
Fn<not_null<InviteResult*>()> resultAddress,
|
||||
Fn<void()> finishRequest);
|
||||
|
||||
[[nodiscard]] int activeVideoSendersCount() const;
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
[[nodiscard]] MTPInputGroupCall inputCallSafe() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
std::shared_ptr<Data::GroupCall> _conferenceCall;
|
||||
std::unique_ptr<TdE2E::Call> _e2e;
|
||||
std::shared_ptr<TdE2E::EncryptDecrypt> _e2eEncryptDecrypt;
|
||||
rpl::variable<QByteArray> _emojiHash;
|
||||
QByteArray _pendingOutboundBlock;
|
||||
std::shared_ptr<StartConferenceInfo> _startConferenceInfo;
|
||||
|
||||
not_null<PeerData*> _peer; // Can change in legacy group migration.
|
||||
rpl::event_stream<PeerData*> _peerStream;
|
||||
not_null<History*> _history; // Can change in legacy group migration.
|
||||
|
@ -583,6 +664,7 @@ private:
|
|||
rpl::variable<State> _state = State::Creating;
|
||||
base::flat_set<uint32> _unresolvedSsrcs;
|
||||
rpl::event_stream<Error> _errors;
|
||||
std::vector<Fn<void()>> _rejoinedCallbacks;
|
||||
bool _recordingStoppedByMe = false;
|
||||
bool _requestedVideoChannelsUpdateScheduled = false;
|
||||
|
||||
|
@ -601,6 +683,8 @@ private:
|
|||
rpl::variable<not_null<PeerData*>> _joinAs;
|
||||
std::vector<not_null<PeerData*>> _possibleJoinAs;
|
||||
QString _joinHash;
|
||||
QString _conferenceLinkSlug;
|
||||
MsgId _conferenceJoinMessageId;
|
||||
int64 _serverTimeMs = 0;
|
||||
crl::time _serverTimeMsGotAt = 0;
|
||||
|
||||
|
@ -688,8 +772,13 @@ private:
|
|||
bool _reloadedStaleCall = false;
|
||||
int _rtmpVolume = 0;
|
||||
|
||||
SubChainState _subchains[kSubChainsCount];
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] TextWithEntities ComposeInviteResultToast(
|
||||
const InviteResult &result);
|
||||
|
||||
} // namespace Calls
|
||||
|
|
|
@ -7,13 +7,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "calls/group/calls_group_common.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/random.h"
|
||||
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
|
||||
#include "boxes/share_box.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "core/application.h"
|
||||
#include "core/local_url_handlers.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_session.h"
|
||||
#include "info/bot/starref/info_bot_starref_common.h"
|
||||
#include "tde2e/tde2e_api.h"
|
||||
#include "tde2e/tde2e_integration.h"
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_media_view.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
|
@ -50,4 +77,422 @@ object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox() {
|
|||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeJoinCallLogo(not_null<QWidget*> parent) {
|
||||
const auto logoSize = st::confcallJoinLogo.size();
|
||||
const auto logoOuter = logoSize.grownBy(st::confcallJoinLogoPadding);
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto logo = result.data();
|
||||
logo->resize(logo->width(), logoOuter.height());
|
||||
logo->paintRequest() | rpl::start_with_next([=] {
|
||||
if (logo->width() < logoOuter.width()) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(logo);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto x = (logo->width() - logoOuter.width()) / 2;
|
||||
const auto outer = QRect(QPoint(x, 0), logoOuter);
|
||||
p.setBrush(st::windowBgActive);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(outer);
|
||||
st::confcallJoinLogo.paintInCenter(p, outer);
|
||||
}, logo->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
void ConferenceCallJoinConfirm(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
UserData *maybeInviter,
|
||||
Fn<void(Fn<void()> close)> join) {
|
||||
box->setStyle(st::confcallJoinBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
box->addRow(
|
||||
MakeJoinCallLogo(box),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
|
||||
box,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_join_title(),
|
||||
st::boxTitle)),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding);
|
||||
const auto wrapName = [&](not_null<PeerData*> peer) {
|
||||
return rpl::single(Ui::Text::Bold(peer->shortName()));
|
||||
};
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
(maybeInviter
|
||||
? tr::lng_confcall_join_text_inviter(
|
||||
lt_user,
|
||||
wrapName(maybeInviter),
|
||||
Ui::Text::RichLangValue)
|
||||
: tr::lng_confcall_join_text(Ui::Text::RichLangValue)),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
const auto &participants = call->participants();
|
||||
const auto known = int(participants.size());
|
||||
if (known) {
|
||||
const auto sep = box->addRow(
|
||||
object_ptr<Ui::RpWidget>(box),
|
||||
st::boxRowPadding + st::confcallJoinSepPadding);
|
||||
sep->resize(sep->width(), st::normalFont->height);
|
||||
sep->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(sep);
|
||||
const auto line = st::lineWidth;
|
||||
const auto top = st::confcallLinkFooterOrLineTop;
|
||||
const auto fg = st::windowSubTextFg->b;
|
||||
p.setOpacity(0.2);
|
||||
p.fillRect(0, top, sep->width(), line, fg);
|
||||
}, sep->lifetime());
|
||||
|
||||
auto peers = std::vector<not_null<PeerData*>>();
|
||||
for (const auto &participant : participants) {
|
||||
peers.push_back(participant.peer);
|
||||
if (peers.size() == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
box->addRow(
|
||||
CreateUserpicsWithMoreBadge(
|
||||
box,
|
||||
rpl::single(peers),
|
||||
st::confcallJoinUserpics,
|
||||
known),
|
||||
st::boxRowPadding + st::confcallJoinUserpicsPadding);
|
||||
|
||||
const auto wrapByIndex = [&](int index) {
|
||||
Expects(index >= 0 && index < known);
|
||||
|
||||
return wrapName(participants[index].peer);
|
||||
};
|
||||
auto text = (known == 1)
|
||||
? tr::lng_confcall_already_joined_one(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
Ui::Text::RichLangValue)
|
||||
: (known == 2)
|
||||
? tr::lng_confcall_already_joined_two(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
Ui::Text::RichLangValue)
|
||||
: (known == 3)
|
||||
? tr::lng_confcall_already_joined_three(
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
lt_third,
|
||||
wrapByIndex(2),
|
||||
Ui::Text::RichLangValue)
|
||||
: tr::lng_confcall_already_joined_many(
|
||||
lt_count,
|
||||
rpl::single(1. * (std::max(known, call->fullCount()) - 2)),
|
||||
lt_user,
|
||||
wrapByIndex(0),
|
||||
lt_other,
|
||||
wrapByIndex(1),
|
||||
Ui::Text::RichLangValue);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(text),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding
|
||||
)->setTryMakeSimilarLines(true);
|
||||
}
|
||||
const auto joinAndClose = [=] {
|
||||
join([weak = Ui::MakeWeak(box)] {
|
||||
if (const auto strong = weak.data()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
});
|
||||
};
|
||||
Info::BotStarRef::AddFullWidthButton(
|
||||
box,
|
||||
tr::lng_confcall_join_button(),
|
||||
joinAndClose,
|
||||
&st::confcallLinkButton);
|
||||
}
|
||||
|
||||
ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle() {
|
||||
return {
|
||||
.box = &st::groupCallLinkBox,
|
||||
.menuToggle = &st::groupCallLinkMenu,
|
||||
.menu = &st::groupCallPopupMenuWithIcons,
|
||||
.close = &st::storiesStealthBoxClose,
|
||||
.centerLabel = &st::groupCallLinkCenteredText,
|
||||
.linkPreview = &st::groupCallLinkPreview,
|
||||
.contextRevoke = &st::mediaMenuIconRemove,
|
||||
.shareBox = std::make_shared<ShareBoxStyleOverrides>(
|
||||
DarkShareBoxStyle()),
|
||||
};
|
||||
}
|
||||
|
||||
void ShowConferenceCallLinkBox(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
const ConferenceCallLinkArgs &args) {
|
||||
const auto st = args.st;
|
||||
const auto initial = args.initial;
|
||||
const auto link = call->conferenceInviteLink();
|
||||
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
struct State {
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
bool resetting = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
|
||||
box->setStyle(st.box
|
||||
? *st.box
|
||||
: initial
|
||||
? st::confcallLinkBoxInitial
|
||||
: st::confcallLinkBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setNoContentMargin(true);
|
||||
const auto close = box->addTopButton(
|
||||
st.close ? *st.close : st::boxTitleClose,
|
||||
[=] { box->closeBox(); });
|
||||
|
||||
if (!args.initial && call->canManage()) {
|
||||
const auto toggle = Ui::CreateChild<Ui::IconButton>(
|
||||
close->parentWidget(),
|
||||
st.menuToggle ? *st.menuToggle : st::confcallLinkMenu);
|
||||
const auto handler = [=] {
|
||||
if (state->resetting) {
|
||||
return;
|
||||
}
|
||||
state->resetting = true;
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
const auto weak = Ui::MakeWeak(box);
|
||||
call->session().api().request(
|
||||
MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_reset_invite_hash),
|
||||
call->input(),
|
||||
MTPbool()) // join_muted
|
||||
).done([=](const MTPUpdates &result) {
|
||||
call->session().api().applyUpdates(result);
|
||||
ShowConferenceCallLinkBox(show, call, args);
|
||||
if (const auto strong = weak.data()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
show->showToast({
|
||||
.title = tr::lng_confcall_link_revoked_title(
|
||||
tr::now),
|
||||
.text = {
|
||||
tr::lng_confcall_link_revoked_text(tr::now),
|
||||
},
|
||||
});
|
||||
}).send();
|
||||
};
|
||||
toggle->setClickedCallback([=] {
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
toggle,
|
||||
st.menu ? *st.menu : st::popupMenuWithIcons);
|
||||
state->menu->addAction(
|
||||
tr::lng_confcall_link_revoke(tr::now),
|
||||
handler,
|
||||
(st.contextRevoke
|
||||
? st.contextRevoke
|
||||
: &st::menuIconRemove));
|
||||
state->menu->popup(QCursor::pos());
|
||||
});
|
||||
|
||||
close->geometryValue(
|
||||
) | rpl::start_with_next([=](QRect geometry) {
|
||||
toggle->moveToLeft(
|
||||
geometry.x() - toggle->width(),
|
||||
geometry.y());
|
||||
}, close->lifetime());
|
||||
}
|
||||
|
||||
box->addRow(
|
||||
Info::BotStarRef::CreateLinkHeaderIcon(box, &call->session()),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
box->addRow(
|
||||
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
|
||||
box,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_link_title(),
|
||||
st.box ? st.box->title : st::boxTitle)),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_confcall_link_about(),
|
||||
(st.centerLabel
|
||||
? *st.centerLabel
|
||||
: st::confcallLinkCenteredText)),
|
||||
st::boxRowPadding
|
||||
)->setTryMakeSimilarLines(true);
|
||||
|
||||
Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2);
|
||||
const auto preview = box->addRow(
|
||||
Info::BotStarRef::MakeLinkLabel(box, link, st.linkPreview));
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
|
||||
const auto copyCallback = [=] {
|
||||
QApplication::clipboard()->setText(link);
|
||||
show->showToast(tr::lng_username_copied(tr::now));
|
||||
};
|
||||
const auto shareCallback = [=] {
|
||||
FastShareLink(
|
||||
show,
|
||||
link,
|
||||
st.shareBox ? *st.shareBox : ShareBoxStyleOverrides());
|
||||
};
|
||||
preview->setClickedCallback(copyCallback);
|
||||
const auto share = box->addButton(
|
||||
tr::lng_group_invite_share(),
|
||||
shareCallback,
|
||||
st::confcallLinkShareButton);
|
||||
const auto copy = box->addButton(
|
||||
tr::lng_group_invite_copy(),
|
||||
copyCallback,
|
||||
st::confcallLinkCopyButton);
|
||||
|
||||
rpl::combine(
|
||||
box->widthValue(),
|
||||
copy->widthValue(),
|
||||
share->widthValue()
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto width = st::boxWideWidth;
|
||||
const auto padding = st::confcallLinkBox.buttonPadding;
|
||||
const auto available = width - 2 * padding.right();
|
||||
const auto buttonWidth = (available - padding.left()) / 2;
|
||||
copy->resizeToWidth(buttonWidth);
|
||||
share->resizeToWidth(buttonWidth);
|
||||
copy->moveToLeft(padding.right(), copy->y(), width);
|
||||
share->moveToRight(padding.right(), share->y(), width);
|
||||
}, box->lifetime());
|
||||
|
||||
if (!initial) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sep = Ui::CreateChild<Ui::FlatLabel>(
|
||||
copy->parentWidget(),
|
||||
tr::lng_confcall_link_or(),
|
||||
st::confcallLinkFooterOr);
|
||||
sep->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(sep);
|
||||
const auto text = sep->textMaxWidth();
|
||||
const auto white = (sep->width() - 2 * text) / 2;
|
||||
const auto line = st::lineWidth;
|
||||
const auto top = st::confcallLinkFooterOrLineTop;
|
||||
const auto fg = st::windowSubTextFg->b;
|
||||
p.setOpacity(0.4);
|
||||
p.fillRect(0, top, white, line, fg);
|
||||
p.fillRect(sep->width() - white, top, white, line, fg);
|
||||
}, sep->lifetime());
|
||||
|
||||
const auto footer = Ui::CreateChild<Ui::FlatLabel>(
|
||||
copy->parentWidget(),
|
||||
tr::lng_confcall_link_join(
|
||||
lt_link,
|
||||
tr::lng_confcall_link_join_link(
|
||||
lt_arrow,
|
||||
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
|
||||
[](QString v) { return Ui::Text::Link(v); }),
|
||||
Ui::Text::WithEntities),
|
||||
(st.centerLabel
|
||||
? *st.centerLabel
|
||||
: st::confcallLinkCenteredText));
|
||||
footer->setTryMakeSimilarLines(true);
|
||||
footer->setClickHandlerFilter([=](const auto &...) {
|
||||
if (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) {
|
||||
Core::App().calls().startOrJoinConferenceCall({
|
||||
.call = call,
|
||||
.linkSlug = std::move(slug),
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
copy->geometryValue() | rpl::start_with_next([=](QRect geometry) {
|
||||
const auto width = st::boxWideWidth
|
||||
- st::boxRowPadding.left()
|
||||
- st::boxRowPadding.right();
|
||||
footer->resizeToWidth(width);
|
||||
const auto top = geometry.y()
|
||||
+ geometry.height()
|
||||
+ st::confcallLinkFooterOrTop;
|
||||
sep->resizeToWidth(width / 2);
|
||||
sep->move(
|
||||
st::boxRowPadding.left() + (width - sep->width()) / 2,
|
||||
top);
|
||||
footer->moveToLeft(
|
||||
st::boxRowPadding.left(),
|
||||
top + sep->height() + st::confcallLinkFooterOrSkip);
|
||||
}, footer->lifetime());
|
||||
}));
|
||||
}
|
||||
|
||||
void MakeConferenceCall(ConferenceFactoryArgs &&args) {
|
||||
const auto show = std::move(args.show);
|
||||
const auto finished = std::move(args.finished);
|
||||
const auto session = &show->session();
|
||||
const auto fail = [=](QString error) {
|
||||
show->showToast(error);
|
||||
if (const auto onstack = finished) {
|
||||
onstack(false);
|
||||
}
|
||||
};
|
||||
session->api().request(MTPphone_CreateConferenceCall(
|
||||
MTP_flags(0),
|
||||
MTP_int(base::RandomValue<int32>()),
|
||||
MTPint256(), // public_key
|
||||
MTPbytes(), // block
|
||||
MTPDataJSON() // params
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
auto call = session->data().sharedConferenceCallFind(result);
|
||||
if (!call) {
|
||||
fail(u"Call not found!"_q);
|
||||
return;
|
||||
}
|
||||
session->api().applyUpdates(result);
|
||||
|
||||
const auto link = call ? call->conferenceInviteLink() : QString();
|
||||
if (link.isEmpty()) {
|
||||
fail(u"Call link not found!"_q);
|
||||
return;
|
||||
}
|
||||
Calls::Group::ShowConferenceCallLinkBox(
|
||||
show,
|
||||
call,
|
||||
{ .initial = true });
|
||||
if (const auto onstack = finished) {
|
||||
finished(true);
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
fail(error.type());
|
||||
}).send();
|
||||
}
|
||||
|
||||
QString ExtractConferenceSlug(const QString &link) {
|
||||
const auto local = Core::TryConvertUrlToLocal(link);
|
||||
const auto parts1 = QStringView(local).split('#');
|
||||
if (!parts1.isEmpty()) {
|
||||
const auto parts2 = parts1.front().split('&');
|
||||
if (!parts2.isEmpty()) {
|
||||
const auto parts3 = parts2.front().split(u"slug="_q);
|
||||
if (parts3.size() > 1) {
|
||||
return parts3.back().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
|
|
|
@ -8,13 +8,81 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
class UserData;
|
||||
struct ShareBoxStyleOverrides;
|
||||
|
||||
namespace style {
|
||||
struct Box;
|
||||
struct FlatLabel;
|
||||
struct IconButton;
|
||||
struct InputField;
|
||||
struct PopupMenu;
|
||||
} // namespace style
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class RpWidget;
|
||||
class GenericBox;
|
||||
} // namespace Ui
|
||||
|
||||
namespace TdE2E {
|
||||
class Call;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace tgcalls {
|
||||
class VideoCaptureInterface;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Window;
|
||||
|
||||
struct InviteRequest {
|
||||
not_null<UserData*> user;
|
||||
bool video = false;
|
||||
};
|
||||
|
||||
struct InviteResult {
|
||||
std::vector<not_null<UserData*>> invited;
|
||||
std::vector<not_null<UserData*>> alreadyIn;
|
||||
std::vector<not_null<UserData*>> privacyRestricted;
|
||||
std::vector<not_null<UserData*>> kicked;
|
||||
std::vector<not_null<UserData*>> failed;
|
||||
};
|
||||
|
||||
struct StartConferenceInfo {
|
||||
std::shared_ptr<Main::SessionShow> show;
|
||||
std::shared_ptr<Data::GroupCall> call;
|
||||
QString linkSlug;
|
||||
MsgId joinMessageId;
|
||||
std::vector<InviteRequest> invite;
|
||||
bool sharingLink = false;
|
||||
bool migrating = false;
|
||||
bool muted = false;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> videoCapture;
|
||||
QString videoCaptureScreenId;
|
||||
};
|
||||
|
||||
struct ConferencePanelMigration {
|
||||
std::shared_ptr<Window> window;
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
constexpr auto kDefaultVolume = 10000;
|
||||
|
@ -93,4 +161,44 @@ using StickedTooltips = base::flags<StickedTooltip>;
|
|||
|
||||
[[nodiscard]] object_ptr<Ui::GenericBox> ScreenSharingPrivacyRequestBox();
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeJoinCallLogo(
|
||||
not_null<QWidget*> parent);
|
||||
|
||||
void ConferenceCallJoinConfirm(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
UserData *maybeInviter,
|
||||
Fn<void(Fn<void()> close)> join);
|
||||
|
||||
struct ConferenceCallLinkStyleOverrides {
|
||||
const style::Box *box = nullptr;
|
||||
const style::IconButton *menuToggle = nullptr;
|
||||
const style::PopupMenu *menu = nullptr;
|
||||
const style::IconButton *close = nullptr;
|
||||
const style::FlatLabel *centerLabel = nullptr;
|
||||
const style::InputField *linkPreview = nullptr;
|
||||
const style::icon *contextRevoke = nullptr;
|
||||
std::shared_ptr<ShareBoxStyleOverrides> shareBox;
|
||||
};
|
||||
[[nodiscard]] ConferenceCallLinkStyleOverrides DarkConferenceCallLinkStyle();
|
||||
|
||||
struct ConferenceCallLinkArgs {
|
||||
ConferenceCallLinkStyleOverrides st;
|
||||
bool initial = false;
|
||||
};
|
||||
void ShowConferenceCallLinkBox(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
const ConferenceCallLinkArgs &args);
|
||||
|
||||
struct ConferenceFactoryArgs {
|
||||
std::shared_ptr<Main::SessionShow> show;
|
||||
Fn<void(bool)> finished;
|
||||
bool joining = false;
|
||||
StartConferenceInfo info;
|
||||
};
|
||||
void MakeConferenceCall(ConferenceFactoryArgs &&args);
|
||||
|
||||
[[nodiscard]] QString ExtractConferenceSlug(const QString &link);
|
||||
|
||||
} // namespace Calls::Group
|
||||
|
|
|
@ -9,20 +9,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#include "api/api_chat_participants.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "core/application.h"
|
||||
#include "boxes/peer_lists_box.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "main/session/session_show.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h" // membersMarginTop
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_dialogs.h" // searchedBarHeight
|
||||
#include "styles/style_layers.h" // boxWideWidth
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
@ -56,6 +71,627 @@ namespace {
|
|||
return result;
|
||||
}
|
||||
|
||||
struct ConfInviteStyles {
|
||||
const style::IconButton *video = nullptr;
|
||||
const style::icon *videoActive = nullptr;
|
||||
const style::IconButton *audio = nullptr;
|
||||
const style::icon *audioActive = nullptr;
|
||||
const style::SettingsButton *inviteViaLink = nullptr;
|
||||
const style::icon *inviteViaLinkIcon = nullptr;
|
||||
};
|
||||
|
||||
class ConfInviteRow final : public PeerListRow {
|
||||
public:
|
||||
ConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st);
|
||||
|
||||
void setAlreadyIn(bool alreadyIn);
|
||||
void setVideo(bool video);
|
||||
|
||||
int elementsCount() const override;
|
||||
QRect elementGeometry(int element, int outerWidth) const override;
|
||||
bool elementDisabled(int element) const override;
|
||||
bool elementOnlySelect(int element) const override;
|
||||
void elementAddRipple(
|
||||
int element,
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) override;
|
||||
void elementsStopLastRipple() override;
|
||||
void elementsPaint(
|
||||
Painter &p,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
int selectedElement) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] const style::IconButton &buttonSt(int element) const;
|
||||
|
||||
const ConfInviteStyles &_st;
|
||||
std::unique_ptr<Ui::RippleAnimation> _videoRipple;
|
||||
std::unique_ptr<Ui::RippleAnimation> _audioRipple;
|
||||
bool _alreadyIn = false;
|
||||
bool _video = false;
|
||||
|
||||
};
|
||||
|
||||
struct PrioritizedSelector {
|
||||
object_ptr<Ui::RpWidget> content = { nullptr };
|
||||
Fn<void()> init;
|
||||
Fn<bool(int, int, int)> overrideKey;
|
||||
Fn<void(PeerListRowId)> deselect;
|
||||
Fn<void()> activate;
|
||||
rpl::producer<Ui::ScrollToRequest> scrollToRequests;
|
||||
};
|
||||
|
||||
class ConfInviteController final : public ContactsBoxController {
|
||||
public:
|
||||
ConfInviteController(
|
||||
not_null<Main::Session*> session,
|
||||
ConfInviteStyles st,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn,
|
||||
Fn<void()> shareLink,
|
||||
std::vector<not_null<UserData*>> prioritize);
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> hasSelectedValue() const;
|
||||
[[nodiscard]] std::vector<InviteRequest> requests(
|
||||
const std::vector<not_null<PeerData*>> &peers) const;
|
||||
|
||||
void noSearchSubmit();
|
||||
[[nodiscard]] auto prioritizeScrollRequests() const
|
||||
-> rpl::producer<Ui::ScrollToRequest>;
|
||||
|
||||
protected:
|
||||
void prepareViewHook() override;
|
||||
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<UserData*> user) override;
|
||||
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
void rowElementClicked(not_null<PeerListRow*> row, int element) override;
|
||||
bool handleDeselectForeignRow(PeerListRowId itemId) override;
|
||||
|
||||
bool overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) override;
|
||||
private:
|
||||
[[nodiscard]] int fullCount() const;
|
||||
void toggleRowSelected(not_null<PeerListRow*> row, bool video);
|
||||
[[nodiscard]] bool toggleRowGetChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool video);
|
||||
void addShareLinkButton();
|
||||
void addPriorityInvites();
|
||||
|
||||
const ConfInviteStyles _st;
|
||||
const base::flat_set<not_null<UserData*>> _alreadyIn;
|
||||
const std::vector<not_null<UserData*>> _prioritize;
|
||||
const Fn<void()> _shareLink;
|
||||
PrioritizedSelector _prioritizeRows;
|
||||
rpl::event_stream<Ui::ScrollToRequest> _prioritizeScrollRequests;
|
||||
base::flat_set<not_null<UserData*>> _skip;
|
||||
rpl::variable<bool> _hasSelected;
|
||||
base::flat_set<not_null<UserData*>> _withVideo;
|
||||
bool _lastSelectWithVideo = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] ConfInviteStyles ConfInviteDarkStyles() {
|
||||
return {
|
||||
.video = &st::confcallInviteVideo,
|
||||
.videoActive = &st::confcallInviteVideoActive,
|
||||
.audio = &st::confcallInviteAudio,
|
||||
.audioActive = &st::confcallInviteAudioActive,
|
||||
.inviteViaLink = &st::groupCallInviteLink,
|
||||
.inviteViaLinkIcon = &st::groupCallInviteLinkIcon,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] ConfInviteStyles ConfInviteDefaultStyles() {
|
||||
return {
|
||||
.video = &st::createCallVideo,
|
||||
.videoActive = &st::createCallVideoActive,
|
||||
.audio = &st::createCallAudio,
|
||||
.audioActive = &st::createCallAudioActive,
|
||||
.inviteViaLink = &st::createCallInviteLink,
|
||||
.inviteViaLinkIcon = &st::createCallInviteLinkIcon,
|
||||
};
|
||||
}
|
||||
|
||||
ConfInviteRow::ConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st)
|
||||
: PeerListRow(user)
|
||||
, _st(st) {
|
||||
}
|
||||
|
||||
void ConfInviteRow::setAlreadyIn(bool alreadyIn) {
|
||||
_alreadyIn = alreadyIn;
|
||||
setDisabledState(alreadyIn ? State::DisabledChecked : State::Active);
|
||||
}
|
||||
|
||||
void ConfInviteRow::setVideo(bool video) {
|
||||
_video = video;
|
||||
}
|
||||
|
||||
const style::IconButton &ConfInviteRow::buttonSt(int element) const {
|
||||
return (element == 1)
|
||||
? (_st.video ? *_st.video : st::createCallVideo)
|
||||
: (_st.audio ? *_st.audio : st::createCallAudio);
|
||||
}
|
||||
|
||||
int ConfInviteRow::elementsCount() const {
|
||||
return _alreadyIn ? 0 : 2;
|
||||
}
|
||||
|
||||
QRect ConfInviteRow::elementGeometry(int element, int outerWidth) const {
|
||||
if (_alreadyIn || (element != 1 && element != 2)) {
|
||||
return QRect();
|
||||
}
|
||||
const auto &st = buttonSt(element);
|
||||
const auto size = QSize(st.width, st.height);
|
||||
const auto margins = (element == 1)
|
||||
? st::createCallVideoMargins
|
||||
: st::createCallAudioMargins;
|
||||
const auto right = margins.right();
|
||||
const auto top = margins.top();
|
||||
const auto side = (element == 1)
|
||||
? outerWidth
|
||||
: elementGeometry(1, outerWidth).x();
|
||||
const auto left = side - right - size.width();
|
||||
return QRect(QPoint(left, top), size);
|
||||
}
|
||||
|
||||
bool ConfInviteRow::elementDisabled(int element) const {
|
||||
return _alreadyIn
|
||||
|| (checked()
|
||||
&& ((_video && element == 1) || (!_video && element == 2)));
|
||||
}
|
||||
|
||||
bool ConfInviteRow::elementOnlySelect(int element) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ConfInviteRow::elementAddRipple(
|
||||
int element,
|
||||
QPoint point,
|
||||
Fn<void()> updateCallback) {
|
||||
if (_alreadyIn || (element != 1 && element != 2)) {
|
||||
return;
|
||||
}
|
||||
auto &ripple = (element == 1) ? _videoRipple : _audioRipple;
|
||||
const auto &st = buttonSt(element);
|
||||
if (!ripple) {
|
||||
auto mask = Ui::RippleAnimation::EllipseMask(QSize(
|
||||
st.rippleAreaSize,
|
||||
st.rippleAreaSize));
|
||||
ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st.ripple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
}
|
||||
ripple->add(point - st.rippleAreaPosition);
|
||||
}
|
||||
|
||||
void ConfInviteRow::elementsStopLastRipple() {
|
||||
if (_videoRipple) {
|
||||
_videoRipple->lastStop();
|
||||
}
|
||||
if (_audioRipple) {
|
||||
_audioRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfInviteRow::elementsPaint(
|
||||
Painter &p,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
int selectedElement) {
|
||||
if (_alreadyIn) {
|
||||
return;
|
||||
}
|
||||
const auto paintElement = [&](int element) {
|
||||
const auto &st = buttonSt(element);
|
||||
auto &ripple = (element == 1) ? _videoRipple : _audioRipple;
|
||||
const auto active = checked() && ((element == 1) ? _video : !_video);
|
||||
const auto geometry = elementGeometry(element, outerWidth);
|
||||
if (ripple) {
|
||||
ripple->paint(
|
||||
p,
|
||||
geometry.x() + st.rippleAreaPosition.x(),
|
||||
geometry.y() + st.rippleAreaPosition.y(),
|
||||
outerWidth);
|
||||
if (ripple->empty()) {
|
||||
ripple.reset();
|
||||
}
|
||||
}
|
||||
const auto selected = (element == selectedElement);
|
||||
const auto &icon = active
|
||||
? (element == 1
|
||||
? (_st.videoActive
|
||||
? *_st.videoActive
|
||||
: st::createCallVideoActive)
|
||||
: (_st.audioActive
|
||||
? *_st.audioActive
|
||||
: st::createCallAudioActive))
|
||||
: (selected ? st.iconOver : st.icon);
|
||||
icon.paintInCenter(p, geometry);
|
||||
};
|
||||
paintElement(1);
|
||||
paintElement(2);
|
||||
}
|
||||
|
||||
[[nodiscard]] PrioritizedSelector PrioritizedInviteSelector(
|
||||
const ConfInviteStyles &st,
|
||||
std::vector<not_null<UserData*>> users,
|
||||
Fn<bool(not_null<PeerListRow*>, bool, anim::type)> toggleGetChecked,
|
||||
Fn<bool()> lastSelectWithVideo,
|
||||
Fn<void(bool)> setLastSelectWithVideo) {
|
||||
class PrioritizedController final : public PeerListController {
|
||||
public:
|
||||
PrioritizedController(
|
||||
const ConfInviteStyles &st,
|
||||
std::vector<not_null<UserData*>> users,
|
||||
Fn<bool(
|
||||
not_null<PeerListRow*>,
|
||||
bool,
|
||||
anim::type)> toggleGetChecked,
|
||||
Fn<bool()> lastSelectWithVideo,
|
||||
Fn<void(bool)> setLastSelectWithVideo)
|
||||
: _st(st)
|
||||
, _users(std::move(users))
|
||||
, _toggleGetChecked(std::move(toggleGetChecked))
|
||||
, _lastSelectWithVideo(std::move(lastSelectWithVideo))
|
||||
, _setLastSelectWithVideo(std::move(setLastSelectWithVideo)) {
|
||||
Expects(!_users.empty());
|
||||
}
|
||||
|
||||
void prepare() override {
|
||||
for (const auto user : _users) {
|
||||
delegate()->peerListAppendRow(
|
||||
std::make_unique<ConfInviteRow>(user, _st));
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
void loadMoreRows() override {
|
||||
}
|
||||
void rowClicked(not_null<PeerListRow*> row) override {
|
||||
toggleRowSelected(row, _lastSelectWithVideo());
|
||||
}
|
||||
void rowElementClicked(
|
||||
not_null<PeerListRow*> row,
|
||||
int element) override {
|
||||
if (row->checked()) {
|
||||
static_cast<ConfInviteRow*>(row.get())->setVideo(
|
||||
element == 1);
|
||||
_setLastSelectWithVideo(element == 1);
|
||||
} else if (element == 1) {
|
||||
toggleRowSelected(row, true);
|
||||
} else if (element == 2) {
|
||||
toggleRowSelected(row, false);
|
||||
}
|
||||
}
|
||||
|
||||
void toggleRowSelected(not_null<PeerListRow*> row, bool video) {
|
||||
delegate()->peerListSetRowChecked(
|
||||
row,
|
||||
_toggleGetChecked(row, video, anim::type::normal));
|
||||
}
|
||||
|
||||
Main::Session &session() const override {
|
||||
return _users.front()->session();
|
||||
}
|
||||
|
||||
void toggleFirst() {
|
||||
rowClicked(delegate()->peerListRowAt(0));
|
||||
}
|
||||
|
||||
private:
|
||||
const ConfInviteStyles &_st;
|
||||
std::vector<not_null<UserData*>> _users;
|
||||
Fn<bool(not_null<PeerListRow*>, bool, anim::type)> _toggleGetChecked;
|
||||
Fn<bool()> _lastSelectWithVideo;
|
||||
Fn<void(bool)> _setLastSelectWithVideo;
|
||||
|
||||
};
|
||||
|
||||
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
|
||||
const auto container = result.data();
|
||||
|
||||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime(
|
||||
).make_state<PrioritizedController>(
|
||||
st,
|
||||
users,
|
||||
toggleGetChecked,
|
||||
lastSelectWithVideo,
|
||||
setLastSelectWithVideo);
|
||||
controller->setStyleOverrides(&st::createCallList);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
controller));
|
||||
const auto activate = [=] {
|
||||
content->submitted();
|
||||
};
|
||||
content->noSearchSubmits() | rpl::start_with_next([=] {
|
||||
controller->toggleFirst();
|
||||
}, content->lifetime());
|
||||
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
|
||||
Ui::AddDivider(container);
|
||||
|
||||
const auto overrideKey = [=](int direction, int from, int to) {
|
||||
if (!content->isVisible()) {
|
||||
return false;
|
||||
} else if (direction > 0 && from < 0 && to >= 0) {
|
||||
if (content->hasSelection()) {
|
||||
const auto was = content->selectedIndex();
|
||||
const auto now = content->selectSkip(1).reallyMovedTo;
|
||||
if (was != now) {
|
||||
return true;
|
||||
}
|
||||
content->clearSelection();
|
||||
} else {
|
||||
content->selectSkip(1);
|
||||
return true;
|
||||
}
|
||||
} else if (direction < 0 && to < 0) {
|
||||
if (!content->hasSelection()) {
|
||||
content->selectLast();
|
||||
} else if (from >= 0 || content->hasSelection()) {
|
||||
content->selectSkip(-1);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto deselect = [=](PeerListRowId rowId) {
|
||||
if (const auto row = delegate->peerListFindRow(rowId)) {
|
||||
delegate->peerListSetRowChecked(row, false);
|
||||
}
|
||||
};
|
||||
|
||||
const auto init = [=] {
|
||||
for (const auto &user : users) {
|
||||
if (const auto row = delegate->peerListFindRow(user->id.value)) {
|
||||
delegate->peerListSetRowChecked(
|
||||
row,
|
||||
toggleGetChecked(row, false, anim::type::instant));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
.content = std::move(result),
|
||||
.init = init,
|
||||
.overrideKey = overrideKey,
|
||||
.deselect = deselect,
|
||||
.activate = activate,
|
||||
.scrollToRequests = content->scrollToRequests(),
|
||||
};
|
||||
}
|
||||
|
||||
ConfInviteController::ConfInviteController(
|
||||
not_null<Main::Session*> session,
|
||||
ConfInviteStyles st,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn,
|
||||
Fn<void()> shareLink,
|
||||
std::vector<not_null<UserData*>> prioritize)
|
||||
: ContactsBoxController(session)
|
||||
, _st(st)
|
||||
, _alreadyIn(std::move(alreadyIn))
|
||||
, _prioritize(std::move(prioritize))
|
||||
, _shareLink(std::move(shareLink)) {
|
||||
if (!_shareLink) {
|
||||
_skip.reserve(_prioritize.size());
|
||||
for (const auto user : _prioritize) {
|
||||
_skip.emplace(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<bool> ConfInviteController::hasSelectedValue() const {
|
||||
return _hasSelected.value();
|
||||
}
|
||||
|
||||
std::vector<InviteRequest> ConfInviteController::requests(
|
||||
const std::vector<not_null<PeerData*>> &peers) const {
|
||||
auto result = std::vector<InviteRequest>();
|
||||
result.reserve(peers.size());
|
||||
for (const auto &peer : peers) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
result.push_back({ user, _withVideo.contains(user) });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> ConfInviteController::createRow(
|
||||
not_null<UserData*> user) {
|
||||
if (user->isSelf()
|
||||
|| user->isBot()
|
||||
|| user->isServiceUser()
|
||||
|| user->isInaccessible()
|
||||
|| _skip.contains(user)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto result = std::make_unique<ConfInviteRow>(user, _st);
|
||||
if (_alreadyIn.contains(user)) {
|
||||
result->setAlreadyIn(true);
|
||||
}
|
||||
if (_withVideo.contains(user)) {
|
||||
result->setVideo(true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int ConfInviteController::fullCount() const {
|
||||
return _alreadyIn.size()
|
||||
+ delegate()->peerListSelectedRowsCount()
|
||||
+ (_alreadyIn.contains(session().user()) ? 1 : 0);
|
||||
}
|
||||
|
||||
void ConfInviteController::rowClicked(not_null<PeerListRow*> row) {
|
||||
toggleRowSelected(row, _lastSelectWithVideo);
|
||||
}
|
||||
|
||||
void ConfInviteController::rowElementClicked(
|
||||
not_null<PeerListRow*> row,
|
||||
int element) {
|
||||
if (row->checked()) {
|
||||
static_cast<ConfInviteRow*>(row.get())->setVideo(element == 1);
|
||||
_lastSelectWithVideo = (element == 1);
|
||||
} else if (element == 1) {
|
||||
toggleRowSelected(row, true);
|
||||
} else if (element == 2) {
|
||||
toggleRowSelected(row, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfInviteController::handleDeselectForeignRow(PeerListRowId itemId) {
|
||||
if (_prioritizeRows.deselect) {
|
||||
const auto userId = peerToUser(PeerId(itemId));
|
||||
if (ranges::contains(_prioritize, session().data().user(userId))) {
|
||||
_prioritizeRows.deselect(itemId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ConfInviteController::overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) {
|
||||
return _prioritizeRows.overrideKey
|
||||
&& _prioritizeRows.overrideKey(direction, fromIndex, toIndex);
|
||||
}
|
||||
|
||||
void ConfInviteController::toggleRowSelected(
|
||||
not_null<PeerListRow*> row,
|
||||
bool video) {
|
||||
delegate()->peerListSetRowChecked(row, toggleRowGetChecked(row, video));
|
||||
|
||||
// row may have been destroyed here, from search.
|
||||
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
|
||||
}
|
||||
|
||||
bool ConfInviteController::toggleRowGetChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool video) {
|
||||
auto count = fullCount();
|
||||
const auto conferenceLimit = session().appConfig().confcallSizeLimit();
|
||||
if (!row->checked() && count >= conferenceLimit) {
|
||||
delegate()->peerListUiShow()->showToast(
|
||||
tr::lng_group_call_invite_limit(tr::now));
|
||||
return false;
|
||||
}
|
||||
const auto real = static_cast<ConfInviteRow*>(row.get());
|
||||
if (!row->checked()) {
|
||||
real->setVideo(video);
|
||||
_lastSelectWithVideo = video;
|
||||
}
|
||||
const auto user = row->peer()->asUser();
|
||||
if (!row->checked() && video) {
|
||||
_withVideo.emplace(user);
|
||||
} else {
|
||||
_withVideo.remove(user);
|
||||
}
|
||||
return !row->checked();
|
||||
}
|
||||
|
||||
void ConfInviteController::noSearchSubmit() {
|
||||
if (const auto onstack = _prioritizeRows.activate) {
|
||||
onstack();
|
||||
} else if (delegate()->peerListFullRowsCount() > 0) {
|
||||
rowClicked(delegate()->peerListRowAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
auto ConfInviteController::prioritizeScrollRequests() const
|
||||
-> rpl::producer<Ui::ScrollToRequest> {
|
||||
return _prioritizeScrollRequests.events();
|
||||
}
|
||||
|
||||
void ConfInviteController::prepareViewHook() {
|
||||
if (_shareLink) {
|
||||
addShareLinkButton();
|
||||
} else if (!_prioritize.empty()) {
|
||||
addPriorityInvites();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfInviteController::addPriorityInvites() {
|
||||
const auto toggleGetChecked = [=](
|
||||
not_null<PeerListRow*> row,
|
||||
bool video,
|
||||
anim::type animated) {
|
||||
const auto result = toggleRowGetChecked(row, video);
|
||||
delegate()->peerListSetForeignRowChecked(
|
||||
row,
|
||||
result,
|
||||
animated);
|
||||
|
||||
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
|
||||
|
||||
return result;
|
||||
};
|
||||
_prioritizeRows = PrioritizedInviteSelector(
|
||||
_st,
|
||||
_prioritize,
|
||||
toggleGetChecked,
|
||||
[=] { return _lastSelectWithVideo; },
|
||||
[=](bool video) { _lastSelectWithVideo = video; });
|
||||
if (auto &scrollTo = _prioritizeRows.scrollToRequests) {
|
||||
std::move(
|
||||
scrollTo
|
||||
) | rpl::start_to_stream(_prioritizeScrollRequests, lifetime());
|
||||
}
|
||||
if (const auto onstack = _prioritizeRows.init) {
|
||||
onstack();
|
||||
|
||||
// Force finishing in instant adding checked rows bunch.
|
||||
delegate()->peerListAddSelectedPeers(
|
||||
std::vector<not_null<PeerData*>>());
|
||||
}
|
||||
delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content));
|
||||
}
|
||||
|
||||
void ConfInviteController::addShareLinkButton() {
|
||||
auto button = object_ptr<Ui::PaddingWrap<Ui::SettingsButton>>(
|
||||
nullptr,
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
nullptr,
|
||||
tr::lng_profile_add_via_link(),
|
||||
(_st.inviteViaLink
|
||||
? *_st.inviteViaLink
|
||||
: st::createCallInviteLink)),
|
||||
style::margins(0, st::membersMarginTop, 0, 0));
|
||||
|
||||
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
|
||||
button->entity(),
|
||||
(_st.inviteViaLinkIcon
|
||||
? *_st.inviteViaLinkIcon
|
||||
: st::createCallInviteLinkIcon),
|
||||
QPoint());
|
||||
button->entity()->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
icon->moveToLeft(
|
||||
st::createCallInviteLinkIconPosition.x(),
|
||||
(height - st::groupCallInviteLinkIcon.height()) / 2);
|
||||
}, icon->lifetime());
|
||||
|
||||
button->entity()->setClickedCallback(_shareLink);
|
||||
button->entity()->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::Enter);
|
||||
}) | rpl::start_with_next([=] {
|
||||
delegate()->peerListMouseLeftGeometry();
|
||||
}, button->lifetime());
|
||||
delegate()->peerListSetAboveWidget(std::move(button));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
InviteController::InviteController(
|
||||
|
@ -167,19 +803,73 @@ std::unique_ptr<PeerListRow> InviteContactsController::createRow(
|
|||
|
||||
object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<GroupCall*> call,
|
||||
Fn<void(TextWithEntities&&)> showToast) {
|
||||
Fn<void(TextWithEntities&&)> showToast,
|
||||
Fn<void()> shareConferenceLink) {
|
||||
const auto real = call->lookupReal();
|
||||
if (!real) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto peer = call->peer();
|
||||
auto alreadyIn = peer->owner().invitedToCallUsers(real->id());
|
||||
const auto conference = call->conference();
|
||||
const auto weak = base::make_weak(call);
|
||||
const auto &invited = peer->owner().invitedToCallUsers(real->id());
|
||||
auto alreadyIn = base::flat_set<not_null<UserData*>>();
|
||||
alreadyIn.reserve(invited.size() + real->participants().size() + 1);
|
||||
alreadyIn.emplace(peer->session().user());
|
||||
for (const auto &participant : real->participants()) {
|
||||
if (const auto user = participant.peer->asUser()) {
|
||||
alreadyIn.emplace(user);
|
||||
}
|
||||
}
|
||||
alreadyIn.emplace(peer->session().user());
|
||||
for (const auto &[user, calling] : invited) {
|
||||
if (!conference || calling) {
|
||||
alreadyIn.emplace(user);
|
||||
}
|
||||
}
|
||||
if (conference) {
|
||||
const auto close = std::make_shared<Fn<void()>>();
|
||||
const auto shareLink = [=] {
|
||||
Expects(shareConferenceLink != nullptr);
|
||||
|
||||
shareConferenceLink();
|
||||
(*close)();
|
||||
};
|
||||
auto controller = std::make_unique<ConfInviteController>(
|
||||
&real->session(),
|
||||
ConfInviteDarkStyles(),
|
||||
alreadyIn,
|
||||
shareLink,
|
||||
std::vector<not_null<UserData*>>());
|
||||
const auto raw = controller.get();
|
||||
raw->setStyleOverrides(
|
||||
&st::groupCallInviteMembersList,
|
||||
&st::groupCallMultiSelect);
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->setTitle(tr::lng_group_call_invite_conf());
|
||||
raw->hasSelectedValue() | rpl::start_with_next([=](bool has) {
|
||||
box->clearButtons();
|
||||
if (has) {
|
||||
box->addButton(tr::lng_group_call_confcall_add(), [=] {
|
||||
const auto call = weak.get();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
const auto done = [=](InviteResult result) {
|
||||
(*close)();
|
||||
showToast({ ComposeInviteResultToast(result) });
|
||||
};
|
||||
call->inviteUsers(
|
||||
raw->requests(box->collectSelectedRows()),
|
||||
done);
|
||||
});
|
||||
}
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}, box->lifetime());
|
||||
*close = crl::guard(box, [=] { box->closeBox(); });
|
||||
};
|
||||
return Box<PeerListBox>(std::move(controller), initBox);
|
||||
}
|
||||
|
||||
auto controller = std::make_unique<InviteController>(peer, alreadyIn);
|
||||
controller->setStyleOverrides(
|
||||
&st::groupCallInviteMembersList,
|
||||
|
@ -194,30 +884,31 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
|
|||
&st::groupCallInviteMembersList,
|
||||
&st::groupCallMultiSelect);
|
||||
|
||||
const auto weak = base::make_weak(call);
|
||||
const auto invite = [=](const std::vector<not_null<UserData*>> &users) {
|
||||
const auto call = weak.get();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
const auto result = call->inviteUsers(users);
|
||||
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
|
||||
showToast(tr::lng_group_call_invite_done_user(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold((*user)->firstName),
|
||||
Ui::Text::WithEntities));
|
||||
} else if (const auto count = std::get_if<int>(&result)) {
|
||||
if (*count > 0) {
|
||||
auto requests = ranges::views::all(
|
||||
users
|
||||
) | ranges::views::transform([](not_null<UserData*> user) {
|
||||
return InviteRequest{ user };
|
||||
}) | ranges::to_vector;
|
||||
call->inviteUsers(std::move(requests), [=](InviteResult result) {
|
||||
if (result.invited.size() == 1) {
|
||||
showToast(tr::lng_group_call_invite_done_user(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(result.invited.front()->firstName),
|
||||
Ui::Text::WithEntities));
|
||||
} else if (result.invited.size() > 1) {
|
||||
showToast(tr::lng_group_call_invite_done_many(
|
||||
tr::now,
|
||||
lt_count,
|
||||
*count,
|
||||
result.invited.size(),
|
||||
Ui::Text::RichLangValue));
|
||||
}
|
||||
} else {
|
||||
Unexpected("Result in GroupCall::inviteUsers.");
|
||||
}
|
||||
});
|
||||
};
|
||||
const auto inviteWithAdd = [=](
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
|
@ -308,4 +999,215 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
|
|||
return Box<PeerListsBox>(std::move(controllers), initBox);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<Call*> call,
|
||||
Fn<void(std::vector<InviteRequest>)> inviteUsers,
|
||||
Fn<void()> shareLink) {
|
||||
const auto user = call->user();
|
||||
const auto weak = base::make_weak(call);
|
||||
auto alreadyIn = base::flat_set<not_null<UserData*>>{ user };
|
||||
auto controller = std::make_unique<ConfInviteController>(
|
||||
&user->session(),
|
||||
ConfInviteDarkStyles(),
|
||||
alreadyIn,
|
||||
shareLink,
|
||||
std::vector<not_null<UserData*>>());
|
||||
const auto raw = controller.get();
|
||||
raw->setStyleOverrides(
|
||||
&st::groupCallInviteMembersList,
|
||||
&st::groupCallMultiSelect);
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->setTitle(tr::lng_group_call_invite_conf());
|
||||
raw->hasSelectedValue() | rpl::start_with_next([=](bool has) {
|
||||
box->clearButtons();
|
||||
if (has) {
|
||||
box->addButton(tr::lng_group_call_invite_button(), [=] {
|
||||
const auto call = weak.get();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
inviteUsers(raw->requests(box->collectSelectedRows()));
|
||||
});
|
||||
}
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}, box->lifetime());
|
||||
};
|
||||
return Box<PeerListBox>(std::move(controller), initBox);
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> CreateReActivateHeader(not_null<QWidget*> parent) {
|
||||
const auto result = Ui::CreateChild<Ui::VerticalLayout>(parent);
|
||||
result->add(
|
||||
MakeJoinCallLogo(result),
|
||||
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
|
||||
|
||||
result->add(
|
||||
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
|
||||
result,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
result,
|
||||
tr::lng_confcall_inactive_title(),
|
||||
st::boxTitle)),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding);
|
||||
result->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
result,
|
||||
tr::lng_confcall_inactive_about(),
|
||||
st::confcallLinkCenteredText),
|
||||
st::boxRowPadding + st::confcallLinkTitlePadding
|
||||
)->setTryMakeSimilarLines(true);
|
||||
Ui::AddDivider(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void InitReActivate(not_null<PeerListBox*> box) {
|
||||
box->setTitle(rpl::producer<TextWithEntities>(nullptr));
|
||||
box->setNoContentMargin(true);
|
||||
|
||||
const auto header = CreateReActivateHeader(box);
|
||||
header->resizeToWidth(st::boxWideWidth);
|
||||
header->heightValue() | rpl::start_with_next([=](int height) {
|
||||
box->setAddedTopScrollSkip(height, true);
|
||||
}, header->lifetime());
|
||||
header->moveToLeft(0, 0);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
MsgId inviteMsgId,
|
||||
std::vector<not_null<UserData*>> prioritize) {
|
||||
auto controller = std::make_unique<ConfInviteController>(
|
||||
&call->session(),
|
||||
ConfInviteDefaultStyles(),
|
||||
base::flat_set<not_null<UserData*>>(),
|
||||
nullptr,
|
||||
std::move(prioritize));
|
||||
const auto raw = controller.get();
|
||||
raw->setStyleOverrides(&st::createCallList);
|
||||
const auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
InitReActivate(box);
|
||||
|
||||
box->noSearchSubmits() | rpl::start_with_next([=] {
|
||||
raw->noSearchSubmit();
|
||||
}, box->lifetime());
|
||||
|
||||
raw->prioritizeScrollRequests(
|
||||
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
|
||||
box->scrollTo(request);
|
||||
}, box->lifetime());
|
||||
|
||||
const auto join = [=] {
|
||||
const auto weak = Ui::MakeWeak(box);
|
||||
auto selected = raw->requests(box->collectSelectedRows());
|
||||
Core::App().calls().startOrJoinConferenceCall({
|
||||
.call = call,
|
||||
.joinMessageId = inviteMsgId,
|
||||
.invite = std::move(selected),
|
||||
});
|
||||
if (const auto strong = weak.data()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
};
|
||||
box->addButton(
|
||||
rpl::conditional(
|
||||
raw->hasSelectedValue(),
|
||||
tr::lng_group_call_confcall_add(),
|
||||
tr::lng_create_group_create()),
|
||||
join);
|
||||
box->addButton(tr::lng_close(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
};
|
||||
return Box<PeerListBox>(std::move(controller), initBox);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> PrepareCreateCallBox(
|
||||
not_null<::Window::SessionController*> window,
|
||||
Fn<void()> created,
|
||||
MsgId discardedInviteMsgId,
|
||||
std::vector<not_null<UserData*>> prioritize) {
|
||||
struct State {
|
||||
bool creatingLink = false;
|
||||
QPointer<PeerListBox> box;
|
||||
};
|
||||
const auto state = std::make_shared<State>();
|
||||
const auto finished = [=](bool ok) {
|
||||
if (!ok) {
|
||||
state->creatingLink = false;
|
||||
} else {
|
||||
if (const auto strong = state->box.data()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
if (const auto onstack = created) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto shareLink = [=] {
|
||||
if (state->creatingLink) {
|
||||
return;
|
||||
}
|
||||
state->creatingLink = true;
|
||||
MakeConferenceCall({
|
||||
.show = window->uiShow(),
|
||||
.finished = finished,
|
||||
});
|
||||
};
|
||||
auto controller = std::make_unique<ConfInviteController>(
|
||||
&window->session(),
|
||||
ConfInviteDefaultStyles(),
|
||||
base::flat_set<not_null<UserData*>>(),
|
||||
discardedInviteMsgId ? Fn<void()>() : shareLink,
|
||||
std::move(prioritize));
|
||||
const auto raw = controller.get();
|
||||
if (discardedInviteMsgId) {
|
||||
raw->setStyleOverrides(&st::createCallList);
|
||||
}
|
||||
const auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
if (discardedInviteMsgId) {
|
||||
InitReActivate(box);
|
||||
} else {
|
||||
box->setTitle(tr::lng_confcall_create_title());
|
||||
}
|
||||
|
||||
box->noSearchSubmits() | rpl::start_with_next([=] {
|
||||
raw->noSearchSubmit();
|
||||
}, box->lifetime());
|
||||
|
||||
raw->prioritizeScrollRequests(
|
||||
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
|
||||
box->scrollTo(request);
|
||||
}, box->lifetime());
|
||||
|
||||
const auto create = [=] {
|
||||
auto selected = raw->requests(box->collectSelectedRows());
|
||||
if (selected.size() != 1 || discardedInviteMsgId) {
|
||||
Core::App().calls().startOrJoinConferenceCall({
|
||||
.show = window->uiShow(),
|
||||
.invite = std::move(selected),
|
||||
});
|
||||
} else {
|
||||
const auto &invite = selected.front();
|
||||
Core::App().calls().startOutgoingCall(
|
||||
invite.user,
|
||||
invite.video);
|
||||
}
|
||||
finished(true);
|
||||
};
|
||||
box->addButton(
|
||||
rpl::conditional(
|
||||
raw->hasSelectedValue(),
|
||||
tr::lng_group_call_confcall_add(),
|
||||
tr::lng_create_group_create()),
|
||||
create);
|
||||
box->addButton(tr::lng_close(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
};
|
||||
auto result = Box<PeerListBox>(std::move(controller), initBox);
|
||||
state->box = result.data();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
|
|
|
@ -11,9 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "boxes/peers/add_participants_box.h"
|
||||
|
||||
namespace Calls {
|
||||
class Call;
|
||||
class GroupCall;
|
||||
struct InviteRequest;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class InviteController final : public ParticipantsBoxController {
|
||||
|
@ -77,6 +83,23 @@ private:
|
|||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<GroupCall*> call,
|
||||
Fn<void(TextWithEntities&&)> showToast);
|
||||
Fn<void(TextWithEntities&&)> showToast,
|
||||
Fn<void()> shareConferenceLink = nullptr);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<Call*> call,
|
||||
Fn<void(std::vector<InviteRequest>)> inviteUsers,
|
||||
Fn<void()> shareLink);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
|
||||
std::shared_ptr<Data::GroupCall> call,
|
||||
MsgId inviteMsgId,
|
||||
std::vector<not_null<UserData*>> prioritize);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareCreateCallBox(
|
||||
not_null<::Window::SessionController*> window,
|
||||
Fn<void()> created = nullptr,
|
||||
MsgId discardedInviteMsgId = 0,
|
||||
std::vector<not_null<UserData*>> prioritize = {});
|
||||
|
||||
} // namespace Calls::Group
|
||||
|
|
|
@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "calls/group/calls_volume_item.h"
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "calls/calls_emoji_fingerprint.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_user.h"
|
||||
|
@ -107,6 +109,9 @@ private:
|
|||
[[nodiscard]] std::unique_ptr<Row> createRow(
|
||||
const Data::GroupCallParticipant &participant);
|
||||
[[nodiscard]] std::unique_ptr<Row> createInvitedRow(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool calling);
|
||||
[[nodiscard]] std::unique_ptr<Row> createWithAccessRow(
|
||||
not_null<PeerData*> participantPeer);
|
||||
|
||||
[[nodiscard]] bool isMe(not_null<PeerData*> participantPeer) const;
|
||||
|
@ -128,7 +133,8 @@ private:
|
|||
void updateRow(
|
||||
not_null<Row*> row,
|
||||
const std::optional<Data::GroupCallParticipant> &was,
|
||||
const Data::GroupCallParticipant *participant);
|
||||
const Data::GroupCallParticipant *participant,
|
||||
Row::State noParticipantState = Row::State::Invited);
|
||||
void updateRowInSoundingMap(
|
||||
not_null<Row*> row,
|
||||
bool wasSounding,
|
||||
|
@ -162,8 +168,13 @@ private:
|
|||
const VideoEndpoint &endpoint,
|
||||
bool active);
|
||||
|
||||
void appendInvitedUsers();
|
||||
void partitionRows();
|
||||
void setupInvitedUsers();
|
||||
[[nodiscard]] bool appendInvitedUsers();
|
||||
void setupWithAccessUsers();
|
||||
[[nodiscard]] bool appendWithAccessUsers();
|
||||
void scheduleRaisedHandStatusRemove();
|
||||
void refreshWithAccessRows(base::flat_set<UserId> &&nowIds);
|
||||
|
||||
void hideRowsWithVideoExcept(const VideoEndpoint &large);
|
||||
void showAllHiddenRows();
|
||||
|
@ -205,6 +216,8 @@ private:
|
|||
Ui::RoundRect _narrowRoundRect;
|
||||
QImage _narrowShadow;
|
||||
|
||||
base::flat_set<UserId> _withAccess;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
@ -414,6 +427,9 @@ void Members::Controller::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
|||
if (const auto row = findRow(participantPeer)) {
|
||||
if (isMe(participantPeer)) {
|
||||
updateRow(row, update.was, nullptr);
|
||||
} else if (_withAccess.contains(peerToUser(participantPeer->id))) {
|
||||
updateRow(row, update.was, nullptr, Row::State::WithAccess);
|
||||
partitionRows();
|
||||
} else {
|
||||
removeRow(row);
|
||||
delegate()->peerListRefreshRows();
|
||||
|
@ -431,10 +447,6 @@ void Members::Controller::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
|||
) | rpl::start_with_next([=](const VideoStateToggle &update) {
|
||||
toggleVideoEndpointActive(update.endpoint, update.value);
|
||||
}, _lifetime);
|
||||
|
||||
if (_prepared) {
|
||||
appendInvitedUsers();
|
||||
}
|
||||
}
|
||||
|
||||
void Members::Controller::toggleVideoEndpointActive(
|
||||
|
@ -481,13 +493,22 @@ void Members::Controller::toggleVideoEndpointActive(
|
|||
|
||||
}
|
||||
|
||||
void Members::Controller::appendInvitedUsers() {
|
||||
bool Members::Controller::appendInvitedUsers() {
|
||||
auto changed = false;
|
||||
if (const auto id = _call->id()) {
|
||||
for (const auto &user : _peer->owner().invitedToCallUsers(id)) {
|
||||
if (auto row = createInvitedRow(user)) {
|
||||
const auto &invited = _peer->owner().invitedToCallUsers(id);
|
||||
for (const auto &[user, calling] : invited) {
|
||||
if (auto row = createInvitedRow(user, calling)) {
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void Members::Controller::setupInvitedUsers() {
|
||||
if (appendInvitedUsers()) {
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
|
@ -496,22 +517,98 @@ void Members::Controller::appendInvitedUsers() {
|
|||
) | rpl::filter([=](const Invite &invite) {
|
||||
return (invite.id == _call->id());
|
||||
}) | rpl::start_with_next([=](const Invite &invite) {
|
||||
if (auto row = createInvitedRow(invite.user)) {
|
||||
const auto user = invite.user;
|
||||
if (invite.removed) {
|
||||
if (const auto row = findRow(user)) {
|
||||
if (row->state() == Row::State::Invited
|
||||
|| row->state() == Row::State::Calling) {
|
||||
delegate()->peerListRemoveRow(row);
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
}
|
||||
} else if (auto row = createInvitedRow(user, invite.calling)) {
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
bool Members::Controller::appendWithAccessUsers() {
|
||||
auto changed = false;
|
||||
for (const auto id : _withAccess) {
|
||||
if (auto row = createWithAccessRow(_peer->owner().user(id))) {
|
||||
changed = true;
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void Members::Controller::setupWithAccessUsers() {
|
||||
const auto conference = _call->conferenceCall().get();
|
||||
if (!conference) {
|
||||
return;
|
||||
}
|
||||
conference->participantsWithAccessValue(
|
||||
) | rpl::start_with_next([=](base::flat_set<UserId> &&nowIds) {
|
||||
for (auto i = begin(_withAccess); i != end(_withAccess);) {
|
||||
const auto oldId = *i;
|
||||
if (nowIds.remove(oldId)) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const auto user = _peer->owner().user(oldId);
|
||||
if (const auto row = findRow(user)) {
|
||||
if (row->state() == Row::State::WithAccess) {
|
||||
removeRow(row);
|
||||
}
|
||||
}
|
||||
i = _withAccess.erase(i);
|
||||
}
|
||||
auto partition = false;
|
||||
auto partitionChecked = false;
|
||||
for (const auto nowId : nowIds) {
|
||||
const auto user = _peer->owner().user(nowId);
|
||||
if (!findRow(user)) {
|
||||
if (auto row = createWithAccessRow(user)) {
|
||||
if (!partitionChecked) {
|
||||
partitionChecked = true;
|
||||
if (const auto count = delegate()->peerListFullRowsCount()) {
|
||||
const auto last = delegate()->peerListRowAt(count - 1);
|
||||
const auto state = static_cast<Row*>(last.get())->state();
|
||||
if (state == Row::State::Invited
|
||||
|| state == Row::State::Calling) {
|
||||
partition = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
}
|
||||
_withAccess.emplace(nowId);
|
||||
}
|
||||
if (partition) {
|
||||
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
||||
const auto state = static_cast<const Row&>(row).state();
|
||||
return (state != Row::State::Invited)
|
||||
&& (state != Row::State::Calling);
|
||||
});
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Members::Controller::updateRow(
|
||||
const std::optional<Data::GroupCallParticipant> &was,
|
||||
const Data::GroupCallParticipant &now) {
|
||||
auto reorderIfInvitedBefore = 0;
|
||||
auto reorderIfNonRealBefore = 0;
|
||||
auto checkPosition = (Row*)nullptr;
|
||||
auto addedToBottom = (Row*)nullptr;
|
||||
if (const auto row = findRow(now.peer)) {
|
||||
if (row->state() == Row::State::Invited) {
|
||||
reorderIfInvitedBefore = row->absoluteIndex();
|
||||
if (row->state() == Row::State::Invited
|
||||
|| row->state() == Row::State::Calling
|
||||
|| row->state() == Row::State::WithAccess) {
|
||||
reorderIfNonRealBefore = row->absoluteIndex();
|
||||
}
|
||||
updateRow(row, was, &now);
|
||||
if ((now.speaking && (!was || !was->speaking))
|
||||
|
@ -523,7 +620,7 @@ void Members::Controller::updateRow(
|
|||
if (row->speaking()) {
|
||||
delegate()->peerListPrependRow(std::move(row));
|
||||
} else {
|
||||
reorderIfInvitedBefore = delegate()->peerListFullRowsCount();
|
||||
reorderIfNonRealBefore = delegate()->peerListFullRowsCount();
|
||||
if (now.raisedHandRating != 0) {
|
||||
checkPosition = row.get();
|
||||
} else {
|
||||
|
@ -533,20 +630,21 @@ void Members::Controller::updateRow(
|
|||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
static constexpr auto kInvited = Row::State::Invited;
|
||||
const auto reorder = [&] {
|
||||
const auto count = reorderIfInvitedBefore;
|
||||
const auto count = reorderIfNonRealBefore;
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
const auto row = delegate()->peerListRowAt(
|
||||
reorderIfInvitedBefore - 1).get();
|
||||
return (static_cast<Row*>(row)->state() == kInvited);
|
||||
reorderIfNonRealBefore - 1).get();
|
||||
using State = Row::State;
|
||||
const auto state = static_cast<Row*>(row)->state();
|
||||
return (state == State::Invited)
|
||||
|| (state == State::Calling)
|
||||
|| (state == State::WithAccess);
|
||||
}();
|
||||
if (reorder) {
|
||||
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
||||
return static_cast<const Row&>(row).state() != kInvited;
|
||||
});
|
||||
partitionRows();
|
||||
}
|
||||
if (checkPosition) {
|
||||
checkRowPosition(checkPosition);
|
||||
|
@ -570,6 +668,27 @@ void Members::Controller::updateRow(
|
|||
}
|
||||
}
|
||||
|
||||
void Members::Controller::partitionRows() {
|
||||
auto hadWithAccess = false;
|
||||
delegate()->peerListPartitionRows([&](const PeerListRow &row) {
|
||||
using State = Row::State;
|
||||
const auto state = static_cast<const Row&>(row).state();
|
||||
if (state == State::WithAccess) {
|
||||
hadWithAccess = true;
|
||||
}
|
||||
return (state != State::Invited)
|
||||
&& (state != State::Calling)
|
||||
&& (state != State::WithAccess);
|
||||
});
|
||||
if (hadWithAccess) {
|
||||
delegate()->peerListPartitionRows([](const PeerListRow &row) {
|
||||
const auto state = static_cast<const Row&>(row).state();
|
||||
return (state != Row::State::Invited)
|
||||
&& (state != Row::State::Calling);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool Members::Controller::allRowsAboveAreSpeaking(not_null<Row*> row) const {
|
||||
const auto count = delegate()->peerListFullRowsCount();
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
|
@ -615,7 +734,7 @@ bool Members::Controller::needToReorder(not_null<Row*> row) const {
|
|||
|
||||
if (row->speaking()) {
|
||||
return !allRowsAboveAreSpeaking(row);
|
||||
} else if (!_peer->canManageGroupCall()) {
|
||||
} else if (!_call->canManage()) {
|
||||
// Raising hands reorder participants only for voice chat admins.
|
||||
return false;
|
||||
}
|
||||
|
@ -684,7 +803,7 @@ void Members::Controller::checkRowPosition(not_null<Row*> row) {
|
|||
return proj(a) > proj(b);
|
||||
};
|
||||
};
|
||||
delegate()->peerListSortRows(_peer->canManageGroupCall()
|
||||
delegate()->peerListSortRows(_call->canManage()
|
||||
? makeComparator(projForAdmin)
|
||||
: makeComparator(projForOther));
|
||||
}
|
||||
|
@ -692,14 +811,21 @@ void Members::Controller::checkRowPosition(not_null<Row*> row) {
|
|||
void Members::Controller::updateRow(
|
||||
not_null<Row*> row,
|
||||
const std::optional<Data::GroupCallParticipant> &was,
|
||||
const Data::GroupCallParticipant *participant) {
|
||||
const Data::GroupCallParticipant *participant,
|
||||
Row::State noParticipantState) {
|
||||
const auto wasSounding = row->sounding();
|
||||
const auto wasSsrc = was ? was->ssrc : 0;
|
||||
const auto wasAdditionalSsrc = was
|
||||
? GetAdditionalAudioSsrc(was->videoParams)
|
||||
: 0;
|
||||
row->setSkipLevelUpdate(_skipRowLevelUpdate);
|
||||
row->updateState(participant);
|
||||
if (participant) {
|
||||
row->updateState(*participant);
|
||||
} else if (noParticipantState == Row::State::WithAccess) {
|
||||
row->updateStateWithAccess();
|
||||
} else {
|
||||
row->updateStateInvited(noParticipantState == Row::State::Calling);
|
||||
}
|
||||
|
||||
const auto wasNoSounding = _soundingRowBySsrc.empty();
|
||||
updateRowInSoundingMap(
|
||||
|
@ -842,7 +968,8 @@ void Members::Controller::prepare() {
|
|||
}
|
||||
|
||||
loadMoreRows();
|
||||
appendInvitedUsers();
|
||||
setupWithAccessUsers();
|
||||
setupInvitedUsers();
|
||||
_prepared = true;
|
||||
|
||||
setupListChangeViewers();
|
||||
|
@ -893,6 +1020,12 @@ void Members::Controller::prepareRows(not_null<Data::GroupCall*> real) {
|
|||
delegate()->peerListAppendRow(std::move(row));
|
||||
}
|
||||
}
|
||||
if (appendWithAccessUsers()) {
|
||||
changed = true;
|
||||
}
|
||||
if (appendInvitedUsers()) {
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
@ -919,7 +1052,7 @@ bool Members::Controller::rowIsMe(not_null<PeerData*> participantPeer) {
|
|||
}
|
||||
|
||||
bool Members::Controller::rowCanMuteMembers() {
|
||||
return _peer->canManageGroupCall();
|
||||
return _call->canManage();
|
||||
}
|
||||
|
||||
void Members::Controller::rowUpdateRow(not_null<Row*> row) {
|
||||
|
@ -973,15 +1106,21 @@ void Members::Controller::rowPaintIcon(
|
|||
return;
|
||||
}
|
||||
const auto narrow = (state.style == MembersRowStyle::Narrow);
|
||||
if (state.invited) {
|
||||
if (state.invited || state.calling) {
|
||||
if (narrow) {
|
||||
st::groupCallNarrowInvitedIcon.paintInCenter(p, rect);
|
||||
(state.invited
|
||||
? st::groupCallNarrowInvitedIcon
|
||||
: st::groupCallNarrowCallingIcon).paintInCenter(p, rect);
|
||||
} else {
|
||||
st::groupCallMemberInvited.paintInCenter(
|
||||
const auto &icon = state.invited
|
||||
? st::groupCallMemberInvited
|
||||
: st::groupCallMemberCalling;
|
||||
const auto shift = state.invited
|
||||
? st::groupCallMemberInvitedPosition
|
||||
: st::groupCallMemberCallingPosition;
|
||||
icon.paintInCenter(
|
||||
p,
|
||||
QRect(
|
||||
rect.topLeft() + st::groupCallMemberInvitedPosition,
|
||||
st::groupCallMemberInvited.size()));
|
||||
QRect(rect.topLeft() + shift, icon.size()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -1189,6 +1328,9 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
|||
const auto participantPeer = row->peer();
|
||||
const auto real = static_cast<Row*>(row.get());
|
||||
const auto muteState = real->state();
|
||||
if (muteState == Row::State::WithAccess) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto muted = (muteState == Row::State::Muted)
|
||||
|| (muteState == Row::State::RaisedHand);
|
||||
const auto addCover = !_call->rtmp();
|
||||
|
@ -1218,22 +1360,22 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
|||
window->invokeForSessionController(
|
||||
account,
|
||||
participantPeer,
|
||||
[&](not_null<Window::SessionController*> newController) {
|
||||
[&](not_null<::Window::SessionController*> newController) {
|
||||
callback(newController);
|
||||
newController->widget()->activate();
|
||||
});
|
||||
}
|
||||
};
|
||||
const auto showProfile = [=] {
|
||||
withActiveWindow([=](not_null<Window::SessionController*> window) {
|
||||
withActiveWindow([=](not_null<::Window::SessionController*> window) {
|
||||
window->showPeerInfo(participantPeer);
|
||||
});
|
||||
};
|
||||
const auto showHistory = [=] {
|
||||
withActiveWindow([=](not_null<Window::SessionController*> window) {
|
||||
withActiveWindow([=](not_null<::Window::SessionController*> window) {
|
||||
window->showPeerHistory(
|
||||
participantPeer,
|
||||
Window::SectionShow::Way::Forward);
|
||||
::Window::SectionShow::Way::Forward);
|
||||
});
|
||||
};
|
||||
const auto removeFromVoiceChat = crl::guard(this, [=] {
|
||||
|
@ -1317,7 +1459,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
|||
false,
|
||||
static_cast<Row*>(row.get()));
|
||||
} else if (participant
|
||||
&& (!isMe(participantPeer) || _peer->canManageGroupCall())
|
||||
&& (!isMe(participantPeer) || _call->canManage())
|
||||
&& (participant->ssrc != 0
|
||||
|| GetAdditionalAudioSsrc(participant->videoParams) != 0)) {
|
||||
addMuteActionsToContextMenu(
|
||||
|
@ -1340,6 +1482,29 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
|||
removeHand);
|
||||
}
|
||||
} else {
|
||||
const auto invited = (muteState == Row::State::Invited)
|
||||
|| (muteState == Row::State::Calling);
|
||||
const auto conference = _call->conferenceCall().get();
|
||||
if (conference
|
||||
&& participantPeer->isUser()
|
||||
&& invited) {
|
||||
const auto id = conference->id();
|
||||
const auto cancelInvite = [=](bool discard) {
|
||||
Core::App().calls().declineOutgoingConferenceInvite(
|
||||
id,
|
||||
participantPeer->asUser(),
|
||||
discard);
|
||||
};
|
||||
if (muteState == Row::State::Calling) {
|
||||
result->addAction(
|
||||
tr::lng_group_call_context_stop_ringing(tr::now),
|
||||
[=] { cancelInvite(false); });
|
||||
}
|
||||
result->addAction(
|
||||
tr::lng_group_call_context_cancel_invite(tr::now),
|
||||
[=] { cancelInvite(true); });
|
||||
result->addSeparator();
|
||||
}
|
||||
result->addAction(
|
||||
(participantPeer->isUser()
|
||||
? tr::lng_context_view_profile(tr::now)
|
||||
|
@ -1354,9 +1519,12 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
|||
}
|
||||
const auto canKick = [&] {
|
||||
const auto user = participantPeer->asUser();
|
||||
if (static_cast<Row*>(row.get())->state()
|
||||
== Row::State::Invited) {
|
||||
if (muteState == Row::State::Invited
|
||||
|| muteState == Row::State::Calling
|
||||
|| muteState == Row::State::WithAccess) {
|
||||
return false;
|
||||
} else if (conference && _call->canManage()) {
|
||||
return true;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
return chat->amCreator()
|
||||
|| (user
|
||||
|
@ -1387,11 +1555,11 @@ void Members::Controller::addMuteActionsToContextMenu(
|
|||
bool participantIsCallAdmin,
|
||||
not_null<Row*> row) {
|
||||
const auto muteUnmuteString = [=](bool muted, bool mutedByMe) {
|
||||
return (muted && _peer->canManageGroupCall())
|
||||
return (muted && _call->canManage())
|
||||
? tr::lng_group_call_context_unmute(tr::now)
|
||||
: mutedByMe
|
||||
? tr::lng_group_call_context_unmute_for_me(tr::now)
|
||||
: _peer->canManageGroupCall()
|
||||
: _call->canManage()
|
||||
? tr::lng_group_call_context_mute(tr::now)
|
||||
: tr::lng_group_call_context_mute_for_me(tr::now);
|
||||
};
|
||||
|
@ -1484,11 +1652,13 @@ void Members::Controller::addMuteActionsToContextMenu(
|
|||
|
||||
const auto muteAction = [&]() -> QAction* {
|
||||
if (muteState == Row::State::Invited
|
||||
|| muteState == Row::State::Calling
|
||||
|| muteState == Row::State::WithAccess
|
||||
|| _call->rtmp()
|
||||
|| isMe(participantPeer)
|
||||
|| (muteState == Row::State::Inactive
|
||||
&& participantIsCallAdmin
|
||||
&& _peer->canManageGroupCall())) {
|
||||
&& _call->canManage())) {
|
||||
return nullptr;
|
||||
}
|
||||
auto callback = [=] {
|
||||
|
@ -1538,12 +1708,29 @@ std::unique_ptr<Row> Members::Controller::createRow(
|
|||
}
|
||||
|
||||
std::unique_ptr<Row> Members::Controller::createInvitedRow(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool calling) {
|
||||
if (const auto row = findRow(participantPeer)) {
|
||||
if (row->state() == Row::State::Invited
|
||||
|| row->state() == Row::State::Calling) {
|
||||
row->updateStateInvited(calling);
|
||||
delegate()->peerListUpdateRow(row);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
const auto state = calling ? Row::State::Calling : Row::State::Invited;
|
||||
auto result = std::make_unique<Row>(this, participantPeer);
|
||||
updateRow(result.get(), std::nullopt, nullptr, state);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Row> Members::Controller::createWithAccessRow(
|
||||
not_null<PeerData*> participantPeer) {
|
||||
if (findRow(participantPeer)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto result = std::make_unique<Row>(this, participantPeer);
|
||||
updateRow(result.get(), std::nullopt, nullptr);
|
||||
updateRow(result.get(), std::nullopt, nullptr, Row::State::WithAccess);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -1559,6 +1746,9 @@ Members::Members(
|
|||
, _listController(std::make_unique<Controller>(call, parent, mode))
|
||||
, _layout(_scroll->setOwnedWidget(
|
||||
object_ptr<Ui::VerticalLayout>(_scroll.data())))
|
||||
, _fingerprint(call->conference()
|
||||
? _layout->add(object_ptr<Ui::RpWidget>(_layout.get()))
|
||||
: nullptr)
|
||||
, _videoWrap(_layout->add(object_ptr<Ui::RpWidget>(_layout.get())))
|
||||
, _viewport(
|
||||
std::make_unique<Viewport>(
|
||||
|
@ -1567,6 +1757,7 @@ Members::Members(
|
|||
backend)) {
|
||||
setupList();
|
||||
setupAddMember(call);
|
||||
setupFingerprint();
|
||||
setContent(_list);
|
||||
setupFakeRoundCorners();
|
||||
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
|
||||
|
@ -1615,6 +1806,7 @@ rpl::producer<int> Members::desiredHeightValue() const {
|
|||
return rpl::combine(
|
||||
heightValue(),
|
||||
_addMemberButton.value(),
|
||||
_shareLinkButton.value(),
|
||||
_listController->fullCountValue(),
|
||||
_mode.value()
|
||||
) | rpl::map([=] {
|
||||
|
@ -1626,8 +1818,11 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
using namespace rpl::mappers;
|
||||
|
||||
const auto peer = call->peer();
|
||||
const auto conference = call->conference();
|
||||
const auto canAddByPeer = [=](not_null<PeerData*> peer) {
|
||||
if (peer->isBroadcast()) {
|
||||
if (conference) {
|
||||
return rpl::single(true) | rpl::type_erased();
|
||||
} else if (peer->isBroadcast()) {
|
||||
return rpl::single(false) | rpl::type_erased();
|
||||
}
|
||||
return rpl::combine(
|
||||
|
@ -1638,6 +1833,9 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
}) | rpl::type_erased();
|
||||
};
|
||||
const auto canInviteByLinkByPeer = [=](not_null<PeerData*> peer) {
|
||||
if (conference) {
|
||||
return rpl::single(true) | rpl::type_erased();
|
||||
}
|
||||
const auto channel = peer->asChannel();
|
||||
if (!channel) {
|
||||
return rpl::single(false) | rpl::type_erased();
|
||||
|
@ -1661,6 +1859,8 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
_canInviteByLink = canInviteByLinkByPeer(channel);
|
||||
});
|
||||
|
||||
const auto baseIndex = _layout->count() - 2;
|
||||
|
||||
rpl::combine(
|
||||
_canAddMembers.value(),
|
||||
_canInviteByLink.value(),
|
||||
|
@ -1672,11 +1872,18 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
_addMemberButton = nullptr;
|
||||
updateControlsGeometry();
|
||||
}
|
||||
if (const auto old = _shareLinkButton.current()) {
|
||||
delete old;
|
||||
_shareLinkButton = nullptr;
|
||||
updateControlsGeometry();
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto addMember = Settings::CreateButtonWithIcon(
|
||||
_layout.get(),
|
||||
tr::lng_group_call_invite(),
|
||||
(conference
|
||||
? tr::lng_group_call_invite_conf()
|
||||
: tr::lng_group_call_invite()),
|
||||
st::groupCallAddMember,
|
||||
{ .icon = &st::groupCallAddMemberIcon });
|
||||
addMember->clicks(
|
||||
|
@ -1687,7 +1894,22 @@ void Members::setupAddMember(not_null<GroupCall*> call) {
|
|||
addMember->resizeToWidth(_layout->width());
|
||||
delete _addMemberButton.current();
|
||||
_addMemberButton = addMember.data();
|
||||
_layout->insert(3, std::move(addMember));
|
||||
_layout->insert(baseIndex, std::move(addMember));
|
||||
if (conference) {
|
||||
auto shareLink = Settings::CreateButtonWithIcon(
|
||||
_layout.get(),
|
||||
tr::lng_group_invite_share(),
|
||||
st::groupCallAddMember,
|
||||
{ .icon = &st::groupCallShareLinkIcon });
|
||||
shareLink->clicks() | rpl::to_empty | rpl::start_to_stream(
|
||||
_shareLinkRequests,
|
||||
shareLink->lifetime());
|
||||
shareLink->show();
|
||||
shareLink->resizeToWidth(_layout->width());
|
||||
delete _shareLinkButton.current();
|
||||
_shareLinkButton = shareLink.data();
|
||||
_layout->insert(baseIndex + 1, std::move(shareLink));
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
updateControlsGeometry();
|
||||
|
@ -1713,13 +1935,15 @@ void Members::setMode(PanelMode mode) {
|
|||
}
|
||||
|
||||
QRect Members::getInnerGeometry() const {
|
||||
const auto shareLink = _shareLinkButton.current();
|
||||
const auto addMembers = _addMemberButton.current();
|
||||
const auto share = shareLink ? shareLink->height() : 0;
|
||||
const auto add = addMembers ? addMembers->height() : 0;
|
||||
return QRect(
|
||||
0,
|
||||
-_scroll->scrollTop(),
|
||||
width(),
|
||||
_list->y() + _list->height() + _bottomSkip->height() + add);
|
||||
_list->y() + _list->height() + _bottomSkip->height() + add + share);
|
||||
}
|
||||
|
||||
rpl::producer<int> Members::fullCountValue() const {
|
||||
|
@ -1783,6 +2007,23 @@ void Members::setupList() {
|
|||
}, _scroll->lifetime());
|
||||
}
|
||||
|
||||
void Members::setupFingerprint() {
|
||||
if (const auto raw = _fingerprint) {
|
||||
auto badge = SetupFingerprintBadge(
|
||||
raw->lifetime(),
|
||||
_call->emojiHashValue());
|
||||
std::move(badge.repaints) | rpl::start_to_stream(
|
||||
_fingerprintRepaints,
|
||||
raw->lifetime());
|
||||
_fingerprintState = badge.state;
|
||||
|
||||
SetupFingerprintBadgeWidget(
|
||||
raw,
|
||||
_fingerprintState,
|
||||
_fingerprintRepaints.events());
|
||||
}
|
||||
}
|
||||
|
||||
void Members::trackViewportGeometry() {
|
||||
_call->videoEndpointLargeValue(
|
||||
) | rpl::start_with_next([=](const VideoEndpoint &large) {
|
||||
|
@ -1894,16 +2135,22 @@ void Members::setupFakeRoundCorners() {
|
|||
const auto bottomleft = create({ 0, shift });
|
||||
const auto bottomright = create({ shift, shift });
|
||||
|
||||
const auto heightValue = [=](Ui::RpWidget *widget) {
|
||||
topleft->raise();
|
||||
topright->raise();
|
||||
bottomleft->raise();
|
||||
bottomright->raise();
|
||||
return widget ? widget->heightValue() : rpl::single(0);
|
||||
};
|
||||
rpl::combine(
|
||||
_list->geometryValue(),
|
||||
_addMemberButton.value() | rpl::map([=](Ui::RpWidget *widget) {
|
||||
topleft->raise();
|
||||
topright->raise();
|
||||
bottomleft->raise();
|
||||
bottomright->raise();
|
||||
return widget ? widget->heightValue() : rpl::single(0);
|
||||
}) | rpl::flatten_latest()
|
||||
) | rpl::start_with_next([=](QRect list, int addMembers) {
|
||||
_addMemberButton.value() | rpl::map(
|
||||
heightValue
|
||||
) | rpl::flatten_latest(),
|
||||
_shareLinkButton.value() | rpl::map(
|
||||
heightValue
|
||||
) | rpl::flatten_latest()
|
||||
) | rpl::start_with_next([=](QRect list, int addMembers, int shareLink) {
|
||||
const auto left = list.x();
|
||||
const auto top = list.y() - _topSkip->height();
|
||||
const auto right = left + list.width() - topright->width();
|
||||
|
@ -1912,6 +2159,7 @@ void Members::setupFakeRoundCorners() {
|
|||
+ list.height()
|
||||
+ _bottomSkip->height()
|
||||
+ addMembers
|
||||
+ shareLink
|
||||
- bottomleft->height();
|
||||
topleft->move(left, top);
|
||||
topright->move(right, top);
|
||||
|
|
|
@ -25,6 +25,7 @@ class GroupCall;
|
|||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
struct FingerprintBadgeState;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
@ -59,6 +60,9 @@ public:
|
|||
[[nodiscard]] rpl::producer<> addMembersRequests() const {
|
||||
return _addMemberRequests.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> shareLinkRequests() const {
|
||||
return _shareLinkRequests.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] MembersRow *lookupRow(not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] not_null<MembersRow*> rtmpFakeRow(
|
||||
|
@ -93,6 +97,7 @@ private:
|
|||
void setupAddMember(not_null<GroupCall*> call);
|
||||
void resizeToList();
|
||||
void setupList();
|
||||
void setupFingerprint();
|
||||
void setupFakeRoundCorners();
|
||||
|
||||
void trackViewportGeometry();
|
||||
|
@ -103,13 +108,18 @@ private:
|
|||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
std::unique_ptr<Controller> _listController;
|
||||
not_null<Ui::VerticalLayout*> _layout;
|
||||
Ui::RpWidget *_fingerprint = nullptr;
|
||||
rpl::event_stream<> _fingerprintRepaints;
|
||||
const FingerprintBadgeState *_fingerprintState = nullptr;
|
||||
const not_null<Ui::RpWidget*> _videoWrap;
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;
|
||||
rpl::variable<Ui::RpWidget*> _shareLinkButton = nullptr;
|
||||
RpWidget *_topSkip = nullptr;
|
||||
RpWidget *_bottomSkip = nullptr;
|
||||
ListWidget *_list = nullptr;
|
||||
rpl::event_stream<> _addMemberRequests;
|
||||
rpl::event_stream<> _shareLinkRequests;
|
||||
|
||||
mutable std::unique_ptr<MembersRow> _rtmpFakeRow;
|
||||
|
||||
|
|
|
@ -138,41 +138,52 @@ void MembersRow::setSkipLevelUpdate(bool value) {
|
|||
_skipLevelUpdate = value;
|
||||
}
|
||||
|
||||
void MembersRow::updateState(
|
||||
const Data::GroupCallParticipant *participant) {
|
||||
setVolume(participant
|
||||
? participant->volume
|
||||
: Group::kDefaultVolume);
|
||||
if (!participant) {
|
||||
setState(State::Invited);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
} else if (!participant->muted
|
||||
|| (participant->sounding && participant->ssrc != 0)
|
||||
|| (participant->additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant->videoParams) != 0)) {
|
||||
void MembersRow::updateStateInvited(bool calling) {
|
||||
setVolume(Group::kDefaultVolume);
|
||||
setState(calling ? State::Calling : State::Invited);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::updateStateWithAccess() {
|
||||
setVolume(Group::kDefaultVolume);
|
||||
setState(State::WithAccess);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::updateState(const Data::GroupCallParticipant &participant) {
|
||||
setVolume(participant.volume);
|
||||
if (!participant.muted
|
||||
|| (participant.sounding && participant.ssrc != 0)
|
||||
|| (participant.additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0)) {
|
||||
setState(State::Active);
|
||||
setSounding((participant->sounding && participant->ssrc != 0)
|
||||
|| (participant->additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant->videoParams) != 0));
|
||||
setSpeaking((participant->speaking && participant->ssrc != 0)
|
||||
|| (participant->additionalSpeaking
|
||||
&& GetAdditionalAudioSsrc(participant->videoParams) != 0));
|
||||
_mutedByMe = participant->mutedByMe;
|
||||
setSounding((participant.sounding && participant.ssrc != 0)
|
||||
|| (participant.additionalSounding
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
|
||||
setSpeaking((participant.speaking && participant.ssrc != 0)
|
||||
|| (participant.additionalSpeaking
|
||||
&& GetAdditionalAudioSsrc(participant.videoParams) != 0));
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else if (participant->canSelfUnmute) {
|
||||
} else if (participant.canSelfUnmute) {
|
||||
setState(State::Inactive);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant->mutedByMe;
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else {
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant->mutedByMe;
|
||||
_raisedHandRating = participant->raisedHandRating;
|
||||
_mutedByMe = participant.mutedByMe;
|
||||
_raisedHandRating = participant.raisedHandRating;
|
||||
setState(_raisedHandRating ? State::RaisedHand : State::Muted);
|
||||
}
|
||||
refreshStatus();
|
||||
|
@ -450,6 +461,20 @@ void MembersRow::paintMuteIcon(
|
|||
_delegate->rowPaintIcon(p, iconRect, computeIconState(style));
|
||||
}
|
||||
|
||||
QString MembersRow::generateName() {
|
||||
const auto result = peer()->name();
|
||||
return result.isEmpty()
|
||||
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
|
||||
: result;
|
||||
}
|
||||
|
||||
QString MembersRow::generateShortName() {
|
||||
const auto result = peer()->shortName();
|
||||
return result.isEmpty()
|
||||
? u"User #%1"_q.arg(peerToUser(peer()->id).bare)
|
||||
: result;
|
||||
}
|
||||
|
||||
auto MembersRow::generatePaintUserpicCallback(bool forceRound)
|
||||
-> PaintRoundImageCallback {
|
||||
return [=](Painter &p, int x, int y, int outerWidth, int size) {
|
||||
|
@ -613,11 +638,16 @@ void MembersRow::paintComplexStatusText(
|
|||
availableWidth -= skip;
|
||||
const auto &font = st::normalFont;
|
||||
const auto useAbout = !_about.isEmpty()
|
||||
&& (_state != State::WithAccess)
|
||||
&& (_state != State::Invited)
|
||||
&& (_state != State::Calling)
|
||||
&& (style != MembersRowStyle::Video)
|
||||
&& ((_state == State::RaisedHand && !_raisedHandStatus)
|
||||
|| (_state != State::RaisedHand && !_speaking));
|
||||
if (!useAbout
|
||||
&& _state != State::Invited
|
||||
&& _state != State::Calling
|
||||
&& _state != State::WithAccess
|
||||
&& !_mutedByMe) {
|
||||
paintStatusIcon(p, x, y, st, font, selected, narrowMode);
|
||||
|
||||
|
@ -663,6 +693,10 @@ void MembersRow::paintComplexStatusText(
|
|||
? tr::lng_group_call_muted_by_me_status(tr::now)
|
||||
: _delegate->rowIsMe(peer())
|
||||
? tr::lng_status_connecting(tr::now)
|
||||
: (_state == State::WithAccess)
|
||||
? tr::lng_group_call_blockchain_only_status(tr::now)
|
||||
: (_state == State::Calling)
|
||||
? tr::lng_group_call_calling_status(tr::now)
|
||||
: tr::lng_group_call_invited_status(tr::now)));
|
||||
}
|
||||
}
|
||||
|
@ -676,6 +710,7 @@ QSize MembersRow::rightActionSize() const {
|
|||
bool MembersRow::rightActionDisabled() const {
|
||||
return _delegate->rowIsMe(peer())
|
||||
|| (_state == State::Invited)
|
||||
|| (_state == State::Calling)
|
||||
|| !_delegate->rowCanMuteMembers();
|
||||
}
|
||||
|
||||
|
@ -701,7 +736,9 @@ void MembersRow::rightActionPaint(
|
|||
size.width(),
|
||||
size.height(),
|
||||
outerWidth);
|
||||
if (_state == State::Invited) {
|
||||
if (_state == State::Invited
|
||||
|| _state == State::Calling
|
||||
|| _state == State::WithAccess) {
|
||||
_actionRipple = nullptr;
|
||||
}
|
||||
if (_actionRipple) {
|
||||
|
@ -731,6 +768,7 @@ MembersRowDelegate::IconState MembersRow::computeIconState(
|
|||
.mutedByMe = _mutedByMe,
|
||||
.raisedHand = (_state == State::RaisedHand),
|
||||
.invited = (_state == State::Invited),
|
||||
.calling = (_state == State::Calling),
|
||||
.style = style,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ struct PeerUserpicView;
|
|||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class MembersRowStyle {
|
||||
enum class MembersRowStyle : uchar {
|
||||
Default,
|
||||
Narrow,
|
||||
Video,
|
||||
|
@ -40,6 +40,7 @@ public:
|
|||
bool mutedByMe = false;
|
||||
bool raisedHand = false;
|
||||
bool invited = false;
|
||||
bool calling = false;
|
||||
MembersRowStyle style = MembersRowStyle::Default;
|
||||
};
|
||||
virtual bool rowIsMe(not_null<PeerData*> participantPeer) = 0;
|
||||
|
@ -75,11 +76,15 @@ public:
|
|||
Muted,
|
||||
RaisedHand,
|
||||
Invited,
|
||||
Calling,
|
||||
WithAccess,
|
||||
};
|
||||
|
||||
void setAbout(const QString &about);
|
||||
void setSkipLevelUpdate(bool value);
|
||||
void updateState(const Data::GroupCallParticipant *participant);
|
||||
void updateState(const Data::GroupCallParticipant &participant);
|
||||
void updateStateInvited(bool calling);
|
||||
void updateStateWithAccess();
|
||||
void updateLevel(float level);
|
||||
void updateBlobAnimation(crl::time now);
|
||||
void clearRaisedHandStatus();
|
||||
|
@ -122,6 +127,8 @@ public:
|
|||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
QString generateName() override;
|
||||
QString generateShortName() override;
|
||||
PaintRoundImageCallback generatePaintUserpicCallback(
|
||||
bool forceRound) override;
|
||||
void paintComplexUserpic(
|
||||
|
|
|
@ -416,10 +416,13 @@ void LeaveBox(
|
|||
not_null<GroupCall*> call,
|
||||
bool discardChecked,
|
||||
BoxContext context) {
|
||||
const auto conference = call->conference();
|
||||
const auto livestream = call->peer()->isBroadcast();
|
||||
const auto scheduled = (call->scheduleDate() != 0);
|
||||
if (!scheduled) {
|
||||
box->setTitle(livestream
|
||||
box->setTitle(conference
|
||||
? tr::lng_group_call_leave_title_call()
|
||||
: livestream
|
||||
? tr::lng_group_call_leave_title_channel()
|
||||
: tr::lng_group_call_leave_title());
|
||||
}
|
||||
|
@ -431,12 +434,14 @@ void LeaveBox(
|
|||
? (livestream
|
||||
? tr::lng_group_call_close_sure_channel()
|
||||
: tr::lng_group_call_close_sure())
|
||||
: (livestream
|
||||
: (conference
|
||||
? tr::lng_group_call_leave_sure_call()
|
||||
: livestream
|
||||
? tr::lng_group_call_leave_sure_channel()
|
||||
: tr::lng_group_call_leave_sure())),
|
||||
(inCall ? st::groupCallBoxLabel : st::boxLabel)),
|
||||
scheduled ? st::boxPadding : st::boxRowPadding);
|
||||
const auto discard = call->peer()->canManageGroupCall()
|
||||
const auto discard = call->canManage()
|
||||
? box->addRow(object_ptr<Ui::Checkbox>(
|
||||
box.get(),
|
||||
(scheduled
|
||||
|
@ -490,20 +495,24 @@ void FillMenu(
|
|||
Fn<void(object_ptr<Ui::BoxContent>)> showBox) {
|
||||
const auto weak = base::make_weak(call);
|
||||
const auto resolveReal = [=] {
|
||||
const auto real = peer->groupCall();
|
||||
const auto strong = weak.get();
|
||||
return (real && strong && (real->id() == strong->id()))
|
||||
? real
|
||||
: nullptr;
|
||||
if (const auto strong = weak.get()) {
|
||||
if (const auto real = strong->lookupReal()) {
|
||||
return real;
|
||||
}
|
||||
}
|
||||
return (Data::GroupCall*)nullptr;
|
||||
};
|
||||
const auto real = resolveReal();
|
||||
if (!real) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto conference = call->conference();
|
||||
const auto addEditJoinAs = call->showChooseJoinAs();
|
||||
const auto addEditTitle = call->canManage();
|
||||
const auto addEditRecording = call->canManage() && !real->scheduleDate();
|
||||
const auto addEditTitle = !conference && call->canManage();
|
||||
const auto addEditRecording = !conference
|
||||
&& call->canManage()
|
||||
&& !real->scheduleDate();
|
||||
const auto addScreenCast = !wide
|
||||
&& call->videoIsWorking()
|
||||
&& !real->scheduleDate();
|
||||
|
|
|
@ -16,7 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "calls/group/calls_group_invite_controller.h"
|
||||
#include "calls/group/ui/calls_group_scheduled_labels.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "calls/calls_emoji_fingerprint.h"
|
||||
#include "calls/calls_window.h"
|
||||
#include "ui/platform/ui_platform_window_title.h" // TitleLayout
|
||||
#include "ui/platform/ui_platform_utility.h"
|
||||
#include "ui/controls/call_mute_button.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
@ -28,7 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/widgets/rp_window.h"
|
||||
#include "ui/chat/group_call_bar.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/layers/layer_manager.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
|
@ -47,12 +48,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "main/session/session_show.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/power_save_blocker.h"
|
||||
#include "apiwrap.h" // api().kick.
|
||||
#include "api/api_chat_participants.h" // api().kick.
|
||||
#include "webrtc/webrtc_environment.h"
|
||||
|
@ -76,77 +77,6 @@ constexpr auto kControlsBackgroundOpacity = 0.8;
|
|||
constexpr auto kOverrideActiveColorBgAlpha = 172;
|
||||
constexpr auto kHideControlsTimeout = 5 * crl::time(1000);
|
||||
|
||||
class Show final : public Main::SessionShow {
|
||||
public:
|
||||
explicit Show(not_null<Panel*> panel);
|
||||
~Show();
|
||||
|
||||
void showOrHideBoxOrLayer(
|
||||
std::variant<
|
||||
v::null_t,
|
||||
object_ptr<Ui::BoxContent>,
|
||||
std::unique_ptr<Ui::LayerWidget>> &&layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) const override;
|
||||
[[nodiscard]] not_null<QWidget*> toastParent() const override;
|
||||
[[nodiscard]] bool valid() const override;
|
||||
operator bool() const override;
|
||||
|
||||
[[nodiscard]] Main::Session &session() const override;
|
||||
|
||||
private:
|
||||
const base::weak_ptr<Panel> _panel;
|
||||
|
||||
};
|
||||
|
||||
Show::Show(not_null<Panel*> panel)
|
||||
: _panel(base::make_weak(panel)) {
|
||||
}
|
||||
|
||||
Show::~Show() = default;
|
||||
|
||||
void Show::showOrHideBoxOrLayer(
|
||||
std::variant<
|
||||
v::null_t,
|
||||
object_ptr<Ui::BoxContent>,
|
||||
std::unique_ptr<Ui::LayerWidget>> &&layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) const {
|
||||
using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
|
||||
using ObjectBox = object_ptr<Ui::BoxContent>;
|
||||
if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
|
||||
if (const auto panel = _panel.get()) {
|
||||
panel->showLayer(std::move(*layerWidget), options, animated);
|
||||
}
|
||||
} else if (auto box = std::get_if<ObjectBox>(&layer)) {
|
||||
if (const auto panel = _panel.get()) {
|
||||
panel->showBox(std::move(*box), options, animated);
|
||||
}
|
||||
} else if (const auto panel = _panel.get()) {
|
||||
panel->hideLayer(animated);
|
||||
}
|
||||
}
|
||||
|
||||
not_null<QWidget*> Show::toastParent() const {
|
||||
const auto panel = _panel.get();
|
||||
Assert(panel != nullptr);
|
||||
return panel->widget();
|
||||
}
|
||||
|
||||
bool Show::valid() const {
|
||||
return !_panel.empty();
|
||||
}
|
||||
|
||||
Show::operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
|
||||
Main::Session &Show::session() const {
|
||||
const auto panel = _panel.get();
|
||||
Assert(panel != nullptr);
|
||||
return panel->call()->peer()->session();
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
void UnpinMaximized(not_null<QWidget*> widget) {
|
||||
SetWindowPos(
|
||||
|
@ -177,22 +107,18 @@ struct Panel::ControlsBackgroundNarrow {
|
|||
};
|
||||
|
||||
Panel::Panel(not_null<GroupCall*> call)
|
||||
: Panel(call, ConferencePanelMigration()) {
|
||||
}
|
||||
|
||||
Panel::Panel(not_null<GroupCall*> call, ConferencePanelMigration info)
|
||||
: _call(call)
|
||||
, _peer(call->peer())
|
||||
, _layerBg(std::make_unique<Ui::LayerManager>(widget()))
|
||||
#ifndef Q_OS_MAC
|
||||
, _controls(Ui::Platform::SetupSeparateTitleControls(
|
||||
window(),
|
||||
st::groupCallTitle,
|
||||
nullptr,
|
||||
_controlsTop.value()))
|
||||
#endif // !Q_OS_MAC
|
||||
, _powerSaveBlocker(std::make_unique<base::PowerSaveBlocker>(
|
||||
base::PowerSaveBlockType::PreventDisplaySleep,
|
||||
u"Video chat is active"_q,
|
||||
window()->windowHandle()))
|
||||
, _window(info.window ? info.window : std::make_shared<Window>())
|
||||
, _viewport(
|
||||
std::make_unique<Viewport>(widget(), PanelMode::Wide, _window.backend()))
|
||||
std::make_unique<Viewport>(
|
||||
widget(),
|
||||
PanelMode::Wide,
|
||||
_window->backend()))
|
||||
, _mute(std::make_unique<Ui::CallMuteButton>(
|
||||
widget(),
|
||||
st::callMuteButton,
|
||||
|
@ -222,9 +148,6 @@ Panel::Panel(not_null<GroupCall*> call)
|
|||
return result;
|
||||
})
|
||||
, _hideControlsTimer([=] { toggleWideControls(false); }) {
|
||||
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
|
||||
_layerBg->setHideByBackgroundClick(true);
|
||||
|
||||
_viewport->widget()->hide();
|
||||
if (!_viewport->requireARGB32()) {
|
||||
_call->setNotRequireARGB32();
|
||||
|
@ -239,7 +162,7 @@ Panel::Panel(not_null<GroupCall*> call)
|
|||
initWindow();
|
||||
initWidget();
|
||||
initControls();
|
||||
initLayout();
|
||||
initLayout(info);
|
||||
showAndActivate();
|
||||
}
|
||||
|
||||
|
@ -268,25 +191,12 @@ bool Panel::isActive() const {
|
|||
return window()->isActiveWindow() && isVisible();
|
||||
}
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
|
||||
const QString &text,
|
||||
crl::time duration) {
|
||||
return Show(this).showToast(text, duration);
|
||||
std::shared_ptr<Main::SessionShow> Panel::sessionShow() {
|
||||
return Main::MakeSessionShow(uiShow(), &_peer->session());
|
||||
}
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
|
||||
TextWithEntities &&text,
|
||||
crl::time duration) {
|
||||
return Show(this).showToast(std::move(text), duration);
|
||||
}
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
|
||||
Ui::Toast::Config &&config) {
|
||||
return Show(this).showToast(std::move(config));
|
||||
}
|
||||
|
||||
std::shared_ptr<Main::SessionShow> Panel::uiShow() {
|
||||
return std::make_shared<Show>(this);
|
||||
std::shared_ptr<Ui::Show> Panel::uiShow() {
|
||||
return _window->uiShow();
|
||||
}
|
||||
|
||||
void Panel::minimize() {
|
||||
|
@ -367,14 +277,28 @@ void Panel::initWindow() {
|
|||
window()->setAttribute(Qt::WA_NoSystemBackground);
|
||||
window()->setTitleStyle(st::groupCallTitle);
|
||||
|
||||
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) {
|
||||
if (e->type() == QEvent::Close && handleClose()) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Close && handleClose()) {
|
||||
e->ignore();
|
||||
return base::EventFilterResult::Cancel;
|
||||
} else if (e->type() == QEvent::KeyPress
|
||||
|| e->type() == QEvent::KeyRelease) {
|
||||
} else if (_call->rtmp()
|
||||
&& (type == QEvent::KeyPress || type == QEvent::KeyRelease)) {
|
||||
const auto key = static_cast<QKeyEvent*>(e.get())->key();
|
||||
if (key == Qt::Key_Space) {
|
||||
_call->pushToTalk(
|
||||
|
@ -384,16 +308,19 @@ void Panel::initWindow() {
|
|||
&& _fullScreenOrMaximized.current()) {
|
||||
toggleFullScreen();
|
||||
}
|
||||
} else if (e->type() == QEvent::WindowStateChange && _call->rtmp()) {
|
||||
const auto state = window()->windowState();
|
||||
_fullScreenOrMaximized = (state & Qt::WindowFullScreen)
|
||||
|| (state & Qt::WindowMaximized);
|
||||
} else if (type == QEvent::WindowStateChange) {
|
||||
updateFullScreen();
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
}, lifetime());
|
||||
updateFullScreen();
|
||||
|
||||
const auto guard = base::make_weak(this);
|
||||
window()->setBodyTitleArea([=](QPoint widgetPoint) {
|
||||
using Flag = Ui::WindowTitleHitTestFlag;
|
||||
if (!guard) {
|
||||
return (Flag::None | Flag(0));
|
||||
}
|
||||
const auto titleRect = QRect(
|
||||
0,
|
||||
0,
|
||||
|
@ -409,7 +336,7 @@ void Panel::initWindow() {
|
|||
if (!moveable) {
|
||||
return (Flag::None | Flag(0));
|
||||
}
|
||||
const auto shown = _layerBg->topShownLayer();
|
||||
const auto shown = _window->topShownLayer();
|
||||
return (!shown || !shown->geometry().contains(widgetPoint))
|
||||
? (Flag::Move | Flag::Menu | Flag::Maximize)
|
||||
: Flag::None;
|
||||
|
@ -419,6 +346,25 @@ void Panel::initWindow() {
|
|||
) | rpl::start_with_next([=] {
|
||||
updateMode();
|
||||
}, lifetime());
|
||||
|
||||
_window->maximizeRequests() | rpl::start_with_next([=](bool maximized) {
|
||||
if (_call->rtmp()) {
|
||||
toggleFullScreen(maximized);
|
||||
} else {
|
||||
window()->setWindowState(maximized
|
||||
? Qt::WindowMaximized
|
||||
: Qt::WindowNoState);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_window->showingLayer() | rpl::start_with_next([=] {
|
||||
hideStickedTooltip(StickedTooltipHide::Unavailable);
|
||||
}, lifetime());
|
||||
|
||||
_window->setControlsStyle(st::groupCallTitle);
|
||||
_window->togglePowerSaveBlocker(true);
|
||||
|
||||
uiShow()->hideLayer(anim::type::instant);
|
||||
}
|
||||
|
||||
void Panel::initWidget() {
|
||||
|
@ -437,7 +383,7 @@ void Panel::initWidget() {
|
|||
|
||||
// some geometries depends on _controls->controls.geometry,
|
||||
// which is not updated here yet.
|
||||
crl::on_main(widget(), [=] { updateControlsGeometry(); });
|
||||
crl::on_main(this, [=] { updateControlsGeometry(); });
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
|
@ -446,7 +392,7 @@ void Panel::endCall() {
|
|||
_call->hangup();
|
||||
return;
|
||||
}
|
||||
showBox(Box(
|
||||
uiShow()->showBox(Box(
|
||||
LeaveBox,
|
||||
_call,
|
||||
false,
|
||||
|
@ -476,7 +422,7 @@ void Panel::startScheduledNow() {
|
|||
.confirmText = tr::lng_group_call_start_now(),
|
||||
});
|
||||
*box = owned.data();
|
||||
showBox(std::move(owned));
|
||||
uiShow()->showBox(std::move(owned));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -500,7 +446,9 @@ void Panel::initControls() {
|
|||
|
||||
const auto oldState = _call->muted();
|
||||
const auto newState = (oldState == MuteState::ForceMuted)
|
||||
? MuteState::RaisedHand
|
||||
? (_call->conference()
|
||||
? MuteState::ForceMuted
|
||||
: MuteState::RaisedHand)
|
||||
: (oldState == MuteState::RaisedHand)
|
||||
? MuteState::RaisedHand
|
||||
: (oldState == MuteState::Muted)
|
||||
|
@ -583,10 +531,15 @@ void Panel::initControls() {
|
|||
}
|
||||
|
||||
void Panel::toggleFullScreen() {
|
||||
if (_fullScreenOrMaximized.current() || window()->isFullScreen()) {
|
||||
window()->showNormal();
|
||||
} else {
|
||||
toggleFullScreen(
|
||||
!_fullScreenOrMaximized.current() && !window()->isFullScreen());
|
||||
}
|
||||
|
||||
void Panel::toggleFullScreen(bool fullscreen) {
|
||||
if (fullscreen) {
|
||||
window()->showFullScreen();
|
||||
} else {
|
||||
window()->showNormal();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -605,7 +558,7 @@ void Panel::refreshLeftButton() {
|
|||
_callShare.destroy();
|
||||
_settings.create(widget(), st::groupCallSettings);
|
||||
_settings->setClickedCallback([=] {
|
||||
showBox(Box(SettingsBox, _call));
|
||||
uiShow()->showBox(Box(SettingsBox, _call));
|
||||
});
|
||||
trackControls(_trackControls, true);
|
||||
}
|
||||
|
@ -795,7 +748,9 @@ void Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {
|
|||
: state == GroupCall::InstanceState::Disconnected
|
||||
? Type::Connecting
|
||||
: mute == MuteState::ForceMuted
|
||||
? Type::ForceMuted
|
||||
? (_call->conference()
|
||||
? Type::ConferenceForceMuted
|
||||
: Type::ForceMuted)
|
||||
: mute == MuteState::RaisedHand
|
||||
? Type::RaisedHand
|
||||
: mute == MuteState::Muted
|
||||
|
@ -890,13 +845,13 @@ void Panel::setupMembers() {
|
|||
_countdown.destroy();
|
||||
_startsWhen.destroy();
|
||||
|
||||
_members.create(widget(), _call, mode(), _window.backend());
|
||||
_members.create(widget(), _call, mode(), _window->backend());
|
||||
|
||||
setupVideo(_viewport.get());
|
||||
setupVideo(_members->viewport());
|
||||
_viewport->mouseInsideValue(
|
||||
) | rpl::filter([=] {
|
||||
return !_fullScreenOrMaximized.current();
|
||||
return !_rtmpFull;
|
||||
}) | rpl::start_with_next([=](bool inside) {
|
||||
toggleWideControls(inside);
|
||||
}, _viewport->lifetime());
|
||||
|
@ -914,16 +869,12 @@ void Panel::setupMembers() {
|
|||
|
||||
_members->toggleMuteRequests(
|
||||
) | rpl::start_with_next([=](MuteRequest request) {
|
||||
if (_call) {
|
||||
_call->toggleMute(request);
|
||||
}
|
||||
_call->toggleMute(request);
|
||||
}, _callLifetime);
|
||||
|
||||
_members->changeVolumeRequests(
|
||||
) | rpl::start_with_next([=](VolumeRequest request) {
|
||||
if (_call) {
|
||||
_call->changeVolume(request);
|
||||
}
|
||||
_call->changeVolume(request);
|
||||
}, _callLifetime);
|
||||
|
||||
_members->kickParticipantRequests(
|
||||
|
@ -933,7 +884,9 @@ void Panel::setupMembers() {
|
|||
|
||||
_members->addMembersRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (!_peer->isBroadcast()
|
||||
if (_call->conference()) {
|
||||
addMembers();
|
||||
} else if (!_peer->isBroadcast()
|
||||
&& Data::CanSend(_peer, ChatRestriction::SendOther, false)
|
||||
&& _call->joinAs()->isSelf()) {
|
||||
addMembers();
|
||||
|
@ -944,6 +897,9 @@ void Panel::setupMembers() {
|
|||
}
|
||||
}, _callLifetime);
|
||||
|
||||
_members->shareLinkRequests(
|
||||
) | rpl::start_with_next(shareConferenceLinkCallback(), _callLifetime);
|
||||
|
||||
_call->videoEndpointLargeValue(
|
||||
) | rpl::start_with_next([=](const VideoEndpoint &large) {
|
||||
if (large && mode() != PanelMode::Wide) {
|
||||
|
@ -953,6 +909,30 @@ void Panel::setupMembers() {
|
|||
}, _callLifetime);
|
||||
}
|
||||
|
||||
Fn<void()> Panel::shareConferenceLinkCallback() {
|
||||
return [=] {
|
||||
Expects(_call->conference());
|
||||
|
||||
ShowConferenceCallLinkBox(sessionShow(), _call->conferenceCall(), {
|
||||
.st = DarkConferenceCallLinkStyle(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void Panel::migrationShowShareLink() {
|
||||
ShowConferenceCallLinkBox(
|
||||
sessionShow(),
|
||||
_call->conferenceCall(),
|
||||
{ .st = DarkConferenceCallLinkStyle() });
|
||||
}
|
||||
|
||||
void Panel::migrationInviteUsers(std::vector<InviteRequest> users) {
|
||||
const auto done = [=](InviteResult result) {
|
||||
uiShow()->showToast({ ComposeInviteResultToast(result) });
|
||||
};
|
||||
_call->inviteUsers(std::move(users), crl::guard(this, done));
|
||||
}
|
||||
|
||||
void Panel::enlargeVideo() {
|
||||
_lastSmallGeometry = window()->geometry();
|
||||
|
||||
|
@ -1035,7 +1015,7 @@ void Panel::raiseControls() {
|
|||
if (_pinOnTop) {
|
||||
_pinOnTop->raise();
|
||||
}
|
||||
_layerBg->raise();
|
||||
_window->raiseLayers();
|
||||
if (_niceTooltip) {
|
||||
_niceTooltip->raise();
|
||||
}
|
||||
|
@ -1113,7 +1093,7 @@ void Panel::toggleWideControls(bool shown) {
|
|||
return;
|
||||
}
|
||||
_showWideControls = shown;
|
||||
crl::on_main(widget(), [=] {
|
||||
crl::on_main(this, [=] {
|
||||
updateWideControlsVisibility();
|
||||
});
|
||||
}
|
||||
|
@ -1124,7 +1104,7 @@ void Panel::updateWideControlsVisibility() {
|
|||
if (_wideControlsShown == shown) {
|
||||
return;
|
||||
}
|
||||
_viewport->setCursorShown(!_fullScreenOrMaximized.current() || shown);
|
||||
_viewport->setCursorShown(!_rtmpFull || shown);
|
||||
_wideControlsShown = shown;
|
||||
_wideControlsAnimation.start(
|
||||
[=] { updateButtonsGeometry(); },
|
||||
|
@ -1151,7 +1131,7 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
|||
const auto skip = st::groupCallRecordingMarkSkip;
|
||||
_recordingMark->resize(size + 2 * skip, size + 2 * skip);
|
||||
_recordingMark->setClickedCallback([=] {
|
||||
showToast({ (livestream
|
||||
uiShow()->showToast({ (livestream
|
||||
? tr::lng_group_call_is_recorded_channel
|
||||
: real->recordVideo()
|
||||
? tr::lng_group_call_is_recorded_video
|
||||
|
@ -1197,7 +1177,7 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
|||
*startedAsVideo = isVideo;
|
||||
}
|
||||
validateRecordingMark(recorded);
|
||||
showToast((recorded
|
||||
uiShow()->showToast((recorded
|
||||
? (livestream
|
||||
? tr::lng_group_call_recording_started_channel
|
||||
: isVideo
|
||||
|
@ -1258,7 +1238,7 @@ void Panel::createPinOnTop() {
|
|||
pin ? &st::groupCallPinnedOnTop : nullptr,
|
||||
pin ? &st::groupCallPinnedOnTop : nullptr);
|
||||
if (!_pinOnTop->isHidden()) {
|
||||
showToast({ pin
|
||||
uiShow()->showToast({ pin
|
||||
? tr::lng_group_call_pinned_on_top(tr::now)
|
||||
: tr::lng_group_call_unpinned_on_top(tr::now) });
|
||||
}
|
||||
|
@ -1266,11 +1246,9 @@ void Panel::createPinOnTop() {
|
|||
};
|
||||
_fullScreenOrMaximized.value(
|
||||
) | rpl::start_with_next([=](bool fullScreenOrMaximized) {
|
||||
#ifndef Q_OS_MAC
|
||||
_controls->controls.setStyle(fullScreenOrMaximized
|
||||
_window->setControlsStyle(fullScreenOrMaximized
|
||||
? st::callTitle
|
||||
: st::groupCallTitle);
|
||||
#endif // Q_OS_MAC
|
||||
|
||||
_pinOnTop->setVisible(!fullScreenOrMaximized);
|
||||
if (fullScreenOrMaximized) {
|
||||
|
@ -1360,7 +1338,7 @@ void Panel::refreshTopButton() {
|
|||
|
||||
void Panel::screenSharingPrivacyRequest() {
|
||||
if (auto box = ScreenSharingPrivacyRequestBox()) {
|
||||
showBox(std::move(box));
|
||||
uiShow()->showBox(std::move(box));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1411,7 +1389,7 @@ void Panel::chooseShareScreenSource() {
|
|||
.confirmText = tr::lng_continue(),
|
||||
});
|
||||
*shared = box.data();
|
||||
showBox(std::move(box));
|
||||
uiShow()->showBox(std::move(box));
|
||||
}
|
||||
|
||||
void Panel::chooseJoinAs() {
|
||||
|
@ -1422,7 +1400,7 @@ void Panel::chooseJoinAs() {
|
|||
_joinAsProcess.start(
|
||||
_peer,
|
||||
context,
|
||||
std::make_shared<Show>(this),
|
||||
uiShow(),
|
||||
callback,
|
||||
_call->joinAs());
|
||||
}
|
||||
|
@ -1443,7 +1421,7 @@ void Panel::showMainMenu() {
|
|||
wide,
|
||||
[=] { chooseJoinAs(); },
|
||||
[=] { chooseShareScreenSource(); },
|
||||
[=](auto box) { showBox(std::move(box)); });
|
||||
[=](auto box) { uiShow()->showBox(std::move(box)); });
|
||||
if (_menu->empty()) {
|
||||
_wideMenuShown = false;
|
||||
_menu.destroy();
|
||||
|
@ -1505,16 +1483,25 @@ void Panel::showMainMenu() {
|
|||
}
|
||||
|
||||
void Panel::addMembers() {
|
||||
const auto &appConfig = _call->peer()->session().appConfig();
|
||||
const auto conferenceLimit = appConfig.confcallSizeLimit();
|
||||
if (_call->conference()
|
||||
&& _call->conferenceCall()->fullCount() >= conferenceLimit) {
|
||||
uiShow()->showToast({ tr::lng_group_call_invite_limit(tr::now) });
|
||||
}
|
||||
const auto showToastCallback = [=](TextWithEntities &&text) {
|
||||
showToast(std::move(text));
|
||||
uiShow()->showToast(std::move(text));
|
||||
};
|
||||
if (auto box = PrepareInviteBox(_call, showToastCallback)) {
|
||||
showBox(std::move(box));
|
||||
const auto link = _call->conference()
|
||||
? shareConferenceLinkCallback()
|
||||
: nullptr;
|
||||
if (auto box = PrepareInviteBox(_call, showToastCallback, link)) {
|
||||
uiShow()->showBox(std::move(box));
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
|
||||
showBox(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
|
@ -1525,7 +1512,9 @@ void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
|
|||
tr::now,
|
||||
lt_channel,
|
||||
participantPeer->name())
|
||||
: (_peer->isBroadcast()
|
||||
: (_call->conference()
|
||||
? tr::lng_confcall_sure_remove
|
||||
: _peer->isBroadcast()
|
||||
? tr::lng_profile_sure_kick_channel
|
||||
: tr::lng_profile_sure_kick)(
|
||||
tr::now,
|
||||
|
@ -1545,48 +1534,12 @@ void Panel::kickParticipant(not_null<PeerData*> participantPeer) {
|
|||
}));
|
||||
}
|
||||
|
||||
void Panel::showBox(object_ptr<Ui::BoxContent> box) {
|
||||
showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal);
|
||||
}
|
||||
|
||||
void Panel::showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) {
|
||||
hideStickedTooltip(StickedTooltipHide::Unavailable);
|
||||
if (window()->width() < st::groupCallWidth
|
||||
|| window()->height() < st::groupCallWidth) {
|
||||
window()->resize(
|
||||
std::max(window()->width(), st::groupCallWidth),
|
||||
std::max(window()->height(), st::groupCallWidth));
|
||||
}
|
||||
_layerBg->showBox(std::move(box), options, animated);
|
||||
}
|
||||
|
||||
void Panel::showLayer(
|
||||
std::unique_ptr<Ui::LayerWidget> layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) {
|
||||
hideStickedTooltip(StickedTooltipHide::Unavailable);
|
||||
if (window()->width() < st::groupCallWidth
|
||||
|| window()->height() < st::groupCallWidth) {
|
||||
window()->resize(
|
||||
std::max(window()->width(), st::groupCallWidth),
|
||||
std::max(window()->height(), st::groupCallWidth));
|
||||
}
|
||||
_layerBg->showLayer(std::move(layer), options, animated);
|
||||
}
|
||||
|
||||
void Panel::hideLayer(anim::type animated) {
|
||||
_layerBg->hideAll(animated);
|
||||
}
|
||||
|
||||
bool Panel::isLayerShown() const {
|
||||
return _layerBg->topShownLayer() != nullptr;
|
||||
}
|
||||
|
||||
void Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {
|
||||
if (const auto chat = _peer->asChat()) {
|
||||
if (_call->conference()) {
|
||||
if (const auto user = participantPeer->asUser()) {
|
||||
_call->removeConferenceParticipants({ peerToUser(user->id) });
|
||||
}
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
chat->session().api().chatParticipants().kick(chat, participantPeer);
|
||||
} else if (const auto channel = _peer->asChannel()) {
|
||||
const auto currentRestrictedRights = [&] {
|
||||
|
@ -1606,20 +1559,19 @@ void Panel::kickParticipantSure(not_null<PeerData*> participantPeer) {
|
|||
}
|
||||
}
|
||||
|
||||
void Panel::initLayout() {
|
||||
initGeometry();
|
||||
void Panel::initLayout(ConferencePanelMigration info) {
|
||||
initGeometry(info);
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
_controls->wrap.raise();
|
||||
_window->raiseControls();
|
||||
|
||||
_controls->controls.layout().changes(
|
||||
_window->controlsLayoutChanges(
|
||||
) | rpl::start_with_next([=] {
|
||||
// _menuToggle geometry depends on _controls arrangement.
|
||||
crl::on_main(widget(), [=] { updateControlsGeometry(); });
|
||||
crl::on_main(this, [=] { updateControlsGeometry(); });
|
||||
}, lifetime());
|
||||
|
||||
raiseControls();
|
||||
#endif // !Q_OS_MAC
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void Panel::showControls() {
|
||||
|
@ -1634,25 +1586,27 @@ void Panel::closeBeforeDestroy() {
|
|||
}
|
||||
|
||||
rpl::lifetime &Panel::lifetime() {
|
||||
return window()->lifetime();
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
void Panel::initGeometry() {
|
||||
const auto center = Core::App().getPointForCallPanelCenter();
|
||||
const auto width = _call->rtmp()
|
||||
? st::groupCallWidthRtmp
|
||||
: st::groupCallWidth;
|
||||
const auto height = _call->rtmp()
|
||||
? st::groupCallHeightRtmp
|
||||
: st::groupCallHeight;
|
||||
void Panel::initGeometry(ConferencePanelMigration info) {
|
||||
const auto minWidth = _call->rtmp()
|
||||
? st::groupCallWidthRtmpMin
|
||||
: st::groupCallWidth;
|
||||
const auto minHeight = _call->rtmp()
|
||||
? st::groupCallHeightRtmpMin
|
||||
: st::groupCallHeight;
|
||||
const auto rect = QRect(0, 0, width, height);
|
||||
window()->setGeometry(rect.translated(center - rect.center()));
|
||||
if (!info.window) {
|
||||
const auto center = Core::App().getPointForCallPanelCenter();
|
||||
const auto width = _call->rtmp()
|
||||
? st::groupCallWidthRtmp
|
||||
: st::groupCallWidth;
|
||||
const auto height = _call->rtmp()
|
||||
? st::groupCallHeightRtmp
|
||||
: st::groupCallHeight;
|
||||
const auto rect = QRect(0, 0, width, height);
|
||||
window()->setGeometry(rect.translated(center - rect.center()));
|
||||
}
|
||||
window()->setMinimumSize({ minWidth, minHeight });
|
||||
window()->show();
|
||||
}
|
||||
|
@ -1673,7 +1627,7 @@ QRect Panel::computeTitleRect() const {
|
|||
#ifdef Q_OS_MAC
|
||||
return QRect(70, 0, width - remove - 70, 28);
|
||||
#else // Q_OS_MAC
|
||||
const auto controls = _controls->controls.geometry();
|
||||
const auto controls = _window->controlsGeometry();
|
||||
const auto right = controls.x() + controls.width() + skip;
|
||||
return (controls.center().x() < width / 2)
|
||||
? QRect(right, 0, width - right - remove, controls.height())
|
||||
|
@ -1835,7 +1789,7 @@ void Panel::refreshControlsBackground() {
|
|||
}
|
||||
|
||||
void Panel::refreshTitleBackground() {
|
||||
if (!_fullScreenOrMaximized.current()) {
|
||||
if (!_rtmpFull) {
|
||||
_titleBackground.destroy();
|
||||
return;
|
||||
} else if (_titleBackground) {
|
||||
|
@ -1980,7 +1934,7 @@ void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) {
|
|||
}
|
||||
|
||||
void Panel::trackControlOver(not_null<Ui::RpWidget*> control, bool over) {
|
||||
if (_fullScreenOrMaximized.current()) {
|
||||
if (_rtmpFull) {
|
||||
return;
|
||||
} else if (_stickedTooltipClose) {
|
||||
if (!over) {
|
||||
|
@ -2021,7 +1975,7 @@ void Panel::showStickedTooltip() {
|
|||
&& callReady
|
||||
&& _mute
|
||||
&& !_call->mutedByAdmin()
|
||||
&& !_layerBg->topShownLayer()) {
|
||||
&& !_window->topShownLayer()) {
|
||||
if (_stickedTooltipClose) {
|
||||
// Showing already.
|
||||
return;
|
||||
|
@ -2224,10 +2178,10 @@ void Panel::updateControlsGeometry() {
|
|||
const auto controlsOnTheLeft = true;
|
||||
const auto controlsPadding = 0;
|
||||
#else // Q_OS_MAC
|
||||
const auto center = _controls->controls.geometry().center();
|
||||
const auto center = _window->controlsGeometry().center();
|
||||
const auto controlsOnTheLeft = center.x()
|
||||
< widget()->width() / 2;
|
||||
const auto controlsPadding = _controls->wrap.y();
|
||||
const auto controlsPadding = _window->controlsWrapTop();
|
||||
#endif // Q_OS_MAC
|
||||
const auto menux = st::groupCallMenuTogglePosition.x();
|
||||
const auto menuy = st::groupCallMenuTogglePosition.y();
|
||||
|
@ -2335,7 +2289,7 @@ void Panel::updateButtonsGeometry() {
|
|||
_controlsBackgroundWide->setGeometry(
|
||||
rect.marginsAdded(st::groupCallControlsBackMargin));
|
||||
}
|
||||
if (_fullScreenOrMaximized.current()) {
|
||||
if (_rtmpFull) {
|
||||
refreshTitleGeometry();
|
||||
}
|
||||
} else {
|
||||
|
@ -2403,10 +2357,9 @@ void Panel::updateMembersGeometry() {
|
|||
_members->setVisible(!_call->rtmp());
|
||||
const auto desiredHeight = _members->desiredHeight();
|
||||
if (mode() == PanelMode::Wide) {
|
||||
const auto full = _fullScreenOrMaximized.current();
|
||||
const auto skip = full ? 0 : st::groupCallNarrowSkip;
|
||||
const auto skip = _rtmpFull ? 0 : st::groupCallNarrowSkip;
|
||||
const auto membersWidth = st::groupCallNarrowMembersWidth;
|
||||
const auto top = full ? 0 : st::groupCallWideVideoTop;
|
||||
const auto top = _rtmpFull ? 0 : st::groupCallWideVideoTop;
|
||||
_members->setGeometry(
|
||||
widget()->width() - skip - membersWidth,
|
||||
top,
|
||||
|
@ -2415,7 +2368,7 @@ void Panel::updateMembersGeometry() {
|
|||
const auto viewportSkip = _call->rtmp()
|
||||
? 0
|
||||
: (skip + membersWidth);
|
||||
_viewport->setGeometry(full, {
|
||||
_viewport->setGeometry(_rtmpFull, {
|
||||
skip,
|
||||
top,
|
||||
widget()->width() - viewportSkip - 2 * skip,
|
||||
|
@ -2445,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() {
|
||||
if (!_title) {
|
||||
auto text = 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;
|
||||
}) | rpl::after_next([=] {
|
||||
auto text = titleText() | rpl::after_next([=] {
|
||||
refreshTitleGeometry();
|
||||
});
|
||||
_title.create(
|
||||
|
@ -2557,9 +2517,8 @@ void Panel::refreshTitleGeometry() {
|
|||
? st::groupCallTitleTop
|
||||
: (st::groupCallWideVideoTop
|
||||
- st::groupCallTitleLabel.style.font->height) / 2;
|
||||
const auto shown = _fullScreenOrMaximized.current()
|
||||
? _wideControlsAnimation.value(
|
||||
_wideControlsShown ? 1. : 0.)
|
||||
const auto shown = _rtmpFull
|
||||
? _wideControlsAnimation.value(_wideControlsShown ? 1. : 0.)
|
||||
: 1.;
|
||||
const auto top = anim::interpolate(
|
||||
-_title->height() - st::boxRadius,
|
||||
|
@ -2623,10 +2582,7 @@ void Panel::refreshTitleGeometry() {
|
|||
} else {
|
||||
layout(left + titleRect.width() - best);
|
||||
}
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
_controlsTop = anim::interpolate(-_controls->wrap.height(), 0, shown);
|
||||
#endif // Q_OS_MAC
|
||||
_window->setControlsShown(shown);
|
||||
}
|
||||
|
||||
void Panel::refreshTitleColors() {
|
||||
|
@ -2663,11 +2619,11 @@ bool Panel::handleClose() {
|
|||
}
|
||||
|
||||
not_null<Ui::RpWindow*> Panel::window() const {
|
||||
return _window.window();
|
||||
return _window->window();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> Panel::widget() const {
|
||||
return _window.widget();
|
||||
return _window->widget();
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
|
|
|
@ -7,32 +7,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/flags.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/gl/gl_window.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class Image;
|
||||
|
||||
namespace base {
|
||||
class PowerSaveBlocker;
|
||||
} // namespace base
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class LayerWidget;
|
||||
enum class LayerOption;
|
||||
|
@ -45,13 +39,13 @@ class CallMuteButton;
|
|||
class IconButton;
|
||||
class FlatLabel;
|
||||
class RpWidget;
|
||||
class RpWindow;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
template <typename Widget>
|
||||
class PaddingWrap;
|
||||
class ScrollArea;
|
||||
class GenericBox;
|
||||
class LayerManager;
|
||||
class GroupCallScheduledLeft;
|
||||
} // namespace Ui
|
||||
|
||||
|
@ -60,19 +54,17 @@ class Instance;
|
|||
struct Config;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace Ui::Platform {
|
||||
struct SeparateTitleControls;
|
||||
} // namespace Ui::Platform
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace style {
|
||||
struct CallSignalBars;
|
||||
struct CallBodyLayout;
|
||||
} // namespace style
|
||||
|
||||
namespace Calls {
|
||||
struct InviteRequest;
|
||||
struct ConferencePanelMigration;
|
||||
class Window;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Toasts;
|
||||
|
@ -86,7 +78,8 @@ class Panel final
|
|||
: public base::has_weak_ptr
|
||||
, private Ui::DesktopCapture::ChooseSourceDelegate {
|
||||
public:
|
||||
Panel(not_null<GroupCall*> call);
|
||||
explicit Panel(not_null<GroupCall*> call);
|
||||
Panel(not_null<GroupCall*> call, ConferencePanelMigration info);
|
||||
~Panel();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
|
@ -94,34 +87,20 @@ public:
|
|||
[[nodiscard]] bool isVisible() const;
|
||||
[[nodiscard]] bool isActive() const;
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> showToast(
|
||||
const QString &text,
|
||||
crl::time duration = 0);
|
||||
base::weak_ptr<Ui::Toast::Instance> showToast(
|
||||
TextWithEntities &&text,
|
||||
crl::time duration = 0);
|
||||
base::weak_ptr<Ui::Toast::Instance> showToast(
|
||||
Ui::Toast::Config &&config);
|
||||
|
||||
void showBox(object_ptr<Ui::BoxContent> box);
|
||||
void showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated = anim::type::normal);
|
||||
void showLayer(
|
||||
std::unique_ptr<Ui::LayerWidget> layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated = anim::type::normal);
|
||||
void hideLayer(anim::type animated = anim::type::normal);
|
||||
[[nodiscard]] bool isLayerShown() const;
|
||||
void migrationShowShareLink();
|
||||
void migrationInviteUsers(std::vector<InviteRequest> users);
|
||||
|
||||
void minimize();
|
||||
void toggleFullScreen();
|
||||
void toggleFullScreen(bool fullscreen);
|
||||
void close();
|
||||
void showAndActivate();
|
||||
void closeBeforeDestroy();
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();
|
||||
[[nodiscard]] std::shared_ptr<Main::SessionShow> sessionShow();
|
||||
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
|
||||
|
@ -139,8 +118,6 @@ private:
|
|||
Discarded,
|
||||
};
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
|
||||
[[nodiscard]] PanelMode mode() const;
|
||||
|
||||
void paint(QRect clip);
|
||||
|
@ -149,12 +126,13 @@ private:
|
|||
void initWidget();
|
||||
void initControls();
|
||||
void initShareAction();
|
||||
void initLayout();
|
||||
void initGeometry();
|
||||
void initLayout(ConferencePanelMigration info);
|
||||
void initGeometry(ConferencePanelMigration info);
|
||||
void setupScheduledLabels(rpl::producer<TimeId> date);
|
||||
void setupMembers();
|
||||
void setupVideo(not_null<Viewport*> viewport);
|
||||
void setupRealMuteButtonState(not_null<Data::GroupCall*> real);
|
||||
[[nodiscard]] rpl::producer<QString> titleText();
|
||||
|
||||
bool handleClose();
|
||||
void startScheduledNow();
|
||||
|
@ -192,6 +170,7 @@ private:
|
|||
void toggleWideControls(bool shown);
|
||||
void updateWideControlsVisibility();
|
||||
[[nodiscard]] bool videoButtonInNarrowMode() const;
|
||||
[[nodiscard]] Fn<void()> shareConferenceLinkCallback();
|
||||
|
||||
void endCall();
|
||||
|
||||
|
@ -225,18 +204,11 @@ private:
|
|||
const not_null<GroupCall*> _call;
|
||||
not_null<PeerData*> _peer;
|
||||
|
||||
Ui::GL::Window _window;
|
||||
const std::unique_ptr<Ui::LayerManager> _layerBg;
|
||||
std::shared_ptr<Window> _window;
|
||||
rpl::variable<PanelMode> _mode;
|
||||
rpl::variable<bool> _fullScreenOrMaximized = false;
|
||||
bool _unpinnedMaximized = false;
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
rpl::variable<int> _controlsTop = 0;
|
||||
const std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
|
||||
#endif // !Q_OS_MAC
|
||||
|
||||
const std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
|
||||
bool _rtmpFull = false;
|
||||
|
||||
rpl::lifetime _callLifetime;
|
||||
|
||||
|
@ -293,6 +265,7 @@ private:
|
|||
rpl::lifetime _hideControlsTimerLifetime;
|
||||
|
||||
rpl::lifetime _peerLifetime;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -766,7 +766,7 @@ void SettingsBox(
|
|||
}, volumeItem->lifetime());
|
||||
}
|
||||
|
||||
if (peer->canManageGroupCall()) {
|
||||
if (call->canManage()) {
|
||||
layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
(peer->isBroadcast()
|
||||
|
|
|
@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "calls/group/calls_group_panel.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "lang/lang_keys.h"
|
||||
|
@ -49,7 +50,7 @@ void Toasts::setupJoinAsChanged() {
|
|||
return (state == State::Joined);
|
||||
}) | rpl::take(1);
|
||||
}) | rpl::flatten_latest() | rpl::start_with_next([=] {
|
||||
_panel->showToast((_call->peer()->isBroadcast()
|
||||
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
|
||||
? tr::lng_group_call_join_as_changed_channel
|
||||
: tr::lng_group_call_join_as_changed)(
|
||||
tr::now,
|
||||
|
@ -69,7 +70,7 @@ void Toasts::setupTitleChanged() {
|
|||
? peer->name()
|
||||
: peer->groupCall()->title();
|
||||
}) | rpl::start_with_next([=](const QString &title) {
|
||||
_panel->showToast((_call->peer()->isBroadcast()
|
||||
_panel->uiShow()->showToast((_call->peer()->isBroadcast()
|
||||
? tr::lng_group_call_title_changed_channel
|
||||
: tr::lng_group_call_title_changed)(
|
||||
tr::now,
|
||||
|
@ -83,7 +84,8 @@ void Toasts::setupAllowedToSpeak() {
|
|||
_call->allowedToSpeakNotifications(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (_panel->isActive()) {
|
||||
_panel->showToast(tr::lng_group_call_can_speak_here(tr::now));
|
||||
_panel->uiShow()->showToast(
|
||||
tr::lng_group_call_can_speak_here(tr::now));
|
||||
} else {
|
||||
const auto real = _call->lookupReal();
|
||||
const auto name = (real && !real->title().isEmpty())
|
||||
|
@ -137,7 +139,7 @@ void Toasts::setupPinnedVideo() {
|
|||
: tr::lng_group_call_unpinned_screen);
|
||||
return key(tr::now, lt_user, peer->shortName());
|
||||
}();
|
||||
_panel->showToast(text);
|
||||
_panel->uiShow()->showToast(text);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
|
@ -146,7 +148,7 @@ void Toasts::setupRequestedToSpeak() {
|
|||
) | rpl::combine_previous(
|
||||
) | rpl::start_with_next([=](MuteState was, MuteState now) {
|
||||
if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {
|
||||
_panel->showToast(
|
||||
_panel->uiShow()->showToast(
|
||||
tr::lng_group_call_tooltip_raised_hand(tr::now));
|
||||
}
|
||||
}, _lifetime);
|
||||
|
@ -173,7 +175,7 @@ void Toasts::setupError() {
|
|||
}
|
||||
Unexpected("Error in Calls::Group::Toasts::setupErrorToasts.");
|
||||
}();
|
||||
_panel->showToast({ key(tr::now) }, kErrorDuration);
|
||||
_panel->uiShow()->showToast({ key(tr::now) }, kErrorDuration);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
|
|
|
@ -2108,7 +2108,7 @@ void EmojiListWidget::colorChosen(EmojiChosen data) {
|
|||
|
||||
const auto emoji = data.emoji;
|
||||
auto &settings = Core::App().settings();
|
||||
if (const auto button = std::get_if<OverButton>(&_pickerSelected)) {
|
||||
if (v::is<OverButton>(_pickerSelected)) {
|
||||
settings.saveAllEmojiVariants(emoji);
|
||||
for (auto section = int(Section::People)
|
||||
; section < _staticCount
|
||||
|
@ -2433,7 +2433,7 @@ Ui::Text::CustomEmoji *EmojiListWidget::resolveCustomRecent(
|
|||
const auto &data = customId.data;
|
||||
if (const auto document = std::get_if<RecentEmojiDocument>(&data)) {
|
||||
return resolveCustomRecent(document->id);
|
||||
} else if (const auto emoji = std::get_if<EmojiPtr>(&data)) {
|
||||
} else if (v::is<EmojiPtr>(data)) {
|
||||
return nullptr;
|
||||
}
|
||||
Unexpected("Custom recent emoji id.");
|
||||
|
|
|
@ -765,7 +765,7 @@ InlineBotQuery ParseInlineBotQuery(
|
|||
result.username = username.toString();
|
||||
if (const auto peer = session->data().peerByUsername(result.username)) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
result.bot = peer->asUser();
|
||||
result.bot = user;
|
||||
} else {
|
||||
result.bot = nullptr;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "base/qthelp_url.h"
|
||||
#include "boxes/premium_limits_box.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/location_picker.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
#include <QtCore/QStandardPaths>
|
||||
|
@ -329,6 +330,8 @@ void Application::run() {
|
|||
|
||||
// Check now to avoid re-entrance later.
|
||||
[[maybe_unused]] const auto ivSupported = Iv::ShowButton();
|
||||
[[maybe_unused]] const auto lpAvailable = Ui::LocationPicker::Available(
|
||||
{});
|
||||
|
||||
_windows.emplace(nullptr, std::make_unique<Window::Controller>());
|
||||
setLastActiveWindow(_windows.front().second.get());
|
||||
|
|
251
Telegram/SourceFiles/core/bank_card_click_handler.cpp
Normal 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;
|
||||
}
|
30
Telegram/SourceFiles/core/bank_card_click_handler.h
Normal 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;
|
||||
|
||||
};
|
|
@ -315,6 +315,8 @@ CloudPasswordState ParseCloudPasswordState(
|
|||
ParseSecureSecretAlgo(data.vnew_secure_algo()));
|
||||
result.unconfirmedPattern = qs(
|
||||
data.vemail_unconfirmed_pattern().value_or_empty());
|
||||
result.loginEmailPattern = qs(
|
||||
data.vlogin_email_pattern().value_or_empty());
|
||||
result.pendingResetDate = data.vpending_reset_date().value_or_empty();
|
||||
|
||||
result.outdatedClient = [&] {
|
||||
|
|
|
@ -135,6 +135,7 @@ struct CloudPasswordState {
|
|||
bool outdatedClient = false;
|
||||
QString hint;
|
||||
QString unconfirmedPattern;
|
||||
QString loginEmailPattern;
|
||||
TimeId pendingResetDate = 0;
|
||||
};
|
||||
|
||||
|
|
|
@ -332,7 +332,7 @@ QString ImagesOrAllFilter() {
|
|||
}
|
||||
|
||||
QString PhotoVideoFilesFilter() {
|
||||
return u"Image and Video Files (*.png *.jpg *.jpeg *.mp4 *.mov *.m4v);;"_q
|
||||
return u"Image and Video Files (*"_q + Ui::ImageExtensions().join(u" *"_q) + u" *.m4v);;"_q
|
||||
+ AllFilesFilter();
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "passport/passport_form_controller.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "data/components/credits.h"
|
||||
#include "data/data_birthday.h"
|
||||
#include "data/data_channel.h"
|
||||
|
@ -940,10 +941,41 @@ bool ShowEditBirthday(
|
|||
: (u"Error: "_q + error.type()));
|
||||
})).handleFloodErrors().send();
|
||||
};
|
||||
controller->show(Box(
|
||||
Ui::EditBirthdayBox,
|
||||
user->birthday(),
|
||||
save));
|
||||
if (match->captured(1).isEmpty()) {
|
||||
controller->show(Box(Ui::EditBirthdayBox, user->birthday(), save));
|
||||
} else {
|
||||
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
Ui::EditBirthdayBox(box, user->birthday(), save);
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
const auto session = &user->session();
|
||||
const auto key = Api::UserPrivacy::Key::Birthday;
|
||||
session->api().userPrivacy().reload(key);
|
||||
auto isExactlyContacts = session->api().userPrivacy().value(
|
||||
key
|
||||
) | rpl::map([=](const Api::UserPrivacy::Rule &value) {
|
||||
return (value.option == Api::UserPrivacy::Option::Contacts)
|
||||
&& value.always.peers.empty()
|
||||
&& !value.always.premiums
|
||||
&& value.never.peers.empty();
|
||||
}) | rpl::distinct_until_changed();
|
||||
Ui::AddSkip(container);
|
||||
const auto link = u"internal:edit_privacy_birthday:from_box"_q;
|
||||
Ui::AddDividerText(container, rpl::conditional(
|
||||
std::move(isExactlyContacts),
|
||||
tr::lng_settings_birthday_contacts(
|
||||
lt_link,
|
||||
tr::lng_settings_birthday_contacts_link(
|
||||
) | Ui::Text::ToLink(link),
|
||||
Ui::Text::WithEntities),
|
||||
tr::lng_settings_birthday_about(
|
||||
lt_link,
|
||||
tr::lng_settings_birthday_about_link(
|
||||
) | Ui::Text::ToLink(link),
|
||||
Ui::Text::WithEntities)));
|
||||
}));
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -954,11 +986,29 @@ bool ShowEditBirthdayPrivacy(
|
|||
if (!controller) {
|
||||
return false;
|
||||
}
|
||||
const auto isFromBox = !match->captured(1).isEmpty();
|
||||
auto syncLifetime = controller->session().api().userPrivacy().value(
|
||||
Api::UserPrivacy::Key::Birthday
|
||||
) | rpl::take(
|
||||
1
|
||||
) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) {
|
||||
if (isFromBox) {
|
||||
using namespace ::Settings;
|
||||
class Controller final : public BirthdayPrivacyController {
|
||||
object_ptr<Ui::RpWidget> setupAboveWidget(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<Option> optionValue,
|
||||
not_null<QWidget*> outerContainer) override {
|
||||
return { nullptr };
|
||||
}
|
||||
};
|
||||
controller->show(Box<EditPrivacyBox>(
|
||||
controller,
|
||||
std::make_unique<Controller>(),
|
||||
value));
|
||||
return;
|
||||
}
|
||||
controller->show(Box<EditPrivacyBox>(
|
||||
controller,
|
||||
std::make_unique<::Settings::BirthdayPrivacyController>(),
|
||||
|
@ -1452,6 +1502,35 @@ bool ResolveUniqueGift(
|
|||
return true;
|
||||
}
|
||||
|
||||
bool ResolveConferenceCall(
|
||||
Window::SessionController *controller,
|
||||
const Match &match,
|
||||
const QVariant &context) {
|
||||
if (!controller) {
|
||||
return false;
|
||||
}
|
||||
const auto slug = match->captured(1);
|
||||
if (slug.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const auto myContext = context.value<ClickHandlerContext>();
|
||||
controller->window().activate();
|
||||
controller->resolveConferenceCall(match->captured(1), myContext.itemId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolveStarsSettings(
|
||||
Window::SessionController *controller,
|
||||
const Match &match,
|
||||
const QVariant &context) {
|
||||
if (!controller) {
|
||||
return false;
|
||||
}
|
||||
controller->showSettings(::Settings::CreditsId());
|
||||
controller->window().activate();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
|
||||
|
@ -1548,6 +1627,14 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
|
|||
u"^nft/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
|
||||
ResolveUniqueGift
|
||||
},
|
||||
{
|
||||
u"^call/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q,
|
||||
ResolveConferenceCall
|
||||
},
|
||||
{
|
||||
u"^stars/?(^\\?.*)?(#|$)"_q,
|
||||
ResolveStarsSettings
|
||||
},
|
||||
{
|
||||
u"^user\\?(.+)(#|$)"_q,
|
||||
AyuUrlHandlers::ResolveUser
|
||||
|
@ -1587,11 +1674,11 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
|
|||
ShowSearchTagsPromo
|
||||
},
|
||||
{
|
||||
u"^edit_birthday$"_q,
|
||||
u"^edit_birthday(.*)$"_q,
|
||||
ShowEditBirthday,
|
||||
},
|
||||
{
|
||||
u"^edit_privacy_birthday$"_q,
|
||||
u"^edit_privacy_birthday(.*)$"_q,
|
||||
ShowEditBirthdayPrivacy,
|
||||
},
|
||||
{
|
||||
|
@ -1716,6 +1803,9 @@ QString TryConvertUrlToLocal(QString url) {
|
|||
} else if (const auto nftMatch = regex_match(u"^nft/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
|
||||
const auto slug = nftMatch->captured(1);
|
||||
return u"tg://nft?slug="_q + slug;
|
||||
} else if (const auto callMatch = regex_match(u"^call/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
|
||||
const auto slug = callMatch->captured(1);
|
||||
return u"tg://call?slug="_q + slug;
|
||||
} else if (const auto privateMatch = regex_match(u"^"
|
||||
"c/(\\-?\\d+)"
|
||||
"("
|
||||
|
|
|
@ -326,7 +326,7 @@ bool NameTypeAllowsThumbnail(NameType type) {
|
|||
|
||||
bool IsIpRevealingPath(const QString &filepath) {
|
||||
static const auto kExtensions = [] {
|
||||
const auto joined = u"htm html svg m4v m3u8 xhtml"_q;
|
||||
const auto joined = u"htm html svg m4v m3u m3u8 xhtml xml"_q;
|
||||
const auto list = joined.split(' ');
|
||||
return base::flat_set<QString>(list.begin(), list.end());
|
||||
}();
|
||||
|
|
|
@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "core/local_url_handlers.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/application.h"
|
||||
#include "core/bank_card_click_handler.h"
|
||||
#include "core/sandbox.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
|
@ -260,6 +261,10 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
|
|||
return my->session
|
||||
? std::make_shared<PhoneClickHandler>(my->session, data.text)
|
||||
: nullptr;
|
||||
case EntityType::BankCard:
|
||||
return my->session
|
||||
? std::make_shared<BankCardClickHandler>(my->session, data.text)
|
||||
: nullptr;
|
||||
}
|
||||
return Integration::createLinkHandler(data, context);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
|
|||
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
|
||||
constexpr auto AppName = "AyuGram Desktop"_cs;
|
||||
constexpr auto AppFile = "AyuGram"_cs;
|
||||
constexpr auto AppVersion = 5013001;
|
||||
constexpr auto AppVersionStr = "5.13.1";
|
||||
constexpr auto AppVersion = 5014001;
|
||||
constexpr auto AppVersionStr = "5.14.1";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
|
|
@ -25,7 +25,7 @@ Ui::LocationPicker *LocationPickers::lookup(const Api::SendAction &action) {
|
|||
for (auto i = begin(_pickers); i != end(_pickers);) {
|
||||
if (const auto strong = i->picker.get()) {
|
||||
if (i->action == action) {
|
||||
return i->picker.get();
|
||||
return strong;
|
||||
}
|
||||
++i;
|
||||
} else {
|
||||
|
@ -41,4 +41,4 @@ void LocationPickers::emplace(
|
|||
_pickers.push_back({ action, picker });
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
} // namespace Data
|
||||
|
|
|
@ -956,7 +956,7 @@ QString ChannelData::invitePeekHash() const {
|
|||
}
|
||||
|
||||
void ChannelData::privateErrorReceived() {
|
||||
if (const auto expires = invitePeekExpires()) {
|
||||
if (invitePeekExpires()) {
|
||||
const auto hash = invitePeekHash();
|
||||
for (const auto &window : session().windows()) {
|
||||
clearInvitePeek();
|
||||
|
@ -1001,10 +1001,13 @@ void ChannelData::setGroupCall(
|
|||
data.vid().v,
|
||||
data.vaccess_hash().v,
|
||||
scheduleDate,
|
||||
rtmp);
|
||||
rtmp,
|
||||
false); // conference
|
||||
owner().registerGroupCall(_call.get());
|
||||
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
|
||||
addFlags(Flag::CallActive);
|
||||
}, [&](const auto &) {
|
||||
clearGroupCall();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -234,10 +234,13 @@ void ChatData::setGroupCall(
|
|||
data.vid().v,
|
||||
data.vaccess_hash().v,
|
||||
scheduleDate,
|
||||
rtmp);
|
||||
rtmp,
|
||||
false); // conference
|
||||
owner().registerGroupCall(_call.get());
|
||||
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
|
||||
addFlags(Flag::CallActive);
|
||||
}, [&](const auto &) {
|
||||
clearGroupCall();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -351,7 +351,7 @@ bool ChatFilter::contains(
|
|||
: user->isContact()
|
||||
? Flag::Contacts
|
||||
: Flag::NonContacts;
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
} else if (peer->isChat()) {
|
||||
return Flag::Groups;
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
if (channel->isBroadcast()) {
|
||||
|
|
|
@ -238,7 +238,7 @@ void CloudThemes::showPreview(
|
|||
void CloudThemes::showPreview(
|
||||
not_null<Window::Controller*> controller,
|
||||
const CloudTheme &cloud) {
|
||||
if (const auto documentId = cloud.documentId) {
|
||||
if (cloud.documentId) {
|
||||
previewFromDocument(controller, cloud);
|
||||
} else if (cloud.createdBy == _session->userId()) {
|
||||
controller->show(Box(
|
||||
|
|
|
@ -60,9 +60,10 @@ bool GroupCallParticipant::screenPaused() const {
|
|||
GroupCall::GroupCall(
|
||||
not_null<PeerData*> peer,
|
||||
CallId id,
|
||||
CallId accessHash,
|
||||
uint64 accessHash,
|
||||
TimeId scheduleDate,
|
||||
bool rtmp)
|
||||
bool rtmp,
|
||||
bool conference)
|
||||
: _id(id)
|
||||
, _accessHash(accessHash)
|
||||
, _peer(peer)
|
||||
|
@ -70,15 +71,50 @@ GroupCall::GroupCall(
|
|||
, _speakingByActiveFinishTimer([=] { checkFinishSpeakingByActive(); })
|
||||
, _scheduleDate(scheduleDate)
|
||||
, _rtmp(rtmp)
|
||||
, _conference(conference)
|
||||
, _listenersHidden(rtmp) {
|
||||
if (_conference) {
|
||||
session().data().registerGroupCall(this);
|
||||
|
||||
_participantUpdates.events(
|
||||
) | rpl::filter([=](const ParticipantUpdate &update) {
|
||||
return !update.now
|
||||
&& !update.was->peer->isSelf()
|
||||
&& !_participantsWithAccess.current().empty();
|
||||
}) | rpl::start_with_next([=](const ParticipantUpdate &update) {
|
||||
if (const auto id = peerToUser(update.was->peer->id)) {
|
||||
if (_participantsWithAccess.current().contains(id)) {
|
||||
_staleParticipantIds.fire({ id });
|
||||
}
|
||||
}
|
||||
}, _checkStaleLifetime);
|
||||
|
||||
_participantsWithAccess.changes(
|
||||
) | rpl::filter([=](const base::flat_set<UserId> &list) {
|
||||
return !list.empty();
|
||||
}) | rpl::start_with_next([=] {
|
||||
if (_allParticipantsLoaded) {
|
||||
checkStaleParticipants();
|
||||
} else {
|
||||
requestParticipants();
|
||||
}
|
||||
}, _checkStaleLifetime);
|
||||
}
|
||||
}
|
||||
|
||||
GroupCall::~GroupCall() {
|
||||
if (_conference) {
|
||||
session().data().unregisterGroupCall(this);
|
||||
}
|
||||
api().request(_unknownParticipantPeersRequestId).cancel();
|
||||
api().request(_participantsRequestId).cancel();
|
||||
api().request(_reloadRequestId).cancel();
|
||||
}
|
||||
|
||||
Main::Session &GroupCall::session() const {
|
||||
return _peer->session();
|
||||
}
|
||||
|
||||
CallId GroupCall::id() const {
|
||||
return _id;
|
||||
}
|
||||
|
@ -91,10 +127,18 @@ bool GroupCall::rtmp() const {
|
|||
return _rtmp;
|
||||
}
|
||||
|
||||
bool GroupCall::canManage() const {
|
||||
return _conference ? _creator : _peer->canManageGroupCall();
|
||||
}
|
||||
|
||||
bool GroupCall::listenersHidden() const {
|
||||
return _listenersHidden;
|
||||
}
|
||||
|
||||
bool GroupCall::blockchainMayBeEmpty() const {
|
||||
return _version < 2;
|
||||
}
|
||||
|
||||
not_null<PeerData*> GroupCall::peer() const {
|
||||
return _peer;
|
||||
}
|
||||
|
@ -146,7 +190,7 @@ void GroupCall::requestParticipants() {
|
|||
: ApplySliceSource::SliceLoaded));
|
||||
setServerParticipantsCount(data.vcount().v);
|
||||
if (data.vparticipants().v.isEmpty()) {
|
||||
_allParticipantsLoaded = true;
|
||||
setParticipantsLoaded();
|
||||
}
|
||||
finishParticipantsSliceRequest();
|
||||
if (reloaded) {
|
||||
|
@ -157,7 +201,7 @@ void GroupCall::requestParticipants() {
|
|||
_participantsRequestId = 0;
|
||||
const auto reloaded = processSavedFullCall();
|
||||
setServerParticipantsCount(_participants.size());
|
||||
_allParticipantsLoaded = true;
|
||||
setParticipantsLoaded();
|
||||
finishParticipantsSliceRequest();
|
||||
if (reloaded) {
|
||||
_participantsReloaded.fire({});
|
||||
|
@ -165,6 +209,36 @@ void GroupCall::requestParticipants() {
|
|||
}).send();
|
||||
}
|
||||
|
||||
void GroupCall::setParticipantsLoaded() {
|
||||
_allParticipantsLoaded = true;
|
||||
checkStaleParticipants();
|
||||
}
|
||||
|
||||
void GroupCall::checkStaleParticipants() {
|
||||
const auto &list = _participantsWithAccess.current();
|
||||
if (list.empty()) {
|
||||
return;
|
||||
}
|
||||
auto existing = base::flat_set<UserId>();
|
||||
existing.reserve(_participants.size() + 1);
|
||||
existing.emplace(session().userId());
|
||||
for (const auto &participant : _participants) {
|
||||
if (const auto id = peerToUser(participant.peer->id)) {
|
||||
existing.emplace(id);
|
||||
}
|
||||
}
|
||||
auto stale = base::flat_set<UserId>();
|
||||
for (const auto &id : list) {
|
||||
if (!existing.contains(id)) {
|
||||
stale.reserve(list.size());
|
||||
stale.emplace(id);
|
||||
}
|
||||
}
|
||||
if (!stale.empty()) {
|
||||
_staleParticipantIds.fire(std::move(stale));
|
||||
}
|
||||
}
|
||||
|
||||
bool GroupCall::processSavedFullCall() {
|
||||
if (!_savedFull) {
|
||||
return false;
|
||||
|
@ -225,6 +299,10 @@ rpl::producer<int> GroupCall::fullCountValue() const {
|
|||
return _fullCount.value();
|
||||
}
|
||||
|
||||
QString GroupCall::conferenceInviteLink() const {
|
||||
return _conferenceInviteLink;
|
||||
}
|
||||
|
||||
bool GroupCall::participantsLoaded() const {
|
||||
return _allParticipantsLoaded;
|
||||
}
|
||||
|
@ -275,7 +353,32 @@ auto GroupCall::participantSpeaking() const
|
|||
return _participantSpeaking.events();
|
||||
}
|
||||
|
||||
void GroupCall::setParticipantsWithAccess(base::flat_set<UserId> list) {
|
||||
_participantsWithAccess = std::move(list);
|
||||
if (_allParticipantsLoaded) {
|
||||
checkStaleParticipants();
|
||||
} else {
|
||||
requestParticipants();
|
||||
}
|
||||
}
|
||||
|
||||
auto GroupCall::participantsWithAccessCurrent() const
|
||||
-> const base::flat_set<UserId> & {
|
||||
return _participantsWithAccess.current();
|
||||
}
|
||||
|
||||
auto GroupCall::participantsWithAccessValue() const
|
||||
-> rpl::producer<base::flat_set<UserId>> {
|
||||
return _participantsWithAccess.value();
|
||||
}
|
||||
|
||||
auto GroupCall::staleParticipantIds() const
|
||||
-> rpl::producer<base::flat_set<UserId>> {
|
||||
return _staleParticipantIds.events();
|
||||
}
|
||||
|
||||
void GroupCall::enqueueUpdate(const MTPUpdate &update) {
|
||||
const auto initial = !_version;
|
||||
update.match([&](const MTPDupdateGroupCall &updateData) {
|
||||
updateData.vcall().match([&](const MTPDgroupCall &data) {
|
||||
const auto version = data.vversion().v;
|
||||
|
@ -329,7 +432,7 @@ void GroupCall::enqueueUpdate(const MTPUpdate &update) {
|
|||
}, [](const auto &) {
|
||||
Unexpected("Type in GroupCall::enqueueUpdate.");
|
||||
});
|
||||
processQueuedUpdates();
|
||||
processQueuedUpdates(initial);
|
||||
}
|
||||
|
||||
void GroupCall::discard(const MTPDgroupCallDiscarded &data) {
|
||||
|
@ -404,6 +507,7 @@ void GroupCall::applyCallFields(const MTPDgroupCall &data) {
|
|||
_version = 1;
|
||||
}
|
||||
_rtmp = data.is_rtmp_stream();
|
||||
_creator = data.is_creator();
|
||||
_listenersHidden = data.is_listeners_hidden();
|
||||
_joinMuted = data.is_join_muted();
|
||||
_canChangeJoinMuted = data.is_can_change_join_muted();
|
||||
|
@ -420,6 +524,7 @@ void GroupCall::applyCallFields(const MTPDgroupCall &data) {
|
|||
_unmutedVideoLimit = data.vunmuted_video_limit().v;
|
||||
_allParticipantsLoaded
|
||||
= (_serverParticipantsCount == _participants.size());
|
||||
_conferenceInviteLink = qs(data.vinvite_link().value_or_empty());
|
||||
}
|
||||
|
||||
void GroupCall::applyLocalUpdate(
|
||||
|
@ -459,12 +564,10 @@ void GroupCall::applyEnqueuedUpdate(const MTPUpdate &update) {
|
|||
}, [](const auto &) {
|
||||
Unexpected("Type in GroupCall::applyEnqueuedUpdate.");
|
||||
});
|
||||
Core::App().calls().applyGroupCallUpdateChecked(
|
||||
&_peer->session(),
|
||||
update);
|
||||
Core::App().calls().applyGroupCallUpdateChecked(&session(), update);
|
||||
}
|
||||
|
||||
void GroupCall::processQueuedUpdates() {
|
||||
void GroupCall::processQueuedUpdates(bool initial) {
|
||||
if (!_version || _applyingQueuedUpdates) {
|
||||
return;
|
||||
}
|
||||
|
@ -476,7 +579,13 @@ void GroupCall::processQueuedUpdates() {
|
|||
const auto type = entry.first.second;
|
||||
const auto incremented = (type == QueuedType::VersionedParticipant);
|
||||
if ((version < _version)
|
||||
|| (version == _version && incremented)) {
|
||||
|| (version == _version && incremented && !initial)) {
|
||||
// There is a case for a new conference call we receive:
|
||||
// - updateGroupCall, version = 2
|
||||
// - updateGroupCallParticipants, version = 2, versioned
|
||||
// In case we were joining together with creation,
|
||||
// in that case we don't want to skip the participants update,
|
||||
// so we pass the `initial` flag specifically for that case.
|
||||
_queuedUpdates.erase(_queuedUpdates.begin());
|
||||
} else if (version == _version
|
||||
|| (version == _version + 1 && incremented)) {
|
||||
|
@ -651,7 +760,8 @@ void GroupCall::applyParticipantsSlice(
|
|||
.videoJoined = videoJoined,
|
||||
.applyVolumeFromMin = applyVolumeFromMin,
|
||||
};
|
||||
if (i == end(_participants)) {
|
||||
const auto adding = (i == end(_participants));
|
||||
if (adding) {
|
||||
if (value.ssrc) {
|
||||
_participantPeerByAudioSsrc.emplace(
|
||||
value.ssrc,
|
||||
|
@ -664,9 +774,6 @@ void GroupCall::applyParticipantsSlice(
|
|||
participantPeer);
|
||||
}
|
||||
_participants.push_back(value);
|
||||
if (const auto user = participantPeer->asUser()) {
|
||||
_peer->owner().unregisterInvitedToCallUser(_id, user);
|
||||
}
|
||||
} else {
|
||||
if (i->ssrc != value.ssrc) {
|
||||
_participantPeerByAudioSsrc.erase(i->ssrc);
|
||||
|
@ -698,6 +805,14 @@ void GroupCall::applyParticipantsSlice(
|
|||
.now = value,
|
||||
});
|
||||
}
|
||||
if (adding) {
|
||||
if (const auto user = participantPeer->asUser()) {
|
||||
_peer->owner().unregisterInvitedToCallUser(
|
||||
_id,
|
||||
user,
|
||||
false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sliceSource == ApplySliceSource::UpdateReceived) {
|
||||
|
@ -984,7 +1099,7 @@ bool GroupCall::joinedToTop() const {
|
|||
}
|
||||
|
||||
ApiWrap &GroupCall::api() const {
|
||||
return _peer->session().api();
|
||||
return session().api();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
|
|
|
@ -17,6 +17,15 @@ namespace Calls {
|
|||
struct ParticipantVideoParams;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace TdE2E {
|
||||
struct ParticipantState;
|
||||
struct UserId;
|
||||
} // namespace TdE2E
|
||||
|
||||
namespace Data {
|
||||
|
||||
[[nodiscard]] const std::string &RtmpEndpointId();
|
||||
|
@ -56,15 +65,20 @@ public:
|
|||
GroupCall(
|
||||
not_null<PeerData*> peer,
|
||||
CallId id,
|
||||
CallId accessHash,
|
||||
uint64 accessHash,
|
||||
TimeId scheduleDate,
|
||||
bool rtmp);
|
||||
bool rtmp,
|
||||
bool conference);
|
||||
~GroupCall();
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
[[nodiscard]] CallId id() const;
|
||||
[[nodiscard]] bool loaded() const;
|
||||
[[nodiscard]] bool rtmp() const;
|
||||
[[nodiscard]] bool canManage() const;
|
||||
[[nodiscard]] bool listenersHidden() const;
|
||||
[[nodiscard]] bool blockchainMayBeEmpty() const;
|
||||
[[nodiscard]] not_null<PeerData*> peer() const;
|
||||
[[nodiscard]] MTPInputGroupCall input() const;
|
||||
[[nodiscard]] QString title() const {
|
||||
|
@ -133,6 +147,16 @@ public:
|
|||
[[nodiscard]] auto participantSpeaking() const
|
||||
-> rpl::producer<not_null<Participant*>>;
|
||||
|
||||
void setParticipantsWithAccess(base::flat_set<UserId> list);
|
||||
[[nodiscard]] auto participantsWithAccessCurrent() const
|
||||
-> const base::flat_set<UserId> &;
|
||||
[[nodiscard]] auto participantsWithAccessValue() const
|
||||
-> rpl::producer<base::flat_set<UserId>>;
|
||||
[[nodiscard]] auto staleParticipantIds() const
|
||||
-> rpl::producer<base::flat_set<UserId>>;
|
||||
void setParticipantsLoaded();
|
||||
void checkStaleParticipants();
|
||||
|
||||
void enqueueUpdate(const MTPUpdate &update);
|
||||
void applyLocalUpdate(
|
||||
const MTPDupdateGroupCallParticipants &update);
|
||||
|
@ -153,6 +177,7 @@ public:
|
|||
|
||||
[[nodiscard]] int fullCount() const;
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const;
|
||||
[[nodiscard]] QString conferenceInviteLink() const;
|
||||
|
||||
void setInCall();
|
||||
void reload();
|
||||
|
@ -191,7 +216,7 @@ private:
|
|||
void applyEnqueuedUpdate(const MTPUpdate &update);
|
||||
void setServerParticipantsCount(int count);
|
||||
void computeParticipantsCount();
|
||||
void processQueuedUpdates();
|
||||
void processQueuedUpdates(bool initial = false);
|
||||
void processFullCallUsersChats(const MTPphone_GroupCall &call);
|
||||
void processFullCallFields(const MTPphone_GroupCall &call);
|
||||
[[nodiscard]] bool requestParticipantsAfterReload(
|
||||
|
@ -201,7 +226,7 @@ private:
|
|||
[[nodiscard]] Participant *findParticipant(not_null<PeerData*> peer);
|
||||
|
||||
const CallId _id = 0;
|
||||
const CallId _accessHash = 0;
|
||||
const uint64 _accessHash = 0;
|
||||
|
||||
not_null<PeerData*> _peer;
|
||||
int _version = 0;
|
||||
|
@ -209,6 +234,7 @@ private:
|
|||
mtpRequestId _reloadRequestId = 0;
|
||||
crl::time _reloadLastFinished = 0;
|
||||
rpl::variable<QString> _title;
|
||||
QString _conferenceInviteLink;
|
||||
|
||||
base::flat_multi_map<
|
||||
std::pair<int, QueuedType>,
|
||||
|
@ -241,13 +267,19 @@ private:
|
|||
rpl::event_stream<not_null<Participant*>> _participantSpeaking;
|
||||
rpl::event_stream<> _participantsReloaded;
|
||||
|
||||
bool _joinMuted = false;
|
||||
bool _canChangeJoinMuted = true;
|
||||
bool _allParticipantsLoaded = false;
|
||||
bool _joinedToTop = false;
|
||||
bool _applyingQueuedUpdates = false;
|
||||
bool _rtmp = false;
|
||||
bool _listenersHidden = false;
|
||||
rpl::variable<base::flat_set<UserId>> _participantsWithAccess;
|
||||
rpl::event_stream<base::flat_set<UserId>> _staleParticipantIds;
|
||||
rpl::lifetime _checkStaleLifetime;
|
||||
|
||||
bool _creator : 1 = false;
|
||||
bool _joinMuted : 1 = false;
|
||||
bool _canChangeJoinMuted : 1 = true;
|
||||
bool _allParticipantsLoaded : 1 = false;
|
||||
bool _joinedToTop : 1 = false;
|
||||
bool _applyingQueuedUpdates : 1 = false;
|
||||
bool _rtmp : 1 = false;
|
||||
bool _conference : 1 = false;
|
||||
bool _listenersHidden : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_user.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h" // ClickHandlerContext
|
||||
#include "lang/lang_keys.h"
|
||||
|
@ -455,30 +456,57 @@ Invoice ComputeInvoiceData(
|
|||
return result;
|
||||
}
|
||||
|
||||
Call ComputeCallData(const MTPDmessageActionPhoneCall &call) {
|
||||
Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionPhoneCall &call) {
|
||||
auto result = Call();
|
||||
result.finishReason = [&] {
|
||||
result.state = [&] {
|
||||
if (const auto reason = call.vreason()) {
|
||||
return reason->match([](const MTPDphoneCallDiscardReasonBusy &) {
|
||||
return CallFinishReason::Busy;
|
||||
return CallState::Busy;
|
||||
}, [](const MTPDphoneCallDiscardReasonDisconnect &) {
|
||||
return CallFinishReason::Disconnected;
|
||||
return CallState::Disconnected;
|
||||
}, [](const MTPDphoneCallDiscardReasonHangup &) {
|
||||
return CallFinishReason::Hangup;
|
||||
return CallState::Hangup;
|
||||
}, [](const MTPDphoneCallDiscardReasonMissed &) {
|
||||
return CallFinishReason::Missed;
|
||||
}, [](const MTPDphoneCallDiscardReasonAllowGroupCall &) {
|
||||
return CallFinishReason::AllowGroupCall;
|
||||
return CallState::Missed;
|
||||
}, [](const MTPDphoneCallDiscardReasonMigrateConferenceCall &) {
|
||||
return CallState::MigrateConferenceCall;
|
||||
});
|
||||
Unexpected("Call reason type.");
|
||||
}
|
||||
return CallFinishReason::Hangup;
|
||||
return CallState::Hangup;
|
||||
}();
|
||||
result.duration = call.vduration().value_or_empty();
|
||||
result.video = call.is_video();
|
||||
return result;
|
||||
}
|
||||
|
||||
Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionConferenceCall &call) {
|
||||
auto participants = std::vector<not_null<PeerData*>>();
|
||||
if (const auto list = call.vother_participants()) {
|
||||
participants.reserve(list->v.size());
|
||||
for (const auto &participant : list->v) {
|
||||
participants.push_back(owner->peer(peerFromMTP(participant)));
|
||||
}
|
||||
}
|
||||
return {
|
||||
.otherParticipants = std::move(participants),
|
||||
.conferenceId = call.vcall_id().v,
|
||||
.duration = call.vduration().value_or_empty(),
|
||||
.state = (call.vduration().value_or_empty()
|
||||
? CallState::Hangup
|
||||
: call.is_missed()
|
||||
? CallState::Missed
|
||||
: call.is_active()
|
||||
? CallState::Active
|
||||
: CallState::Invitation),
|
||||
.video = call.is_video(),
|
||||
};
|
||||
}
|
||||
|
||||
GiveawayStart ComputeGiveawayStartData(
|
||||
not_null<HistoryItem*> item,
|
||||
const MTPDmessageMediaGiveaway &data) {
|
||||
|
@ -1111,7 +1139,7 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
|
|||
return toGroupPreview(group->items, options);
|
||||
}
|
||||
}
|
||||
if (const auto sticker = _document->sticker()) {
|
||||
if (_document->sticker()) {
|
||||
return Media::toPreview(options);
|
||||
}
|
||||
auto images = std::vector<ItemPreviewImage>();
|
||||
|
@ -1178,7 +1206,7 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
|
|||
}
|
||||
|
||||
TextWithEntities MediaFile::notificationText() const {
|
||||
if (const auto sticker = _document->sticker()) {
|
||||
if (_document->sticker()) {
|
||||
const auto text = _emoji.isEmpty()
|
||||
? tr::lng_in_dlg_sticker(tr::now)
|
||||
: tr::lng_in_dlg_sticker_emoji(tr::now, lt_emoji, _emoji);
|
||||
|
@ -1210,7 +1238,7 @@ TextWithEntities MediaFile::notificationText() const {
|
|||
}
|
||||
|
||||
QString MediaFile::pinnedTextSubstring() const {
|
||||
if (const auto sticker = _document->sticker()) {
|
||||
if (_document->sticker()) {
|
||||
if (!_emoji.isEmpty()) {
|
||||
return tr::lng_action_pinned_media_emoji_sticker(
|
||||
tr::now,
|
||||
|
@ -1670,11 +1698,28 @@ std::unique_ptr<HistoryView::Media> MediaLocation::createView(
|
|||
MediaCall::MediaCall(not_null<HistoryItem*> parent, const Call &call)
|
||||
: Media(parent)
|
||||
, _call(call) {
|
||||
parent->history()->owner().registerCallItem(parent);
|
||||
const auto peer = parent->history()->peer;
|
||||
peer->owner().registerCallItem(parent);
|
||||
if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {
|
||||
Core::App().calls().registerConferenceInvite(
|
||||
_call.conferenceId,
|
||||
user,
|
||||
parent->id,
|
||||
!parent->out());
|
||||
}
|
||||
}
|
||||
|
||||
MediaCall::~MediaCall() {
|
||||
parent()->history()->owner().unregisterCallItem(parent());
|
||||
const auto parent = this->parent();
|
||||
const auto peer = parent->history()->peer;
|
||||
peer->owner().unregisterCallItem(parent);
|
||||
if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) {
|
||||
Core::App().calls().unregisterConferenceInvite(
|
||||
_call.conferenceId,
|
||||
user,
|
||||
parent->id,
|
||||
!parent->out());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Media> MediaCall::clone(not_null<HistoryItem*> parent) {
|
||||
|
@ -1686,7 +1731,8 @@ const Call *MediaCall::call() const {
|
|||
}
|
||||
|
||||
TextWithEntities MediaCall::notificationText() const {
|
||||
auto result = Text(parent(), _call.finishReason, _call.video);
|
||||
const auto conference = (_call.conferenceId != 0);
|
||||
auto result = Text(parent(), _call.state, conference, _call.video);
|
||||
if (_call.duration > 0) {
|
||||
result = tr::lng_call_type_and_duration(
|
||||
tr::now,
|
||||
|
@ -1727,26 +1773,39 @@ std::unique_ptr<HistoryView::Media> MediaCall::createView(
|
|||
|
||||
QString MediaCall::Text(
|
||||
not_null<HistoryItem*> item,
|
||||
CallFinishReason reason,
|
||||
CallState state,
|
||||
bool conference,
|
||||
bool video) {
|
||||
if (item->out()) {
|
||||
return ((reason == CallFinishReason::Missed)
|
||||
? (video
|
||||
if (state == CallState::Invitation) {
|
||||
return tr::lng_call_invitation(tr::now);
|
||||
} else if (state == CallState::Active) {
|
||||
return tr::lng_call_ongoing(tr::now);
|
||||
} else if (item->out()) {
|
||||
return ((state == CallState::Missed)
|
||||
? (conference
|
||||
? tr::lng_call_group_declined
|
||||
: video
|
||||
? tr::lng_call_video_cancelled
|
||||
: tr::lng_call_cancelled)
|
||||
: (video
|
||||
: (conference
|
||||
? tr::lng_call_group_outgoing
|
||||
: video
|
||||
? tr::lng_call_video_outgoing
|
||||
: tr::lng_call_outgoing))(tr::now);
|
||||
} else if (reason == CallFinishReason::Missed) {
|
||||
return (video
|
||||
} else if (state == CallState::Missed) {
|
||||
return (conference
|
||||
? tr::lng_call_group_missed
|
||||
: video
|
||||
? tr::lng_call_video_missed
|
||||
: tr::lng_call_missed)(tr::now);
|
||||
} else if (reason == CallFinishReason::Busy) {
|
||||
} else if (state == CallState::Busy) {
|
||||
return (video
|
||||
? tr::lng_call_video_declined
|
||||
: tr::lng_call_declined)(tr::now);
|
||||
}
|
||||
return (video
|
||||
return (conference
|
||||
? tr::lng_call_group_incoming
|
||||
: video
|
||||
? tr::lng_call_video_incoming
|
||||
: tr::lng_call_incoming)(tr::now);
|
||||
}
|
||||
|
|
|
@ -41,12 +41,14 @@ class WallPaper;
|
|||
class Session;
|
||||
struct UniqueGift;
|
||||
|
||||
enum class CallFinishReason : char {
|
||||
enum class CallState : char {
|
||||
Missed,
|
||||
Busy,
|
||||
Disconnected,
|
||||
Hangup,
|
||||
AllowGroupCall,
|
||||
MigrateConferenceCall,
|
||||
Invitation,
|
||||
Active,
|
||||
};
|
||||
|
||||
struct SharedContact final {
|
||||
|
@ -78,10 +80,12 @@ struct SharedContact final {
|
|||
};
|
||||
|
||||
struct Call {
|
||||
using FinishReason = CallFinishReason;
|
||||
using State = CallState;
|
||||
|
||||
std::vector<not_null<PeerData*>> otherParticipants;
|
||||
CallId conferenceId = 0;
|
||||
int duration = 0;
|
||||
FinishReason finishReason = FinishReason::Missed;
|
||||
State state = State::Missed;
|
||||
bool video = false;
|
||||
|
||||
};
|
||||
|
@ -462,9 +466,10 @@ public:
|
|||
not_null<HistoryItem*> realParent,
|
||||
HistoryView::Element *replacing = nullptr) override;
|
||||
|
||||
static QString Text(
|
||||
[[nodiscard]] static QString Text(
|
||||
not_null<HistoryItem*> item,
|
||||
CallFinishReason reason,
|
||||
CallState state,
|
||||
bool conference,
|
||||
bool video);
|
||||
|
||||
private:
|
||||
|
@ -798,7 +803,12 @@ private:
|
|||
not_null<HistoryItem*> item,
|
||||
const MTPDmessageMediaPaidMedia &data);
|
||||
|
||||
[[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call);
|
||||
[[nodiscard]] Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionPhoneCall &call);
|
||||
[[nodiscard]] Call ComputeCallData(
|
||||
not_null<Session*> owner,
|
||||
const MTPDmessageActionConferenceCall &call);
|
||||
|
||||
[[nodiscard]] GiveawayStart ComputeGiveawayStartData(
|
||||
not_null<HistoryItem*> item,
|
||||
|
|
|
@ -673,7 +673,7 @@ bool PeerData::canTransferGifts() const {
|
|||
bool PeerData::canEditMessagesIndefinitely() const {
|
||||
if (const auto user = asUser()) {
|
||||
return user->isSelf();
|
||||
} else if (const auto chat = asChat()) {
|
||||
} else if (isChat()) {
|
||||
return false;
|
||||
} else if (const auto channel = asChannel()) {
|
||||
return channel->isMegagroup()
|
||||
|
@ -1380,7 +1380,7 @@ Data::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const {
|
|||
}
|
||||
|
||||
bool PeerData::allowsForwarding() const {
|
||||
if (const auto user = asUser()) {
|
||||
if (isUser()) {
|
||||
return true;
|
||||
} else if (const auto channel = asChannel()) {
|
||||
return channel->allowsForwarding();
|
||||
|
|
|
@ -608,7 +608,7 @@ rpl::producer<int> UniqueReactionsLimitValue(
|
|||
) | rpl::map([config = &peer->session().appConfig()] {
|
||||
return UniqueReactionsLimit(config);
|
||||
}) | rpl::distinct_until_changed();
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
if (peer->isChannel()) {
|
||||
return rpl::combine(
|
||||
PeerAllowedReactionsValue(peer),
|
||||
std::move(configValue)
|
||||
|
@ -617,7 +617,7 @@ rpl::producer<int> UniqueReactionsLimitValue(
|
|||
? allowedReactions.maxCount
|
||||
: limit;
|
||||
});
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
} else if (peer->isChat()) {
|
||||
return rpl::combine(
|
||||
PeerAllowedReactionsValue(peer),
|
||||
std::move(configValue)
|
||||
|
|
|
@ -65,13 +65,13 @@ QByteArray PhotoMedia::imageBytes(PhotoSize size) const {
|
|||
auto PhotoMedia::resolveLoadedImage(PhotoSize size) const
|
||||
-> const PhotoImage * {
|
||||
const auto &original = _images[PhotoSizeIndex(size)];
|
||||
if (const auto image = original.data.get()) {
|
||||
if (original.data) {
|
||||
if (original.goodFor >= size) {
|
||||
return &original;
|
||||
}
|
||||
}
|
||||
const auto &valid = _images[_owner->validSizeIndex(size)];
|
||||
if (const auto image = valid.data.get()) {
|
||||
if (valid.data.get()) {
|
||||
if (valid.goodFor >= size) {
|
||||
return &valid;
|
||||
}
|
||||
|
|
|
@ -10,9 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
namespace Data {
|
||||
|
||||
struct PremiumSubscriptionOption {
|
||||
int months = 0;
|
||||
QString duration;
|
||||
QString discount;
|
||||
QString costPerMonth;
|
||||
QString costNoDiscount;
|
||||
QString costTotal;
|
||||
QString total;
|
||||
QString botUrl;
|
||||
|
|
|
@ -370,7 +370,7 @@ bool RepliesList::buildFromData(not_null<Viewer*> viewer) {
|
|||
const auto around = [&] {
|
||||
if (viewer->around != ShowAtUnreadMsgId) {
|
||||
return viewer->around;
|
||||
} else if (const auto item = lookupRoot()) {
|
||||
} else if (lookupRoot()) {
|
||||
return computeInboxReadTillFull();
|
||||
} else if (_owningTopic) {
|
||||
// Somehow we don't want always to jump to computed inboxReadTill
|
||||
|
|