Merge tag 'v6.6.2' into dev

This commit is contained in:
AlexeyZavar
2026-03-19 01:20:42 +03:00
322 changed files with 11602 additions and 2747 deletions
+454
View File
@@ -0,0 +1,454 @@
# Phase Prompts
Use these templates for `codex exec --json` child runs. Replace `<TASK>`, `<PROJECT>`, `<LETTER>`, and `<REPO_ROOT>`.
## Phase 0: Setup
**Record the current time now** and store it as `$START_TIME`. You will use this at the end to display total elapsed time.
Before running any phase prompts, the orchestrator must determine whether this is a new project or a follow-up task.
**Follow-up detection (MANDATORY — do this BEFORE anything else):**
1. Extract the first word/token from the task description. Call it `FIRST_TOKEN`.
2. Run these two checks IN PARALLEL:
- `ls .ai/` — to see all existing project names
- `ls .ai/<FIRST_TOKEN>/about.md` — to check if this specific project exists
3. If check 2 **succeeds** (the file exists): this is a **follow-up task**. The project name is `FIRST_TOKEN`. The task description is everything after `FIRST_TOKEN`.
4. If check 2 **fails** (file not found): this is a **new project**. The full input is the task description.
**Do NOT proceed until you have run these checks and determined follow-up vs new.**
**For new projects:**
- Using the list from check 1, pick a unique short name (1-2 lowercase words, hyphen-separated) that doesn't collide with existing projects.
- Create `.ai/<PROJECT>/` and `.ai/<PROJECT>/a/` and `logs/`.
- Set `<LETTER>` = `a`.
**For follow-up tasks:**
- Scan `.ai/<PROJECT>/` for existing task folders (`a/`, `b/`, ...). Find the latest one (highest letter).
- The previous task letter = that highest letter.
- The new task letter = next letter in sequence.
- Create `.ai/<PROJECT>/<LETTER>/` and `logs/`.
Then proceed to Phase 1. Follow-up tasks do NOT skip context gathering — they go through a modified version of it.
## Phase 1: Context (New Project, letter = `a`)
```text
You are a context-gathering agent for a large C++ codebase (Telegram Desktop).
TASK: <TASK>
YOUR JOB: Read AGENTS.md, inspect the codebase, find ALL files and code relevant to this task, and write two documents.
Steps:
1. Read AGENTS.md for project conventions and build instructions.
2. Search the codebase for files, classes, functions, and patterns related to the task.
3. Read all potentially relevant files. Be thorough - read more rather than less.
4. For each relevant file, note:
- File path
- Relevant line ranges
- What the code does and how it relates to the task
- Key data structures, function signatures, patterns used
5. Look for similar existing features that could serve as a reference implementation.
6. Check api.tl if the task involves Telegram API.
7. Check .style files if the task involves UI.
8. Check lang.strings if the task involves user-visible text.
Write TWO files:
### File 1: .ai/<PROJECT>/about.md
NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. No planning, implementation, or review agent will ever read it. Only the context-gathering agent of the next follow-up task reads about.md (together with the latest context.md) to produce a fresh context.md for that next task.
Write it as if the project is already fully implemented and working. It should contain:
- **Project**: What this project does (feature description, goals, scope)
- **Architecture**: High-level architectural decisions, which modules are involved, how they interact
- **Key Design Decisions**: Important choices made about the approach
- **Relevant Codebase Areas**: Which parts of the codebase this project touches, key types and APIs involved
Do NOT include temporal state like "Current State", "Pending Changes", "Not yet implemented", "TODO", or any other framing that distinguishes between "done" and "not done". Describe the project as a complete, coherent whole — as if everything is already working. This is a project overview, not a status tracker. Task-specific work belongs exclusively in context.md.
### File 2: .ai/<PROJECT>/a/context.md
This is the task-specific implementation context. This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. It should contain:
- **Task Description**: The full task restated clearly
- **Relevant Files**: Every file path with line ranges and descriptions of what's there
- **Key Code Patterns**: How similar things are done in the codebase (with code snippets)
- **Data Structures**: Relevant types, structs, classes
- **API Methods**: Any TL schema methods involved (copied from api.tl)
- **UI Styles**: Any relevant style definitions
- **Localization**: Any relevant string keys
- **Build Info**: Build command and any special notes
- **Reference Implementations**: Similar features that can serve as templates
Be extremely thorough. Another agent with NO prior context will read this file and must be able to understand everything needed to implement the task.
Do not implement code in this phase.
```
## Phase 1F: Context (Follow-up Task, letter = `b`, `c`, ...)
```text
You are a context-gathering agent for a follow-up task on an existing project in a large C++ codebase (Telegram Desktop).
NEW TASK: <TASK>
YOUR JOB: Read the existing project state, gather any additional context needed, and produce fresh documents for the new task.
Steps:
1. Read AGENTS.md for project conventions and build instructions.
2. Read .ai/<PROJECT>/about.md — this is the project-level blueprint describing everything done so far.
3. Read .ai/<PROJECT>/<PREV_LETTER>/context.md — this is the previous task's gathered context.
4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context.
5. Based on the NEW TASK description, search the codebase for any ADDITIONAL files, classes, functions, and patterns that are relevant to the new task but not already covered.
6. Read all newly relevant files thoroughly.
Write TWO files:
### File 1: .ai/<PROJECT>/about.md (REWRITE)
NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. No planning, implementation, or review agent will ever read it. You are rewriting it now so that the next follow-up has an accurate project overview to start from.
REWRITE this file (not append). The new about.md must be a single coherent document that describes the project as if everything — including this new task's changes — is already fully implemented and working.
It should incorporate:
- Everything from the old about.md that is still accurate and relevant
- The new task's functionality described as part of the project (not as "changes to make")
- Any changed design decisions or architectural updates from the new task requirements
It should NOT contain:
- Any temporal state: "Current State", "Pending Changes", "TODO", "Not yet implemented"
- History of how requirements changed between tasks
- References to "the old approach" vs "the new approach"
- Task-by-task changelog or timeline
- Any distinction between "what was done before" and "what this task adds"
- Information that contradicts the new task requirements (if the new task changes direction, the about.md should reflect the NEW direction as if it was always the plan)
### File 2: .ai/<PROJECT>/<LETTER>/context.md
This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. about.md will NOT be available to them.
It should contain:
- **Task Description**: The new task restated clearly, with enough project background (from about.md and previous context.md) that an implementation agent can understand it without reading any other .ai/ files
- **Relevant Files**: Every file path with line ranges relevant to THIS task (including files modified by previous tasks and any newly relevant files)
- **Key Code Patterns**: How similar things are done in the codebase
- **Data Structures**: Relevant types, structs, classes
- **API Methods**: Any TL schema methods involved
- **UI Styles**: Any relevant style definitions
- **Localization**: Any relevant string keys
- **Build Info**: Build command and any special notes
- **Reference Implementations**: Similar features that can serve as templates
Be extremely thorough. Another agent with NO prior context will read ONLY this file and must be able to understand everything needed to implement the new task. Do NOT assume the reader has seen about.md or any previous task files. The context.md is the single source of truth for all downstream agents — it must include all relevant project background, not just the delta.
Do not implement code in this phase.
```
## Phase 2: Plan
```text
You are a planning agent. You must create a detailed implementation plan.
Read these files:
- .ai/<PROJECT>/<LETTER>/context.md - Contains all gathered context for this task
- Then read the specific source files referenced in context.md to understand the code deeply.
Create a detailed plan in: .ai/<PROJECT>/<LETTER>/plan.md
The plan.md should contain:
## Task
<one-line summary>
## Approach
<high-level description of the implementation approach>
## Files to Modify
<list of files that will be created or modified>
## Files to Create
<list of new files, if any>
## Implementation Steps
Each step must be specific enough that an agent can execute it without ambiguity:
- Exact file paths
- Exact function names
- What code to add/modify/remove
- Where exactly in the file (after which function, in which class, etc.)
Number every step. Group steps into phases if there are more than ~8 steps.
### Phase 1: <name>
1. <specific step>
2. <specific step>
...
### Phase 2: <name> (if needed)
...
## Build Verification
- Build command to run
- Expected outcome
## Status
- [ ] Phase 1: <name>
- [ ] Phase 2: <name> (if applicable)
- [ ] Build verification
- [ ] Code review
Do not implement code in this phase.
```
## Phase 3: Plan Assessment
```text
You are a plan assessment agent. Review and refine an implementation plan.
Read these files:
- .ai/<PROJECT>/<LETTER>/context.md
- .ai/<PROJECT>/<LETTER>/plan.md
- Then read the actual source files referenced to verify the plan makes sense.
Assess the plan:
1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types?
2. **Completeness**: Are there missing steps? Edge cases not handled?
3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from AGENTS.md?
4. **Design**: Could the approach be improved? Are there better patterns already used in the codebase?
5. **Phase sizing**: Each phase should be implementable by a single agent in one session. If a phase has more than ~8-10 substantive code changes, split it further.
Update plan.md with your refinements. Keep the same structure but:
- Fix any inaccuracies
- Add missing steps
- Improve the approach if you found better patterns
- Ensure phases are properly sized for single-agent execution
- Add a line at the top of the Status section: `Phases: <N>` indicating how many implementation phases there are
- Add `Assessed: yes` at the bottom of the file
If the plan is small enough for a single agent (roughly <=8 steps), mark it as a single phase.
Do not implement code in this phase.
```
## Phase 4: Implementation
For each phase in the plan that is not yet marked as done, run a separate child session:
```text
You are an implementation agent working on phase <N> of an implementation plan.
Read these files first:
- .ai/<PROJECT>/<LETTER>/context.md - Full codebase context
- .ai/<PROJECT>/<LETTER>/plan.md - Implementation plan
Then read the source files you'll be modifying.
YOUR TASK: Implement ONLY Phase <N> from the plan:
<paste the specific phase steps here>
Rules:
- Follow the plan precisely
- Follow AGENTS.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.)
- Do NOT modify .ai/ files except to update the Status section in plan.md
- When done, update plan.md Status section: change `- [ ] Phase <N>: ...` to `- [x] Phase <N>: ...`
- Do NOT work on other phases
When finished, report what you did and any issues encountered.
```
After each implementation agent returns:
1. Read `plan.md` to check the status was updated.
2. If more phases remain, run the next implementation child session.
3. If all phases are done, proceed to build verification.
## Phase 5: Build Verification
Only run this phase if the task involved modifying project source code (not just docs or config).
```text
You are a build verification agent.
Read these files:
- .ai/<PROJECT>/<LETTER>/context.md
- .ai/<PROJECT>/<LETTER>/plan.md
The implementation is complete. Your job is to build the project and fix any build errors.
Steps:
1. Run (from repository root): cmake --build ./out --config Debug --target Telegram
2. If the build succeeds, update plan.md: change `- [ ] Build verification` to `- [x] Build verification`
3. If the build fails:
a. Read the error messages carefully
b. Read the relevant source files
c. Fix the errors in accordance with the plan and AGENTS.md conventions
d. Rebuild and repeat until the build passes
e. Update plan.md status when done
Rules:
- Only fix build errors, do not refactor or improve code
- Follow AGENTS.md conventions
- If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry
When finished, report the build result.
```
## Phase 6: Code Review Loop
After build verification passes, run up to 3 review-fix iterations to improve code quality. Set iteration counter `R = 1`.
### Review Loop
```
LOOP:
1. Run review agent (Step 6a) with iteration R
2. Read review<R>.md verdict:
- "APPROVED" → go to FINISH
- Has improvement suggestions → run fix agent (Step 6b)
3. After fix agent completes and build passes:
R = R + 1
If R > 3 → go to FINISH (stop iterating, accept current state)
Otherwise → go to step 1
FINISH:
- Update plan.md: change `- [ ] Code review` to `- [x] Code review`
- Proceed to Completion
```
### Step 6a: Code Review Agent
```text
You are a code review agent for Telegram Desktop (C++ / Qt).
Read these files:
- .ai/<PROJECT>/<LETTER>/context.md - Codebase context
- .ai/<PROJECT>/<LETTER>/plan.md - Implementation plan
- REVIEW.md - Style and formatting rules to enforce
<if R > 1, also read:>
- .ai/<PROJECT>/<LETTER>/review<R-1>.md - Previous review (to see what was already addressed)
Then run `git diff` to see all uncommitted changes made by the implementation. Implementation agents do not commit, so `git diff` shows exactly the current feature's changes.
Then read the modified source files in full to understand changes in context.
Perform a thorough code review.
REVIEW CRITERIA (in order of importance):
1. **Correctness and safety**: Obvious logic errors, missing null checks at API boundaries, potential crashes, use-after-free, dangling references, race conditions. This is the highest priority — bugs and safety issues must be caught first. Do NOT nitpick internal code that relies on framework guarantees.
2. **Dead code**: Any code added or left behind that is never called or used, within the scope of the changes. Unused variables, unreachable branches, leftover scaffolding.
3. **Redundant changes**: Changes in the diff that have no functional effect — moving declarations or code blocks to a different location without reason, reformatting untouched code, reordering includes or fields with no purpose. Every line in the diff should serve the feature. If a file appears in `git diff` but contains only no-op rearrangements, flag it for revert.
4. **Code duplication**: Unnecessary repetition of logic that should be shared. Look for near-identical blocks that differ only in minor details and could be unified.
5. **Wrong placement**: Code added to a module where it doesn't logically belong. If another existing module is a clearly better fit for the new code, flag it. Consider the existing module boundaries and responsibilities visible in context.md.
6. **Function decomposition**: For longer functions (roughly 50+ lines), consider whether a logical sub-task could be cleanly extracted into a separate function. This is NOT a hard rule — a 100-line function that flows naturally and isn't easily divisible is perfectly fine. But sometimes even a 20-line function contains a clear isolated subtask that reads better as two 10-line functions. The key is to think about it each time: does extracting improve readability and reduce cognitive load, or does it just scatter logic across call sites for no real benefit? Only suggest extraction when there's a genuinely self-contained piece of logic with a clear name and purpose.
7. **Module structure**: Only in exceptional cases — if a large amount of newly added code (hundreds of lines) is logically distinct from the rest of its host module, suggest extracting it into a new module. But do NOT suggest new modules lightly: every module adds significant build overhead due to PCH and heavy template usage. Only suggest this when the new code is both large enough AND logically separated enough to justify it. At the same time, don't let modules grow into multi-thousand-line monoliths either.
8. **Style compliance**: Verify adherence to REVIEW.md rules (empty line before closing brace, operators at start of continuation lines, minimize type checks with direct cast instead of is+as, no if-with-initializer when simpler alternatives exist) and AGENTS.md conventions (no unnecessary comments, `auto` usage, no hardcoded sizes — must use .style definitions), etc.
IMPORTANT GUIDELINES:
- Review ONLY the changes made, not pre-existing code in the repository.
- Be pragmatic. Don't suggest changes for the sake of it. Each suggestion should have a clear, concrete benefit.
- Don't suggest adding comments, docstrings, or type annotations — the codebase style avoids these.
- Don't suggest error handling for impossible scenarios or over-engineering.
Write your review to: .ai/<PROJECT>/<LETTER>/review<R>.md
The review document should contain:
## Code Review - Iteration <R>
## Summary
<1-2 sentence overall assessment>
## Verdict: <APPROVED or NEEDS_CHANGES>
<If APPROVED, stop here. Everything looks good.>
<If NEEDS_CHANGES, continue with:>
## Changes Required
### <Issue 1 title>
- **Category**: <dead code | duplication | wrong placement | function decomposition | module structure | style | correctness>
- **File(s)**: <file paths>
- **Problem**: <clear description of what's wrong>
- **Fix**: <specific description of what to change>
### <Issue 2 title>
...
Keep the list focused. Only include issues that genuinely improve the code. If you find yourself listing more than ~5-6 issues, prioritize the most impactful ones.
When finished, report your verdict clearly as: APPROVED or NEEDS_CHANGES.
```
### Step 6b: Review Fix Agent
```text
You are a review fix agent. You implement improvements identified during code review.
Read these files:
- .ai/<PROJECT>/<LETTER>/context.md - Codebase context
- .ai/<PROJECT>/<LETTER>/plan.md - Original implementation plan
- .ai/<PROJECT>/<LETTER>/review<R>.md - Code review with required changes
Then read the source files mentioned in the review.
YOUR TASK: Implement ALL changes listed in review<R>.md.
For each issue in the review:
1. Read the relevant source file(s).
2. Make the specified change.
3. Verify the change makes sense in context.
After all changes are made:
1. Build (from repository root): cmake --build ./out --config Debug --target Telegram
2. If the build fails, fix build errors and rebuild until it passes.
3. If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry.
Rules:
- Implement exactly the changes from the review, nothing more.
- Follow AGENTS.md coding conventions.
- Do NOT modify .ai/ files.
When finished, report what changes were made.
```
## Completion
When all phases including build verification and code review are done:
1. Read the final `plan.md` and report the summary to the user.
2. Show which files were modified/created.
3. Note any issues encountered during implementation.
4. Summarize code review iterations: how many rounds, what was found and fixed, or if it was approved on first pass.
5. Calculate and display the total elapsed time since `$START_TIME` (format as `Xh Ym Zs`, omitting zero components — e.g. `12m 34s` or `1h 5m 12s`).
6. Remind the user of the project name so they can request follow-up tasks within the same project.
## Error Handling
- If any phase fails or gets stuck, report the issue to the user and ask how to proceed.
- If context.md or plan.md is not written properly by a phase, re-run that phase with more specific instructions.
- If build errors persist after the build phase's attempts, report the remaining errors to the user.
- If a review fix phase introduces new build errors that it cannot resolve, report to the user.
## Reasoning Effort
Phases 2 (Plan), 3 (Assessment), and 6a (Review) require elevated reasoning. Pass `-c model_reasoning_effort="xhigh"` on those `codex exec` invocations. All other phases use the default reasoning effort.
## Example Runner Commands
```powershell
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-1-context.jsonl
codex exec --json -C <REPO_ROOT> -c model_reasoning_effort="xhigh" "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-2-plan.jsonl
codex exec --json -C <REPO_ROOT> -c model_reasoning_effort="xhigh" "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-3-assess.jsonl
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-4-impl-N.jsonl
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-5-build.jsonl
codex exec --json -C <REPO_ROOT> -c model_reasoning_effort="xhigh" "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-6a-review-R.jsonl
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-6b-fix-R.jsonl
```
@@ -1,6 +1,6 @@
---
name: task-think
description: Orchestrate a multi-phase implementation workflow for this repository with artifact files under .ai/<project-name>/<letter>/ and optional fresh codex exec child runs per phase. Use when the user wants one prompt to drive context gathering, planning, implementation, verification, and review iterations while keeping the main session context clean.
description: Orchestrate a multi-phase implementation workflow for this repository with artifact files under .ai/<project-name>/<letter>/ and fresh codex exec child runs per phase. Use when the user wants one prompt to drive context gathering, planning, plan assessment, implementation, build verification, and review iterations while keeping the main session context clean.
---
# Task Pipeline
@@ -12,7 +12,7 @@ Run a full implementation workflow with repository artifacts and clear phase bou
Collect:
- task description
- optional project name (if missing, derive a short kebab-case name)
- optional constraints (files, architecture, deadlines, risk tolerance)
- optional constraints (files, architecture, risk tolerance)
- optional screenshot paths
If screenshots are attached in UI but not present as files, write a brief textual summary in `.ai/<project-name>/about.md` so child runs can consume the requirements.
@@ -28,11 +28,13 @@ Project structure:
a/ # First task
context.md # Gathered codebase context for this task
plan.md # Implementation plan
review.md # Code review document
review1.md # Code review documents (up to 3 iterations)
review2.md
review3.md
b/ # Follow-up task
context.md
plan.md
review.md
review1.md
c/ # Another follow-up task
...
```
@@ -46,41 +48,55 @@ Create and maintain:
- `.ai/<project-name>/about.md`
- `.ai/<project-name>/<letter>/context.md`
- `.ai/<project-name>/<letter>/plan.md`
- `.ai/<project-name>/<letter>/implementation.md`
- `.ai/<project-name>/<letter>/review.md`
- `.ai/<project-name>/<letter>/review<R>.md` (up to 3 review iterations)
- `.ai/<project-name>/<letter>/logs/phase-*.jsonl` (when running child `codex exec`)
## Execution Mode
## Phases
Run `codex exec --json` child sessions for each phase.
The workflow runs these phases sequentially via `codex exec --json` child sessions:
## Fresh-Run Mode Procedure
1. Detect follow-up vs new project (check if first token of task description matches an existing project name with `about.md`).
2. For new projects: pick unique short name, create `.ai/<project-name>/` and `.ai/<project-name>/a/`.
3. For follow-up tasks: find latest task letter, create `.ai/<project-name>/<next-letter>/`.
4. Run child phase sessions sequentially, waiting for each to finish.
5. After each phase, validate artifact file exists and has substantive content.
6. Summarize status in the parent session after each phase.
7. Stop immediately on blocking errors and report exact blocker.
1. **Phase 0: Setup** — Record start time, detect follow-up vs new project, create directories.
2. **Phase 1: Context Gathering** — Read codebase, write `about.md` and `context.md`. (Phase 1F for follow-ups.)
3. **Phase 2: Planning** — Read context, write detailed `plan.md` with numbered steps grouped into phases.
4. **Phase 3: Plan Assessment** — Review and refine the plan for correctness, completeness, code quality, and phase sizing.
5. **Phase 4: Implementation** — One child session per plan phase. Each implements only its assigned phase and updates `plan.md` status.
6. **Phase 5: Build Verification** — Build the project, fix any build errors. Skip if no source code was modified.
7. **Phase 6: Code Review Loop** — Up to 3 review-fix iterations:
- 6a: Review agent writes `review<R>.md` with verdict (APPROVED or NEEDS_CHANGES).
- 6b: Fix agent implements review changes and rebuilds.
- Loop until APPROVED or R > 3.
Use the phase prompt templates in `PROMPTS.md`.
## Execution Mode
Run `codex exec --json` child sessions for each phase. Wait for each to finish before starting the next. After each phase, validate that the expected artifact file exists and has substantive content.
Phases that require elevated reasoning (Planning, Plan Assessment, Code Review) must use `-c model_reasoning_effort="xhigh"`. See example commands in `PROMPTS.md`.
## Verification Rules
- If build or test commands fail due to file locks or access-denied outputs, stop and ask the user to close locking processes before retrying.
- If build or test commands fail due to file locks or access-denied outputs (C1041, LNK1104), stop and ask the user to close locking processes before retrying.
- Never claim completion without:
- implemented code changes present
- build/test attempt results recorded
- build attempt results recorded
- review pass documented with any follow-up fixes
## Completion Criteria
Mark complete only when:
- plan phases are done
- verification results are recorded
- review issues are addressed or explicitly deferred with rationale
- Remind the user of the project name so they can request follow-up tasks within the same project.
- All plan phases are done
- Build verification is recorded
- Review issues are addressed or explicitly deferred with rationale
- Display total elapsed time since start (format: `Xh Ym Zs`, omitting zero components)
- Remind the user of the project name so they can request follow-up tasks within the same project
## Error Handling
- If any phase fails or gets stuck, report the issue to the user and ask how to proceed.
- If context.md or plan.md is not written properly by a phase, re-run that phase with more specific instructions.
- If build errors persist after the build phase's attempts, report the remaining errors to the user.
- If a review fix phase introduces new build errors that it cannot resolve, report to the user.
## User Invocation
+2 -10
View File
@@ -37,16 +37,8 @@ ls .ai/
Suggest a name to the user or let them specify one directly via $ARGUMENTS.
### 3. Use /ultrathink for Planning
Before writing the prompt, use `/ultrathink` to carefully plan:
- The structure of the prompt
- What context the autonomous agent needs
- How tasks should be broken down
- What patterns/examples to include
- Edge cases and error handling
### 4. Create the Folder and Files
### 3. Create the Folder and Files
Create `.ai/<featurename>/`:
@@ -91,7 +83,7 @@ IMPORTANT: Never try to commit files in .ai/
}
```
### 5. Iterate with the User
### 4. Iterate with the User
After creating initial files, the user may want to:
- Add more tasks to tasks.json
+18 -15
View File
@@ -38,6 +38,8 @@ Project structure:
## Phase 0: Setup
**Record the current time now** (using `Get-Date` in PowerShell or equivalent) and store it as `$START_TIME`. You will use this at the end to display total elapsed time.
⚠️ **CRITICAL: Follow-up detection MUST happen FIRST, before anything else.**
### Step 0a: Follow-up detection (MANDATORY — do this BEFORE understanding the task)
@@ -80,10 +82,10 @@ You are a context-gathering agent for a large C++ codebase (Telegram Desktop).
TASK: <paste the user's task description here>
YOUR JOB: Read CLAUDE.md, inspect the codebase, find ALL files and code relevant to this task, and write two documents.
YOUR JOB: Read AGENTS.md, inspect the codebase, find ALL files and code relevant to this task, and write two documents.
Steps:
1. Read CLAUDE.md for project conventions and build instructions.
1. Read AGENTS.md for project conventions and build instructions.
2. Search the codebase for files, classes, functions, and patterns related to the task.
3. Read all potentially relevant files. Be thorough - read more rather than less.
4. For each relevant file, note:
@@ -140,7 +142,7 @@ NEW TASK: <paste the follow-up task description here>
YOUR JOB: Read the existing project state, gather any additional context needed, and produce fresh documents for the new task.
Steps:
1. Read CLAUDE.md for project conventions and build instructions.
1. Read AGENTS.md for project conventions and build instructions.
2. Read .ai/<project-name>/about.md — this is the project-level blueprint describing everything done so far.
3. Read .ai/<project-name>/<previous-letter>/context.md — this is the previous task's gathered context.
4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context.
@@ -201,7 +203,7 @@ Read these files:
- .ai/<project-name>/<letter>/context.md - Contains all gathered context for this task
- Then read the specific source files referenced in context.md to understand the code deeply.
Use /ultrathink to reason carefully about the implementation approach.
Think carefully about the implementation approach.
Create a detailed plan in: .ai/<project-name>/<letter>/plan.md
@@ -262,11 +264,11 @@ Read these files:
- .ai/<project-name>/<letter>/plan.md
- Then read the actual source files referenced to verify the plan makes sense.
Use /ultrathink to assess the plan:
Carefully assess the plan:
1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types?
2. **Completeness**: Are there missing steps? Edge cases not handled?
3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from CLAUDE.md?
3. **Code quality**: Will the plan minimize code duplication? Does it follow existing codebase patterns from AGENTS.md?
4. **Design**: Could the approach be improved? Are there better patterns already used in the codebase?
5. **Phase sizing**: Each phase should be implementable by a single agent in one session. If a phase has more than ~8-10 substantive code changes, split it further.
@@ -303,7 +305,7 @@ YOUR TASK: Implement ONLY Phase <N> from the plan:
Rules:
- Follow the plan precisely
- Follow CLAUDE.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.)
- Follow AGENTS.md coding conventions (no comments except complex algorithms, use auto, empty line before closing brace, etc.)
- Do NOT modify .ai/ files except to update the Status section in plan.md
- When done, update plan.md Status section: change `- [ ] Phase <N>: ...` to `- [x] Phase <N>: ...`
- Do NOT work on other phases
@@ -332,18 +334,18 @@ Read these files:
The implementation is complete. Your job is to build the project and fix any build errors.
Steps:
1. Run: cmake --build "c:\Telegram\tdesktop\out" --config Debug --target Telegram
1. Run (from repository root): cmake --build ./out --config Debug --target Telegram
2. If the build succeeds, update plan.md: change `- [ ] Build verification` to `- [x] Build verification`
3. If the build fails:
a. Read the error messages carefully
b. Read the relevant source files
c. Fix the errors in accordance with the plan and CLAUDE.md conventions
c. Fix the errors in accordance with the plan and AGENTS.md conventions
d. Rebuild and repeat until the build passes
e. Update plan.md status when done
Rules:
- Only fix build errors, do not refactor or improve code
- Follow CLAUDE.md conventions
- Follow AGENTS.md conventions
- If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry
When finished, report the build result.
@@ -391,7 +393,7 @@ Then run `git diff` to see all uncommitted changes made by the implementation. I
Then read the modified source files in full to understand changes in context.
Use /ultrathink to perform a thorough code review.
Perform a thorough code review.
REVIEW CRITERIA (in order of importance):
@@ -409,7 +411,7 @@ REVIEW CRITERIA (in order of importance):
7. **Module structure**: Only in exceptional cases — if a large amount of newly added code (hundreds of lines) is logically distinct from the rest of its host module, suggest extracting it into a new module. But do NOT suggest new modules lightly: every module adds significant build overhead due to PCH and heavy template usage. Only suggest this when the new code is both large enough AND logically separated enough to justify it. At the same time, don't let modules grow into multi-thousand-line monoliths either.
8. **Style compliance**: Verify adherence to REVIEW.md rules (empty line before closing brace, operators at start of continuation lines, minimize type checks with direct cast instead of is+as, no if-with-initializer when simpler alternatives exist) and CLAUDE.md conventions (no unnecessary comments, `auto` usage, no hardcoded sizes — must use .style definitions), etc.
8. **Style compliance**: Verify adherence to REVIEW.md rules (empty line before closing brace, operators at start of continuation lines, minimize type checks with direct cast instead of is+as, no if-with-initializer when simpler alternatives exist) and AGENTS.md conventions (no unnecessary comments, `auto` usage, no hardcoded sizes — must use .style definitions), etc.
IMPORTANT GUIDELINES:
- Review ONLY the changes made, not pre-existing code in the repository.
@@ -472,13 +474,13 @@ For each issue in the review:
3. Verify the change makes sense in context.
After all changes are made:
1. Build: cmake --build "c:\Telegram\tdesktop\out" --config Debug --target Telegram
1. Build (from repository root): cmake --build ./out --config Debug --target Telegram
2. If the build fails, fix build errors and rebuild until it passes.
3. If build fails with file-locked errors (C1041, LNK1104), STOP and report - do not retry.
Rules:
- Implement exactly the changes from the review, nothing more.
- Follow CLAUDE.md coding conventions.
- Follow AGENTS.md coding conventions.
- Do NOT modify .ai/ files.
When finished, report what changes were made.
@@ -493,7 +495,8 @@ When all phases including build verification and code review are done:
2. Show which files were modified/created.
3. Note any issues encountered during implementation.
4. Summarize code review iterations: how many rounds, what was found and fixed, or if it was approved on first pass.
5. Remind the user of the project name so they can use `/task <project-name> <follow-up description>` for follow-up changes.
5. Calculate and display the total elapsed time since `$START_TIME` (format as `Xh Ym Zs`, omitting zero components — e.g. `12m 34s` or `1h 5m 12s`).
6. Remind the user of the project name so they can use `/task <project-name> <follow-up description>` for follow-up changes.
## Error Handling
+5 -5
View File
@@ -100,7 +100,7 @@ YOUR JOB:
- [ ] Testing
Assessed: yes
Use /ultrathink to reason carefully. The follow-up plan should be self-contained enough that an implementation agent can execute it by reading context.md and the updated plan.md.
Reason carefully. The follow-up plan should be self-contained enough that an implementation agent can execute it by reading context.md and the updated plan.md.
```
After this agent completes, read `plan.md` to verify the follow-up plan was written. Then proceed to Phase 4 (Implementation), using the follow-up phases (F1, F2, etc.) instead of the original phases. After implementation and build verification, proceed to Stage 2 (Testing Loop) as normal.
@@ -163,7 +163,7 @@ Read these files:
- .ai/<feature-name>/context.md - Contains all gathered context
- Then read the specific source files referenced in context.md to understand the code deeply.
Use /ultrathink to reason carefully about the implementation approach.
Think carefully about the implementation approach.
Create a detailed plan in: .ai/<feature-name>/plan.md
@@ -224,7 +224,7 @@ Read these files:
- .ai/<feature-name>/plan.md
- Then read the actual source files referenced to verify the plan makes sense.
Use /ultrathink to assess the plan:
Carefully assess the plan:
1. **Correctness**: Are the file paths and line references accurate? Does the plan reference real functions and types?
2. **Completeness**: Are there missing steps? Edge cases not handled?
@@ -367,7 +367,7 @@ YOUR TASKS:
- Decide: reuse/modify existing test code or start fresh.
3. **Plan the test code.**
Use /ultrathink to design test code that will verify the implementation works correctly.
Carefully design test code that will verify the implementation works correctly.
The test code must:
- Be wrapped in `#ifdef _DEBUG` blocks so it only runs in Debug builds
@@ -530,7 +530,7 @@ Read these files:
- .ai/<feature-name>/result<N>.md
<if N > 1, also read previous test/result pairs for history>
Use /ultrathink to carefully analyze the test results.
Carefully analyze the test results.
DECIDE one of three outcomes:
+1 -1
View File
@@ -173,7 +173,7 @@ Do exactly ONE task per iteration.
## Steps
1. Read tasks.json and find the most suitable task to implement (it can be first uncompleted task or it can be some task in the middle, if it is better suited to be implemented right now, respecting dependencies)
2. Use /ultrathink to plan the implementation carefully
2. Plan the implementation carefully
3. Implement that ONE task only
4. After successful implementation:
$AfterImplementation
-227
View File
@@ -1,227 +0,0 @@
# Phase Prompts
Use these templates for `codex exec --json` child runs. Replace `<TASK>`, `<PROJECT>`, `<LETTER>`, and `<REPO_ROOT>`.
## Phase 0: Setup
Before running any phase prompts, the orchestrator must determine whether this is a new project or a follow-up task.
**Follow-up detection:**
1. Extract the first word/token from the task description. Call it `FIRST_TOKEN`.
2. Check if `.ai/<FIRST_TOKEN>/about.md` exists.
3. If it exists: this is a **follow-up task**. The project name is `FIRST_TOKEN`. The task description is everything after `FIRST_TOKEN`.
4. If it does not exist: this is a **new project**. The full input is the task description.
**For new projects:**
- List existing `.ai/` folders to pick a unique short name (1-2 lowercase words, hyphen-separated).
- Create `.ai/<PROJECT>/` and `.ai/<PROJECT>/a/` and `logs/`.
- Set `<LETTER>` = `a`.
**For follow-up tasks:**
- Scan `.ai/<PROJECT>/` for existing task folders (`a/`, `b/`, ...). Find the latest one (highest letter).
- The new task letter = next letter in sequence.
- Create `.ai/<PROJECT>/<LETTER>/` and `logs/`.
Then proceed to Phase 1. Follow-up tasks do NOT skip context gathering — they go through a modified version of it.
## Phase 1: Context (New Project, letter = `a`)
```text
You are the context phase for task "<TASK>" in repository <REPO_ROOT>.
Read CLAUDE.md for the basic coding rules and guidelines.
Read AGENTS.md and all relevant source files. Write TWO documents:
### File 1: .ai/<PROJECT>/about.md
NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. No planning, implementation, or review agent will ever read it.
Write it as if the project is already fully implemented and working. It should contain:
- **Project**: What this project does (feature description, goals, scope)
- **Architecture**: High-level architectural decisions, which modules are involved, how they interact
- **Key Design Decisions**: Important choices made about the approach
- **Relevant Codebase Areas**: Which parts of the codebase this project touches, key types and APIs involved
Do NOT include temporal state like "Current State", "Pending Changes", "Not yet implemented", "TODO", or any other framing that distinguishes between "done" and "not done". Describe the project as a complete, coherent whole.
### File 2: .ai/<PROJECT>/a/context.md
This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. Include:
1. Task description restated clearly
2. Relevant files with line ranges and why they matter
3. Existing patterns to follow (with code snippets)
4. Data structures, types, classes
5. API methods (from api.tl if applicable)
6. UI styles (from .style files if applicable)
7. Localization (from lang.strings if applicable)
8. Build info and verification hooks
9. Reference implementations of similar features
10. Risks and unknowns
Be extremely thorough. Another agent with NO prior context will read this file and must be able to understand everything needed to implement the task.
Do not implement code in this phase.
```
## Phase 1F: Context (Follow-up Task, letter = `b`, `c`, ...)
```text
You are the context phase for a follow-up task on an existing project in repository <REPO_ROOT>.
NEW TASK: <TASK>
Read CLAUDE.md for the basic coding rules and guidelines.
Steps:
1. Read AGENTS.md for project conventions.
2. Read .ai/<PROJECT>/about.md — the project-level blueprint describing everything done so far.
3. Read .ai/<PROJECT>/<PREV_LETTER>/context.md — the previous task's gathered context.
4. Understand what has already been implemented by reading the actual source files referenced in about.md and the previous context.
5. Search the codebase for any ADDITIONAL files, classes, functions, and patterns relevant to the new task but not already covered.
6. Read all newly relevant files thoroughly.
Write TWO files:
### File 1: .ai/<PROJECT>/about.md (REWRITE)
NOTE: This file is NOT used by any agent in the current task. It exists solely as a starting point for a FUTURE follow-up task's context gatherer. You are rewriting it now so that the next follow-up has an accurate project overview to start from.
REWRITE this file (not append). The new about.md must be a single coherent document that describes the project as if everything — including this new task's changes — is already fully implemented and working.
It should incorporate:
- Everything from the old about.md that is still accurate and relevant
- The new task's functionality described as part of the project (not as "changes to make")
- Any changed design decisions or architectural updates from the new task requirements
It should NOT contain:
- Any temporal state: "Current State", "Pending Changes", "TODO", "Not yet implemented"
- History of how requirements changed between tasks
- References to "the old approach" vs "the new approach"
- Task-by-task changelog or timeline
- Any distinction between "what was done before" and "what this task adds"
- Information that contradicts the new task requirements
### File 2: .ai/<PROJECT>/<LETTER>/context.md
This is the PRIMARY document — all downstream agents (planning, implementation, review) will read ONLY this file. It must be completely self-contained. about.md will NOT be available to them.
It should contain:
- **Task Description**: The new task restated clearly, with enough project background that an implementation agent can understand it without reading any other .ai/ files
- **Relevant Files**: Every file path with line ranges relevant to THIS task (including files modified by previous tasks and any newly relevant files)
- **Key Code Patterns**: How similar things are done in the codebase
- **Data Structures**: Relevant types, structs, classes
- **API Methods**: Any TL schema methods involved
- **UI Styles**: Any relevant style definitions
- **Localization**: Any relevant string keys
- **Build Info**: Build command and any special notes
- **Reference Implementations**: Similar features that can serve as templates
Be extremely thorough. Another agent with NO prior context will read ONLY this file and must be able to understand everything needed to implement the new task. Do NOT assume the reader has seen about.md or any previous task files.
Do not implement code in this phase.
```
## Phase 2: Plan
```text
You are the planning phase for task "<TASK>" in repository <REPO_ROOT>.
Read CLAUDE.md for the basic coding rules and guidelines.
Read:
- .ai/<PROJECT>/<LETTER>/context.md
Then read the specific source files referenced in context.md to understand the code deeply.
Create:
- .ai/<PROJECT>/<LETTER>/plan.md
Plan requirements:
1. Concrete file-level edits
2. Ordered phases (each phase implementable by a single agent, roughly <=8 steps)
3. Verification commands
4. Rollback/risk notes
5. Status section with checklist of phases, build verification, and code review
```
## Phase 3: Implement
```text
You are the implementation phase for task "<TASK>" in repository <REPO_ROOT>.
Read CLAUDE.md for the basic coding rules and guidelines.
Read:
- .ai/<PROJECT>/<LETTER>/context.md
- .ai/<PROJECT>/<LETTER>/plan.md
Implement the plan in code. Then write:
- .ai/<PROJECT>/<LETTER>/implementation.md
Include:
1. Files changed
2. What was implemented
3. Any deviations from plan and why
4. Update plan.md Status section to mark completed phases
```
## Phase 4: Verify
```text
You are the verification phase for task "<TASK>" in repository <REPO_ROOT>.
Read CLAUDE.md for the basic coding rules and guidelines.
Read:
- .ai/<PROJECT>/<LETTER>/plan.md
- .ai/<PROJECT>/<LETTER>/implementation.md
Run the relevant build/test commands from AGENTS.md and plan.md.
Append results to:
- .ai/<PROJECT>/<LETTER>/implementation.md
If blocked by locked files or access errors, stop and report exact blocker.
```
## Phase 5: Review
```text
You are the review phase for task "<TASK>" in repository <REPO_ROOT>.
Read AGENTS.md for the basic coding rules and guidelines.
Read REVIEW.md for the style and formatting rules you must enforce.
Read:
- .ai/<PROJECT>/<LETTER>/context.md
- .ai/<PROJECT>/<LETTER>/plan.md
- .ai/<PROJECT>/<LETTER>/implementation.md
Run `git diff` to see all uncommitted changes made by the implementation. Implementation phases do not commit, so `git diff` shows exactly the current feature's changes. Then read the modified source files in full.
Perform a code review using these criteria (in order of importance):
1. Correctness and safety: logic errors, null-check gaps at API boundaries, crashes, use-after-free, dangling references, race conditions.
2. Dead code: code added or left behind that is never called or used. Unused variables, unreachable branches, leftover scaffolding.
3. Redundant changes: changes in the diff with no functional effect — moving declarations or code blocks without reason, reformatting untouched code, reordering includes or fields with no purpose. Every line in the diff should serve the feature. If a file appears in `git diff` but contains only no-op rearrangements, flag it for revert.
4. Code duplication: unnecessary repetition of logic that should be shared.
5. Wrong placement: code added to a module where it doesn't logically belong.
6. Function decomposition: for longer functions (~50+ lines), consider whether a sub-task could be cleanly extracted. Only suggest when there is a genuinely self-contained piece of logic.
7. Module structure: only flag if a large amount of new code (hundreds of lines) is logically distinct from its host module.
8. Style compliance: verify adherence to REVIEW.md rules and AGENTS.md conventions.
Write:
- .ai/<PROJECT>/<LETTER>/review.md
If issues are found, implement fixes and update implementation.md/review.md with final status.
```
## Example Runner Commands
```powershell
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-1-context.jsonl
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-2-plan.jsonl
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-3-implement.jsonl
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-4-verify.jsonl
codex exec --json -C <REPO_ROOT> "<PHASE_PROMPT>" | Tee-Object .ai/<PROJECT>/<LETTER>/logs/phase-5-review.jsonl
```
-105
View File
@@ -1,105 +0,0 @@
---
description: For tasks requiring sending Telegram server API requests or working with generated API types.
globs:
alwaysApply: false
---
# 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.
-164
View File
@@ -1,164 +0,0 @@
---
description: For tasks requiring changing or adding user facing phrases and text parts.
globs:
alwaysApply: false
---
# 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.
-216
View File
@@ -1,216 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# 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::on_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::on_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::on_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::on_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::on_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::on_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::on_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 `on_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::on_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::on_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::on_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 `
-154
View File
@@ -1,154 +0,0 @@
---
description: For tasks requiring working with user facing UI components.
globs:
alwaysApply: false
---
# 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`).
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""Convert changelog.txt to a static HTML page for GitHub Pages."""
import re
import shutil
import sys
import html
from pathlib import Path
MONTHS = [
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
]
VERSION_RE = re.compile(
r"^(\d+\.\d+(?:\.\d+)?)\s*" # version number
r"(?:(alpha|beta|dev|stable)\s*)?" # optional tag
r"\((\d{2})\.(\d{2})\.(\d{2,4})\)$" # date (DD.MM.YY or DD.MM.YYYY)
)
def parse_date(day: str, month: str, year: str) -> tuple[str, str, str]:
"""Return (sort_key, raw_display, full_display) from DD, MM, YY strings."""
y = int(year)
if y < 100:
y += 2000
m = int(month)
d = int(day)
sort_key = f"{y:04d}-{m:02d}-{d:02d}"
raw_display = f"{day}.{month}.{year}"
full_display = f"{d} {MONTHS[m]} {y}"
return sort_key, raw_display, full_display
def parse_changelog(text: str) -> list[dict]:
entries = []
current = None
for raw_line in text.splitlines():
line = raw_line.rstrip()
m = VERSION_RE.match(line)
if m:
if current:
entries.append(current)
version, tag, day, month, year = m.groups()
sort_key, raw_date, full_date = parse_date(day, month, year)
current = {
"version": version,
"tag": tag or "",
"date": raw_date,
"full_date": full_date,
"sort_key": sort_key,
"lines": [],
}
elif current is not None:
# Skip blank lines at the start
if not line and not current["lines"]:
continue
# Skip stray artifact lines
if line.strip() in ("),", "),"):
continue
current["lines"].append(line)
if current:
entries.append(current)
# Trim trailing blank lines from each entry
for entry in entries:
while entry["lines"] and not entry["lines"][-1]:
entry["lines"].pop()
return entries
def render_entry(entry: dict) -> str:
version = html.escape(entry["version"])
tag = entry["tag"]
date = html.escape(entry["date"])
anchor = f"v{version}"
tag_html = ""
if tag and tag not in ("stable",):
tag_html = f' {html.escape(tag)}'
parts = [
f'<article class="entry" id="{anchor}">',
f' <h2><a class="anchor" href="#{anchor}"></a>'
f'{version}{tag_html}'
f' <time>{date}</time></h2>',
]
in_list = False
for line in entry["lines"]:
stripped = line.lstrip()
if stripped.startswith("- ") or stripped.startswith("\u2014 "):
# Bullet point (- or em dash)
if not in_list:
parts.append(" <ul>")
in_list = True
bullet_text = stripped[2:]
parts.append(f" <li>{html.escape(bullet_text)}</li>")
else:
if in_list:
parts.append(" </ul>")
in_list = False
if stripped:
parts.append(f" <p>{html.escape(stripped)}</p>")
if in_list:
parts.append(" </ul>")
parts.append("</article>")
return "\n".join(parts)
def build_html(entries: list[dict]) -> str:
count = len(entries)
first_date = entries[-1]["full_date"] if entries else ""
latest_version = entries[0]["version"] if entries else ""
entries_html = "\n\n".join(render_entry(e) for e in entries)
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Version history</title>
<link rel="icon" type="image/png" sizes="32x32" href="icon32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icon16.png">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font: 12px / 18px "Lucida Grande", "Lucida Sans Unicode", Arial,
Helvetica, Verdana, sans-serif;
background: #fff;
color: #000;
}}
header {{
background: #1d98dc;
color: #fff;
padding: 2rem 1.5rem;
text-align: center;
}}
header h1 {{ font-size: 18px; font-weight: 700; }}
header p {{ opacity: .85; margin-top: 4px; font-size: 12px; }}
.container {{
max-width: 600px;
margin: 0 auto;
padding: 20px 15px;
}}
.search-box {{
position: sticky;
top: 0;
z-index: 10;
background: #fff;
padding: 8px 0 12px;
}}
.search-box input {{
width: 100%;
padding: 6px 10px;
font: 12px / 18px "Lucida Grande", "Lucida Sans Unicode", Arial,
Helvetica, Verdana, sans-serif;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
color: #000;
outline: none;
}}
.search-box input:focus {{ border-color: #1d98dc; }}
.entry {{
padding: 14px 0 4px;
scroll-margin-top: 48px;
}}
.entry h2 {{
font-size: 16px;
font-weight: 700;
line-height: 22px;
margin-bottom: 6px;
position: relative;
}}
.entry h2 .anchor {{
position: absolute;
left: -24px;
top: 0;
width: 24px;
height: 22px;
display: block;
opacity: 0;
transition: opacity .15s;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 16 16'%3E%3Cpath fill='%23168acd' d='M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0z'/%3E%3C/svg%3E") 0 center / 18px no-repeat;
cursor: pointer;
}}
.entry h2:hover .anchor {{ opacity: .6; }}
.entry h2 .anchor:hover {{ opacity: 1; }}
.entry h2 time {{
font-size: 12px;
font-weight: 400;
color: #999;
margin-left: 6px;
}}
.entry ul {{
margin: 0 0 4px 8px;
padding: 0;
list-style: none;
}}
.entry li {{
padding: 2px 0 2px 16px;
position: relative;
color: #333;
}}
.entry li::before {{
content: "";
position: absolute;
left: 0;
top: 9px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #009be1;
}}
.entry p {{
margin: 4px 0;
color: #555;
font-style: italic;
}}
.hidden {{ display: none; }}
footer {{
text-align: center;
padding: 24px 15px;
font-size: 11px;
color: #999;
}}
footer a {{ color: #168acd; text-decoration: none; }}
footer a:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<header>
<h1>Version history</h1>
<p>{count} releases since {first_date} &middot; latest: {latest_version}</p>
</header>
<div class="container">
<div class="search-box">
<input type="text" id="search" placeholder="Search versions and changes\u2026"
autocomplete="off" spellcheck="false">
</div>
<div id="entries">
{entries_html}
</div>
</div>
<footer>
Auto-generated from
<a href="https://github.com/telegramdesktop/tdesktop/blob/dev/changelog.txt">changelog.txt</a>.
Source code is published under
<a href="https://github.com/telegramdesktop/tdesktop">GPL v3</a>.
</footer>
<script>
(function() {{
var input = document.getElementById('search');
var entries = document.querySelectorAll('.entry');
var timer;
input.addEventListener('input', function() {{
clearTimeout(timer);
timer = setTimeout(function() {{
var q = input.value.toLowerCase().trim();
entries.forEach(function(el) {{
if (!q) {{
el.classList.remove('hidden');
}} else {{
el.classList.toggle('hidden', el.textContent.toLowerCase().indexOf(q) === -1);
}}
}});
}}, 150);
}});
// Anchor links: copy URL on click
document.addEventListener('click', function(e) {{
var anchor = e.target.closest('.anchor');
if (!anchor) return;
e.preventDefault();
var url = location.origin + location.pathname + anchor.getAttribute('href');
history.replaceState(null, '', anchor.getAttribute('href'));
if (navigator.clipboard) {{
navigator.clipboard.writeText(url);
}}
}});
}})();
</script>
</body>
</html>"""
def main():
repo = Path(__file__).resolve().parent.parent.parent
src = repo / "changelog.txt"
if len(sys.argv) > 1:
src = Path(sys.argv[1])
out = repo / "docs" / "changelog" / "index.html"
if len(sys.argv) > 2:
out = Path(sys.argv[2])
text = src.read_text(encoding="utf-8")
entries = parse_changelog(text)
html_content = build_html(entries)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(html_content, encoding="utf-8")
# Copy favicon files from resources
icons_src = repo / "Telegram" / "Resources" / "art"
for name in ("icon16.png", "icon32.png"):
icon = icons_src / name
if icon.exists():
shutil.copy2(icon, out.parent / name)
print(f"Generated {out} ({len(entries)} entries, {out.stat().st_size:,} bytes)")
if __name__ == "__main__":
main()
+47
View File
@@ -0,0 +1,47 @@
name: Changelog
on:
push:
branches: [dev]
paths: [changelog.txt]
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Generate HTML
run: python .github/scripts/generate_changelog.py
- name: Upload pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: docs
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@v4
+3
View File
@@ -68,5 +68,8 @@ settings.local.json
# AI work folder (session-specific, not for version control)
.ai
# Generated changelog page (built by CI, deployed to GitHub Pages)
/docs/changelog/
# Project specific
Telegram/SourceFiles/_other/packer_private.h
+11 -1
View File
@@ -336,15 +336,25 @@ auto filesTextProducer = tr::lng_files_selected(
- Placeholders use `lt_tag_name, value` pattern
- For `{count}`: immediate uses `int`, reactive uses `rpl::producer<float64>` with `| tr::to_count()`
- Move producers with `std::move` when passing to placeholders
- Rich text projectors — pass as the last argument to produce `TextWithEntities` instead of `QString`. These are smart objects with multiple `operator()` overloads:
- Rich text projectors — these `tr::` helpers serve double duty: as the **last argument** (projector) they set the return type to `TextWithEntities`, and as **placeholder values** they wrap individual substitutions in formatting. Always prefer them over `Ui::Text::Bold()`, `Ui::Text::RichLangValue`, etc. — see REVIEW.md for the full mapping.
- `tr::marked` — basic projection, converts `QString` to `TextWithEntities`
- `tr::rich` — interprets `**bold**`/`__italic__` markup in the string
- `tr::bold`, `tr::italic`, `tr::underline` — wrap text in that formatting
- `tr::link` — wrap as a clickable link
- `tr::url(u"https://..."_q)` — returns a projection that converts text to a link pointing to the given URL; can be passed to `rpl::map` or directly to a `tr::lng_...` call
```cpp
// As last argument (projector):
auto title = tr::lng_export_progress_title(tr::now, tr::bold);
auto text = tr::lng_proxy_incorrect_secret(tr::now, tr::rich);
// As placeholder value wrapper + projector:
auto desc = tr::lng_some_key(
tr::now,
lt_name,
tr::bold(userName),
lt_group,
tr::bold(groupName),
tr::rich);
// Nested tr::lng as placeholder:
auto linked = tr::lng_settings_birthday_contacts(
lt_link,
tr::lng_settings_birthday_contacts_link(tr::url(link)),
+10
View File
@@ -12,6 +12,10 @@ include(cmake/validate_special_target.cmake)
include(cmake/version.cmake)
desktop_app_parse_version(Telegram/build/version)
if (NOT DEFINED CMAKE_CONFIGURATION_TYPES)
set(configuration_types_init 1)
endif()
project(Telegram
LANGUAGES C CXX
VERSION ${desktop_app_version_cmake}
@@ -23,6 +27,12 @@ if (APPLE)
enable_language(OBJC OBJCXX)
endif()
if (configuration_types_init
AND CMAKE_CONFIGURATION_TYPES
AND NOT MinSizeRel IN_LIST CMAKE_CONFIGURATION_TYPES)
set(CMAKE_CONFIGURATION_TYPES "${CMAKE_CONFIGURATION_TYPES};MinSizeRel" CACHE STRING "" FORCE)
endif()
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Telegram)
get_filename_component(third_party_loc "Telegram/ThirdParty" REALPATH)
+77 -1
View File
@@ -4,7 +4,7 @@ This file contains style and formatting rules that the review subagent must chec
## Empty line before closing brace
Always add an empty line before the closing brace of a class (after all private fields):
Always add an empty line before the closing brace of a **class** (which has one or more sections like `public:` / `private:`). Plain **structs** with just data members do NOT get a trailing empty line — they are compact: `struct Foo { data lines; };`.
```cpp
// BAD:
@@ -94,6 +94,82 @@ if (const auto peer = session().data().peerLoaded(peerId)
if (const auto peer = session().data().peerLoaded(peerId)) {
if (const auto user = peer->asUser()) {
## Always initialize variables of basic types
Never leave variables of basic types (`int`, `float`, `bool`, pointers, etc.) uninitialized. Custom types with constructors are fine they initialize themselves. But for any basic type, always provide a default value (`= 0`, `= false`, `= nullptr`, etc.). This applies especially to class fields, where uninitialized members are a persistent source of bugs.
The only exception is performance-critical hot paths where you can prove no read-from-uninitialized-memory occurs. For class fields there is no such exception always initialize.
```cpp
// BAD:
int _bulletLeft;
int _bulletTop;
bool _expanded;
SomeType *_pointer;
// GOOD:
int _bulletLeft = 0;
int _bulletTop = 0;
bool _expanded = false;
SomeType *_pointer = nullptr;
```
## Prefer tr:: projections over Ui::Text:: in localization calls
Inside `tr::lng_...()` calls, always use the `tr::` projection helpers instead of their `Ui::Text::` equivalents. The `tr::` helpers are shorter and work uniformly as both placeholder wrappers and final projectors.
| Instead of | Use |
|---|---|
| `Ui::Text::Bold(x)` | `tr::bold(x)` |
| `Ui::Text::Italic(x)` | `tr::italic(x)` |
| `Ui::Text::RichLangValue` | `tr::rich` |
| `Ui::Text::WithEntities` | `tr::marked` |
```cpp
// BAD - verbose Ui::Text:: functions:
tr::lng_some_key(
tr::now,
lt_name,
Ui::Text::Bold(name),
lt_group,
Ui::Text::Bold(group),
Ui::Text::RichLangValue)
// GOOD - concise tr:: helpers:
tr::lng_some_key(
tr::now,
lt_name,
tr::bold(name),
lt_group,
tr::bold(group),
tr::rich)
```
## Multi-line calls — one argument per line
When a function call doesn't fit on one line, put each argument on its own line. Don't group "logical pairs" on the same line — it creates inconsistent line lengths and makes diffs noisier.
```cpp
// BAD - pairs of arguments sharing lines:
tr::lng_some_key(
tr::now,
lt_name, tr::bold(name),
lt_group, tr::bold(group),
tr::rich)
// GOOD - one argument per line:
tr::lng_some_key(
tr::now,
lt_name,
tr::bold(name),
lt_group,
tr::bold(group),
tr::rich)
// Single-line is fine when everything fits:
auto text = tr::lng_settings_title(tr::now);
```
## std::optional access — avoid value()
Do not call `std::optional::value()` because it throws an exception that is not available on older macOS targets. Use `has_value()`, `value_or()`, `operator bool()`, or `operator*` instead.
+36 -4
View File
@@ -278,6 +278,8 @@ PRIVATE
api/api_chat_participants.h
api/api_cloud_password.cpp
api/api_cloud_password.h
data/data_search_calendar.h
data/data_search_calendar.cpp
api/api_common.cpp
api/api_common.h
api/api_confirm_phone.cpp
@@ -385,6 +387,8 @@ PRIVATE
boxes/peers/edit_members_visible.h
boxes/peers/edit_participant_box.cpp
boxes/peers/edit_participant_box.h
boxes/peers/edit_tag_control.cpp
boxes/peers/edit_tag_control.h
boxes/peers/edit_participants_box.cpp
boxes/peers/edit_participants_box.h
boxes/peers/edit_peer_color_box.cpp
@@ -412,6 +416,8 @@ PRIVATE
boxes/peers/prepare_short_info_box.h
boxes/peers/replace_boost_box.cpp
boxes/peers/replace_boost_box.h
boxes/peers/tag_info_box.cpp
boxes/peers/tag_info_box.h
boxes/peers/verify_peers_box.cpp
boxes/peers/verify_peers_box.h
boxes/about_box.cpp
@@ -1015,6 +1021,8 @@ PRIVATE
history/view/media/history_view_media_spoiler.h
history/view/media/history_view_media_unwrapped.cpp
history/view/media/history_view_media_unwrapped.h
history/view/media/history_view_no_forwards_request.cpp
history/view/media/history_view_no_forwards_request.h
history/view/media/history_view_photo.cpp
history/view/media/history_view_photo.h
history/view/media/history_view_poll.cpp
@@ -1110,6 +1118,8 @@ PRIVATE
history/view/history_view_reaction_preview.h
history/view/history_view_reply.cpp
history/view/history_view_reply.h
history/view/history_view_reply_button.cpp
history/view/history_view_reply_button.h
history/view/history_view_requests_bar.cpp
history/view/history_view_requests_bar.h
history/view/history_view_schedule_box.cpp
@@ -1871,6 +1881,8 @@ PRIVATE
ui/image/image_location_factory.h
ui/text/format_song_document_name.cpp
ui/text/format_song_document_name.h
ui/toast/toast_lottie_icon.cpp
ui/toast/toast_lottie_icon.h
ui/widgets/expandable_peer_list.cpp
ui/widgets/expandable_peer_list.h
ui/widgets/chat_filters_tabs_strip.cpp
@@ -1969,10 +1981,23 @@ PRIVATE
settings.cpp
settings.h
stdafx.h
tray_accounts_menu.h
tray.cpp
tray.h
)
if (APPLE)
nice_target_sources(Telegram ${src_loc}
PRIVATE
tray_accounts_menu.cpp
)
else()
nice_target_sources(Telegram ${src_loc}
PRIVATE
tray_accounts_menu_dummy.cpp
)
endif()
if (NOT build_winstore)
remove_target_sources(Telegram ${src_loc}
platform/win/windows_start_task.cpp
@@ -2396,12 +2421,19 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED)
generate_appstream_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/com.ayugram.desktop.metainfo.xml")
install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon16@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16@2/apps" RENAME "org.telegram.desktop.png")
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "org.telegram.desktop.png")
install(FILES "Resources/art/icon32@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32@2/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon48.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps" RENAME "org.telegram.desktop.png")
install(FILES "Resources/art/icon48@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48@2/apps" RENAME "org.telegram.desktop.png")
install(FILES "Resources/art/icon64.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon64@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64@2/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon128.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps" RENAME "org.telegram.desktop.png")
install(FILES "Resources/art/icon128@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128@2/apps" RENAME "org.telegram.desktop.png")
install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon256@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256@2/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "org.telegram.desktop.png")
install(FILES "Resources/art/icon512@2x.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512@2/apps" RENAME "com.ayugram.desktop.png")
install(FILES "Resources/icons/tray_monochrome.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-symbolic.svg")
install(FILES "Resources/icons/tray_monochrome_attention.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-attention-symbolic.svg")
install(FILES "Resources/icons/tray_monochrome_mute.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/symbolic/apps" RENAME "com.ayugram.desktop-mute-symbolic.svg")
Binary file not shown.
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="192px" height="192px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Other / Large / large_user_tag</title>
<g id="Other-/-Large-/-large_user_tag" stroke="none" fill="none" fill-rule="evenodd">
<path d="M134.779316,99.2776105 L155.861136,126.586165 C159.213567,130.92877 158.410871,137.166828 154.068266,140.519258 L137.349024,153.118955 C133.003707,156.393386 126.832804,155.562756 123.507963,151.255889 L102.341831,123.838121 C101.020315,122.126282 100.709294,119.840656 101.525344,117.837943 L107.904357,102.18287 C108.669944,100.304003 110.404748,98.9967346 112.421855,98.7786913 L129.321766,96.9518613 C131.421678,96.724867 133.488626,97.6057022 134.779316,99.2776105 Z M93.0045522,97.5 C96.0738449,97.5 99.0280025,97.6802939 101.867025,98.0408816 C101.658034,98.4287114 101.468568,98.8297452 101.299796,99.2430624 L94.927069,114.849633 C93.2728501,118.900753 93.7623745,123.498344 96.1864336,127.10949 L96.6969772,127.818112 L110.089,145.166 L55.2019123,145.166667 C50.0608762,145.166667 46.4337638,144.380785 44.3205752,142.809021 L44.1207992,142.654461 C42.0402664,140.979657 41,138.612081 41,135.551733 C41,131.543526 42.2098787,127.327177 44.6296361,122.902685 C47.0493935,118.478193 50.5360562,114.345377 55.0896242,110.504236 C59.6431923,106.663095 65.1152969,103.535437 71.5059383,101.121262 C77.8965797,98.7070875 85.0627843,97.5 93.0045522,97.5 Z M115.981148,108.01419 C113.77151,109.670379 113.331818,112.797477 114.999066,114.998781 C116.666338,117.200067 119.809193,117.641973 122.018852,115.98581 C124.22849,114.329621 124.668182,111.202523 123.000934,109.001219 C121.333662,106.799933 118.190807,106.358027 115.981148,108.01419 Z M93,86.6666667 C97.7764274,86.6666667 102.130651,85.4958452 106.062672,83.1542021 C109.994693,80.8125591 113.133919,77.6591033 115.480352,73.6938349 C117.826784,69.7285665 119,65.2736229 119,60.3290043 C119,55.562273 117.815549,51.2391277 115.446646,47.3595686 C113.077743,43.4800095 109.922365,40.394636 105.980514,38.1034483 C102.038662,35.8122605 97.7118241,34.6666667 93,34.6666667 C88.2881759,34.6666667 83.9615135,35.8242275 80.020013,38.1393492 C76.0785124,40.4544708 72.9231351,43.5619993 70.5538811,47.4619346 C68.184627,51.3618699 67,55.6835597 67,60.427004 C67.0196619,65.3062895 68.2027089,69.7285665 70.5491411,73.6938349 C72.8955734,77.6591033 76.0412953,80.8125591 79.9863069,83.1542021 C83.9313185,85.4958452 88.2692162,86.6666667 93,86.6666667 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>General / menu_download_off</title>
<g id="General-/-menu_download_off" stroke="none" fill="none" fill-rule="nonzero">
<path d="M9.47389883,17.6528876 L12.7944831,20.7769886 C9.92145398,25.1476842 8.25,30.3786476 8.25,36 C8.25,51.3259018 20.6740982,63.75 36,63.75 C42.2291078,63.75 47.9788491,61.6975883 52.6098705,58.2321183 L55.930069,61.3563149 C50.444238,65.674153 43.5229296,68.25 36,68.25 C18.1888168,68.25 3.75,53.8111832 3.75,36 C3.75,29.1836121 5.86472873,22.8611455 9.47389883,17.6528876 Z M10.5466159,7.36583985 L66.5466159,60.3658399 C67.4491376,61.2200122 67.4883325,62.6440941 66.6341601,63.5466159 C65.7799878,64.4491376 64.3559059,64.4883325 63.4533841,63.6341601 L7.45338414,10.6341601 C6.55086241,9.7799878 6.5116675,8.35590587 7.36583985,7.45338414 C8.2200122,6.55086241 9.64409413,6.5116675 10.5466159,7.36583985 Z M23.5706043,35.8888817 L33.2499989,45.324 L33.2499989,40.02 L37.7499989,44.253 L37.7499989,46.299 L38.818,45.258 L42.099,48.344 L37.7102136,52.6229689 C36.7587254,53.5505305 35.2412746,53.5505305 34.2897864,52.6229689 L20.4293957,39.1111183 C19.5395996,38.2436975 19.5214609,36.8191917 20.3888817,35.9293957 C21.2563025,35.0395996 22.6808083,35.0214609 23.5706043,35.8888817 Z M36,3.75 C53.8111832,3.75 68.25,18.1888168 68.25,36 C68.25,42.0362889 66.5916122,47.6852405 63.7056013,52.5160902 L60.3364031,49.3450226 C62.512335,45.3853526 63.75,40.8372124 63.75,36 C63.75,20.6740982 51.3259018,8.25 36,8.25 C30.5567647,8.25 25.4795707,9.81720938 21.1950318,12.5250144 L17.8250746,9.35540043 C23.0002277,5.81848433 29.2585536,3.75 36,3.75 Z M51.6111183,35.9293957 C52.4785391,36.8191917 52.4604004,38.2436975 51.5706043,39.1111183 L50.532,40.123 L47.251,37.036 L48.4293957,35.8888817 C49.3191917,35.0214609 50.7436975,35.0395996 51.6111183,35.9293957 Z M35.5,17.75 C36.7426407,17.75 37.75,18.7573593 37.75,20 L37.75,28.099 L33.25,23.865 L33.25,20 C33.25,18.7573593 34.2573593,17.75 35.5,17.75 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>General / menu_share_off</title>
<g id="General-/-menu_share_off" stroke="none" fill="none" fill-rule="evenodd" stroke-width="1">
<g id="Group-Copy" fill="#FFFFFF">
<path d="M13.5410475,10.3605877 L63.5410475,57.3605877 C64.4464699,58.2116848 64.4905093,59.6356251 63.6394123,60.5410475 C62.7883152,61.4464699 61.3643749,61.4905093 60.4589525,60.6394123 L10.4589525,13.6394123 C9.55353009,12.7883152 9.5094907,11.3643749 10.3605877,10.4589525 C11.2116848,9.55353009 12.6356251,9.5094907 13.5410475,10.3605877 Z M20.8697864,27.9167669 L24.2120013,31.0494419 C22.5720741,32.209287 20.97837,33.5705168 19.4634089,35.1234281 C14.9053119,39.7957062 11.9408626,45.1740812 10.5605883,51.3047505 L11.5051987,50.6015513 C16.0028436,47.2792607 19.5161459,45.0657707 22.0998368,43.9379122 C25.3941073,42.4998643 29.8873875,41.7830384 35.6139217,41.7402273 L39.7497201,45.6169821 L39.75,52.8610336 L43.6427201,49.2669821 L46.9337201,52.3519821 L41.5435504,57.3295353 C40.8508419,57.9689585 39.9427126,58.3240224 39,58.3240224 C36.9289322,58.3240224 35.25,56.6450902 35.25,54.5740224 L35.25,46.2445368 C30.2823664,46.3118224 26.4887541,46.9320902 23.9001632,48.0620878 C21.1038403,49.2827666 16.6790868,52.217586 10.7334856,56.8296594 C10.0454974,57.3626476 9.17248701,57.5974385 8.30996087,57.4814518 C6.53104748,57.2422354 5.28287643,55.606218 5.5220928,53.8273046 L5.82195865,51.5973762 L5.91112003,51.609366 C7.32643988,44.1287015 10.7867131,37.5733134 16.2423132,31.9810496 C17.7180145,30.4683817 19.2679346,29.1105456 20.8697864,27.9167669 Z M41.574499,10.6994311 L63.4340398,30.8770703 C63.5173005,30.9539264 63.5974393,31.0340651 63.6742803,31.1173096 C65.2663463,32.8420477 65.1587942,35.5308487 63.4340561,37.1229146 L55.0377201,44.8719821 L51.7467201,41.7869821 L60.1827844,34.0000018 L39.7914686,15.1776163 L39.8121759,19.4395349 L39.81008,21.9362814 C39.8076961,22.3780357 39.8040398,22.7522447 39.7990732,23.0599114 L39.7825303,23.7289937 C39.7787867,23.8265061 39.7745714,23.9114264 39.7697493,23.9851995 C39.7624707,24.096554 39.7535986,24.1918121 39.7385791,24.2928873 L39.661515,24.796446 C39.4402142,26.1576904 39.2505293,26.2209711 37.5424339,26.2531907 C36.8212,26.2667952 36.0924969,26.3233582 35.3590026,26.4220805 L31.3609553,22.6738894 C32.6774415,22.3146094 33.997132,22.0625163 35.3100594,21.9217361 L35.3094753,18.5338121 C35.304347,17.1233929 35.2942592,15.4381587 35.2792041,13.4782339 C35.2728525,12.5247482 35.6299537,11.6046499 36.2778705,10.9050936 C37.6851807,9.38561997 40.0578077,9.29469349 41.574499,10.6994311 Z" id="Shape" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>General / menu_share_on</title>
<g id="General-/-menu_share_on" stroke="none" fill="none" fill-rule="nonzero">
<path d="M70.5418968,45.3555412 C71.4743766,46.1769042 71.5644546,47.5986747 70.7430916,48.5311546 L57.3925886,63.6877603 C57.3238151,63.7641971 57.3238151,63.7641971 57.2518797,63.8376661 C55.9826757,65.1068701 53.9248896,65.1068701 52.6556856,63.8376661 L46.4090097,57.5909903 C45.5303301,56.7123106 45.5303301,55.2876894 46.4090097,54.4090097 C47.2876894,53.5303301 48.7123106,53.5303301 49.5909903,54.4090097 L54.896011,59.7140304 L67.3662834,45.556736 C68.1876464,44.6242562 69.6094169,44.5341782 70.5418968,45.3555412 Z M40.574499,6.69943107 L62.4340398,26.8770703 C62.5173005,26.9539264 62.5974393,27.0340651 62.6742803,27.1173096 C64.2663463,28.8420477 64.1587942,31.5308487 62.4340561,33.1229146 L40.5435504,53.3295353 C39.8508419,53.9689585 38.9427126,54.3240224 38,54.3240224 C35.9289322,54.3240224 34.25,52.6450902 34.25,50.5740224 L34.25,42.2445368 C29.2823664,42.3118224 25.4887541,42.9320902 22.9001632,44.0620878 C20.1038403,45.2827666 15.6790868,48.217586 9.73348561,52.8296594 C9.04549739,53.3626476 8.17248701,53.5974385 7.30996087,53.4814518 C5.53104748,53.2422354 4.28287643,51.606218 4.5220928,49.8273046 L4.82195865,47.5973762 L4.91112003,47.609366 C6.32643988,40.1287015 9.78671313,33.5733134 15.2423132,27.9810496 C20.9223073,22.1587705 27.7018464,18.6303097 34.3100594,17.9217361 C34.311043,17.7474678 34.3117992,17.5601548 34.3123494,17.3612422 L34.312932,17.1056401 L34.3131028,15.9634276 C34.3110268,14.3126951 34.2997338,12.1508586 34.2792041,9.47823391 C34.2728525,8.52474824 34.6299537,7.60464986 35.2778705,6.90509359 C36.6851807,5.38561997 39.0578077,5.29469349 40.574499,6.69943107 Z M38.7914686,11.1776163 C38.8038479,13.0150348 38.8109783,14.5636507 38.8129119,15.8271389 L38.8129621,17.0925577 C38.8127949,17.188613 38.812585,17.2823229 38.8123322,17.3736902 C38.8098628,18.2664023 38.8033194,18.9389672 38.7924819,19.3972354 C38.7866103,19.6455183 38.7793935,19.8376533 38.7697493,19.9851995 C38.7624707,20.096554 38.7535986,20.1918121 38.7385791,20.2928873 C38.4636206,22.1432446 38.4403177,22.2173911 36.5424339,22.2531907 C30.5168733,22.3668501 23.9699768,25.4789196 18.4634089,31.1234281 C13.9053119,35.7957062 10.9408626,41.1740812 9.56058826,47.3047505 C14.5138287,43.5897332 18.3379603,41.1435541 21.0998368,39.9379122 C24.7547558,38.3424304 29.8855842,37.6347299 36.5390895,37.7503396 L38.75,37.7887558 L38.75,48.8610336 L59.1827844,30.0000018 L38.7914686,11.1776163 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Menu / menu_tag_add</title>
<g id="Icon-/-Menu-/-menu_tag_add" stroke="none" fill="none" fill-rule="evenodd">
<path d="M16.599856,4.56363636 C17.535585,4.56363636 18.4029715,5.05848397 18.895196,5.86914299 L22.2846438,11.4513241 C22.7051187,12.1438167 22.7051187,13.0198196 22.2846438,13.7123122 L18.895196,19.2944934 C18.4029715,20.1051524 17.535585,20.6 16.599856,20.6 L7.25495844,20.6 C5.42706662,20.6 3.95240838,19.0820888 3.95240838,17.2181818 L3.95240838,11.5364421 C3.95240838,11.2050712 4.22103753,10.9364421 4.55240838,10.9364421 C4.88377923,10.9364421 5.15240838,11.2050712 5.15240838,11.5364421 L5.15240838,17.2181818 C5.15240838,18.4269864 6.09769589,19.4 7.25495844,19.4 L16.599856,19.4 C17.1136128,19.4 17.5934712,19.1262387 17.8694727,18.6716839 L21.2589205,13.0895027 C21.4470265,12.7797053 21.4470265,12.3839311 21.2589205,12.0741337 L17.8694727,6.49195251 C17.5934712,6.03739762 17.1136128,5.76363636 16.599856,5.76363636 L10.24363,5.76363636 C9.91225919,5.76363636 9.64363004,5.49500721 9.64363004,5.16363636 C9.64363004,4.83226551 9.91225919,4.56363636 10.24363,4.56363636 L16.599856,4.56363636 Z M16.2634586,11.1909091 C17.0097472,11.1909091 17.6147336,11.8136403 17.6147336,12.5818182 C17.6147336,13.3499961 17.0097472,13.9727273 16.2634586,13.9727273 C15.51717,13.9727273 14.9121836,13.3499961 14.9121836,12.5818182 C14.9121836,11.8136403 15.51717,11.1909091 16.2634586,11.1909091 Z M4.43002129,2.4 C4.76139214,2.4 5.03002129,2.66862915 5.03002129,3 L5.02908502,5.233 L7.26004258,5.23333333 C7.59141343,5.23333333 7.86004258,5.50196248 7.86004258,5.83333333 C7.86004258,6.16470418 7.59141343,6.43333333 7.26004258,6.43333333 L5.02908502,6.433 L5.03002129,8.66666667 C5.03002129,8.99803752 4.76139214,9.26666667 4.43002129,9.26666667 C4.09865044,9.26666667 3.83002129,8.99803752 3.83002129,8.66666667 L3.82908502,6.433 L1.6,6.43333333 C1.26862915,6.43333333 1,6.16470418 1,5.83333333 C1,5.50196248 1.26862915,5.23333333 1.6,5.23333333 L3.82908502,5.233 L3.83002129,3 C3.83002129,2.66862915 4.09865044,2.4 4.43002129,2.4 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Menu / menu_tag_edit</title>
<g id="Icon-/-Menu-/-menu_tag_edit" stroke="none" fill="none" fill-rule="evenodd">
<path d="M16.599856,4.56363636 C17.535585,4.56363636 18.4029715,5.05848397 18.895196,5.86914299 L22.2846438,11.4513241 C22.7051187,12.1438167 22.7051187,13.0198196 22.2846438,13.7123122 L18.895196,19.2944934 C18.4029715,20.1051524 17.535585,20.6 16.599856,20.6 L7.25495844,20.6 C5.42706662,20.6 3.95240838,19.0820888 3.95240838,17.2181818 L3.95240838,14.6702947 C3.95240838,14.3389239 4.22103753,14.0702947 4.55240838,14.0702947 C4.88377923,14.0702947 5.15240838,14.3389239 5.15240838,14.6702947 L5.15240838,17.2181818 C5.15240838,18.4269864 6.09769589,19.4 7.25495844,19.4 L16.599856,19.4 C17.1136128,19.4 17.5934712,19.1262387 17.8694727,18.6716839 L21.2589205,13.0895027 C21.4470265,12.7797053 21.4470265,12.3839311 21.2589205,12.0741337 L17.8694727,6.49195251 C17.5934712,6.03739762 17.1136128,5.76363636 16.599856,5.76363636 L14.031014,5.76363636 C13.6996432,5.76363636 13.431014,5.49500721 13.431014,5.16363636 C13.431014,4.83226551 13.6996432,4.56363636 14.031014,4.56363636 L16.599856,4.56363636 Z M16.2634586,11.1909091 C17.0097472,11.1909091 17.6147336,11.8136403 17.6147336,12.5818182 C17.6147336,13.3499961 17.0097472,13.9727273 16.2634586,13.9727273 C15.51717,13.9727273 14.9121836,13.3499961 14.9121836,12.5818182 C14.9121836,11.8136403 15.51717,11.1909091 16.2634586,11.1909091 Z M9.83896948,2.96952606 L11.6623203,4.84566378 C12.0985241,5.29466176 12.0985241,6.00915906 11.6623203,6.45815704 L6.52054212,11.7507481 C6.31002174,11.9674432 6.02309451,12.0932021 5.72108049,12.1011485 L3.59699439,12.157036 C3.11196702,12.1697977 2.70842924,11.7869508 2.69566752,11.3019234 L2.75052739,9.04653645 C2.75775533,8.75547719 2.87444695,8.47784136 3.0773236,8.26901415 L8.2028283,2.99317397 C8.64810602,2.53483587 9.38063138,2.52424834 9.83896948,2.96952606 Z M7.00385646,5.94974937 L3.94974021,9.093133 L3.90366423,10.9485518 L5.67202981,10.9020238 L8.76590842,7.71849906 C8.69791967,7.68828862 8.63444899,7.64484068 8.57943411,7.58821217 L7.07780938,6.04254349 C7.04964358,6.01355156 7.02499515,5.98244405 7.00385646,5.94974937 Z M9.03272284,3.86106211 L7.81747041,5.11015524 C7.8608342,5.13627396 7.90157846,5.16835024 7.93851179,5.20636686 L9.44013652,6.75203554 C9.48642883,6.79968564 9.52321964,6.85305056 9.55054286,6.90978429 L10.7725408,5.65191041 L9.03272284,3.86106211 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Mini / mini_replace2</title>
<g id="Mini-/-mini_replace2" stroke="none" fill="none" fill-rule="nonzero">
<path d="M8.27981904,18.1901792 C8.35180301,18.2477663 8.41718661,18.3131499 8.47477379,18.3851339 L14.6887446,26.1525974 C14.9040856,26.4217737 14.8604435,26.8145527 14.5912672,27.0298938 C14.4805955,27.1184311 14.3430866,27.1666667 14.2013577,27.1666667 L9.42766478,27.1670537 C10.878665,33.9028527 16.8626042,38.95 24.0232121,38.95 C27.9154401,38.95 31.5678594,37.4552395 34.3277951,34.8190502 C35.1465138,34.0370401 36.4441608,34.0667983 37.2261709,34.885517 C38.0081809,35.7042357 37.9784227,37.0018828 37.159704,37.7838928 C33.6445004,41.1414863 28.981083,43.05 24.0232121,43.05 C14.5899526,43.05 6.76036297,36.1801958 5.25373571,27.166733 L0.798642328,27.1666667 C0.453928405,27.1666667 0.174482557,26.8872208 0.174482557,26.5425069 C0.174482557,26.400778 0.222718094,26.263269 0.311255431,26.1525974 L6.52522621,18.3851339 C6.95590837,17.8467812 7.74146633,17.759497 8.27981904,18.1901792 Z M24.0232121,4.95 C33.5722941,4.95 41.4780844,11.9895362 42.8453157,21.1662324 L47.2013577,21.1666667 C47.3430866,21.1666667 47.4805955,21.2149022 47.5912672,21.3034395 C47.8604435,21.5187806 47.9040856,21.9115596 47.6887446,22.180736 L41.4747738,29.9481994 C41.4171866,30.0201834 41.351803,30.085567 41.279819,30.1431542 C40.7414663,30.5738363 39.9559084,30.4865521 39.5252262,29.9481994 L33.3112554,22.180736 C33.2227181,22.0700643 33.1744826,21.9325553 33.1744826,21.7908264 C33.1744826,21.4461125 33.4539284,21.1666667 33.7986423,21.1666667 L38.6865289,21.1659705 C37.363877,14.2641899 31.3013507,9.05 24.0232121,9.05 C19.9913296,9.05 16.2185331,10.6547806 13.4323785,13.4617549 C12.6347893,14.2653042 11.3368101,14.2701357 10.5332608,13.4725465 C9.72971154,12.6749573 9.72487998,11.3769781 10.5224692,10.5734288 C14.070616,6.99876826 18.8872155,4.95 24.0232121,4.95 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+2 -2
View File
@@ -159,13 +159,13 @@ var IV = {
return false;
},
initPreBlocks: function() {
if (!hljs) {
if (!window.hljs) {
return;
}
var pres = document.getElementsByTagName('pre');
for (var i = 0; i < pres.length; i++) {
if (pres[i].hasAttribute('data-language')) {
hljs.highlightBlock(pres[i]);
window.hljs.highlightBlock(pres[i]);
}
}
},
+142 -2
View File
@@ -313,6 +313,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_error_noforwards_channel" = "Sorry, forwarding from this channel is disabled by admins.";
"lng_error_nocopy_group" = "Sorry, copying from this group is disabled by admins.";
"lng_error_nocopy_channel" = "Sorry, copying from this channel is disabled by admins.";
"lng_error_noforwards_user" = "Sorry, forwarding from this chat is restricted.";
"lng_error_nocopy_user" = "Sorry, copying from this chat is restricted.";
"lng_error_nocopy_story" = "Sorry, the creator of this story disabled copying.";
"lng_error_schedule_limit" = "Sorry, you can't schedule more than 100 messages.";
"lng_sure_add_admin_invite" = "This user is not a member of this group. Add them to the group and promote them to admin?";
@@ -456,6 +458,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_contacts_loading" = "Loading...";
"lng_contacts_not_found" = "No contacts found";
"lng_topics_not_found" = "No topics found.";
"lng_forum_all_messages" = "All Messages";
"lng_forum_create_new_topic" = "Create New Thread";
"lng_dlg_search_for_messages" = "Search for messages";
"lng_update_telegram" = "Update Telegram";
"lng_dlg_search_in" = "Search messages in";
@@ -666,6 +670,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_send_cmdenter" = "Send with Cmd+Enter";
"lng_settings_chat_quick_action_reply" = "Reply with double click";
"lng_settings_chat_quick_action_react" = "Send reaction with double click";
"lng_settings_chat_corner_reply" = "Reply button on messages";
"lng_settings_chat_corner_reaction" = "Reaction button on messages";
"lng_settings_shortcuts" = "Keyboard shortcuts";
@@ -1655,6 +1660,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_leave_next_owner_box_title_group" = "Leave Group?";
"lng_leave_next_owner_box_about" = "If you leave, **{user}** will become the new owner of **{chat}** in **1 week**.";
"lng_leave_next_owner_box_about_admin" = "If you leave, **{user}** will become the new admin of **{chat}** in **1 week**.";
"lng_leave_next_owner_box_about_legacy" = "If you leave, **{user}** will immediately become the new owner of **{chat}**.";
"lng_leave_next_owner_box_about_admin_legacy" = "If you leave, **{user}** will immediately become the new admin of **{chat}**.";
"lng_select_next_owner_box" = "Appoint Another Owner";
"lng_select_next_owner_box_admin" = "Appoint Another Admin";
"lng_select_next_owner_box_title" = "Appoint New Owner";
@@ -1892,6 +1899,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_no_forwards_about" = "Members won't be able to copy, save or forward content from this group.";
"lng_manage_peer_no_forwards_about_channel" = "Subscribers won't be able to copy, save or forward content from this channel.";
"lng_disable_sharing" = "Disable Sharing";
"lng_enable_sharing" = "Enable Sharing";
"lng_disable_sharing_title" = "Disable Sharing";
"lng_disable_sharing_no_forwarding" = "No Forwarding";
"lng_disable_sharing_no_forwarding_about" = "Disable message forwarding to other chats.";
"lng_disable_sharing_no_saving" = "No Saving";
"lng_disable_sharing_no_saving_about" = "Disable copying texts and saving photos and videos to gallery.";
"lng_disable_sharing_unlock" = "Unlock with Telegram Premium";
"lng_disable_sharing_button" = "Disable Sharing";
"lng_disable_sharing_toast" = "Sharing disabled for this chat.";
"lng_enable_sharing_toast" = "Sharing enabled for this chat.";
"lng_enable_sharing_request_title" = "Enable Sharing";
"lng_enable_sharing_request_text" = "You need **{name}'s** approval to enable sharing. Send a request?";
"lng_enable_sharing_request_button" = "Send Request";
"lng_action_no_forwards_you_disabled" = "You disabled sharing in this chat";
"lng_action_no_forwards_you_enabled" = "You enabled sharing in this chat";
"lng_action_no_forwards_disabled" = "{from} disabled sharing in this chat";
"lng_action_no_forwards_enabled" = "{from} enabled sharing in this chat";
"lng_action_no_forwards_still_disabled" = "Sharing in this chat is still disabled";
"lng_action_no_forwards_request" = "{from} would like to enable sharing in this chat, which includes:";
"lng_action_no_forwards_request_you" = "You requested to enable sharing in this chat, which includes:";
"lng_action_no_forwards_feature_forwarding" = "Forwarding messages";
"lng_action_no_forwards_feature_saving" = "Saving photos and videos";
"lng_action_no_forwards_feature_copying" = "Copying messages";
"lng_action_no_forwards_accept" = "Accept";
"lng_action_no_forwards_reject" = "Reject";
"lng_action_no_forwards_request_expired" = "Sharing enable request has expired";
"lng_manage_peer_bot_public_link" = "Public Link";
"lng_manage_peer_bot_public_links" = "Public Links";
"lng_manage_peer_bot_balance" = "Balance";
@@ -2386,7 +2422,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_gift_premium_about" = "Subscription for exclusive Telegram features.";
"lng_action_gift_refunded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned.";
"lng_action_gift_got_ton" = "Use TON to suggest posts to channels.";
"lng_action_gift_offer" = "{user} offered you {cost} for {name}.";
"lng_action_gift_offer_incoming" = "An offer to buy this gift for {amount}.";
"lng_action_gift_offer_you" = "You offered {cost} for {name}.";
"lng_action_gift_offer_state_expires" = "This offer expires in {time}.";
"lng_action_gift_offer_time_large" = "{hours} h";
@@ -2907,6 +2943,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_about_peer_colors" = "Choose a color and logo for your profile and replies to your messages.";
"lng_premium_summary_subtitle_gifts" = "Telegram Gifts";
"lng_premium_summary_about_gifts" = "Gifts are collectible items you can trade or showcase on your profile.";
"lng_premium_summary_subtitle_no_forwards" = "Disable Sharing";
"lng_premium_summary_about_no_forwards" = "Restrict forwarding, copying, and saving content from your private chats.";
"lng_premium_summary_bottom_subtitle" = "About Telegram Premium";
"lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
"lng_premium_summary_button" = "Subscribe for {cost} per month";
@@ -4170,6 +4208,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_craft_another_button" = "Craft Another Gift";
"lng_gift_craft_unavailable" = "Crafting unavailable";
"lng_gift_craft_when" = "This gift will become available for crafting on {date} at {time}.";
"lng_gift_craft_address_error" = "This gift has been exported to the blockchain and can't be used as the primary gift for crafting.";
"lng_auction_about_title" = "Auction";
"lng_auction_about_subtitle" = "Join the battle for exclusive gifts.";
@@ -4699,6 +4738,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_url_auth_phone_toast_bad_title" = "Login failed";
"lng_url_auth_phone_toast_bad" = "Please try logging in to {domain} again.";
"lng_url_auth_phone_toast_bad_expired" = "This link is expired.";
"lng_url_auth_match_code_title" = "Tap the emoji shown on your other device";
"lng_url_auth_match_code_info" = "Login request from {domain}";
"lng_bot_start" = "Start";
"lng_bot_choose_group" = "Select a Group";
@@ -4864,6 +4905,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_edit_permissions" = "Edit admin rights";
"lng_context_restrict_user" = "Restrict user";
"lng_context_ban_user" = "Ban";
"lng_context_delete_and_ban" = "Delete and ban";
"lng_context_remove_from_group" = "Remove from group";
"lng_context_add_to_group" = "Add to group";
@@ -4942,6 +4984,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_seen_watched_none" = "Nobody Listened";
"lng_context_seen_reacted#one" = "{count} Reacted";
"lng_context_seen_reacted#other" = "{count} Reacted";
"lng_context_seen_reactions_count#one" = "{count} Reaction";
"lng_context_seen_reactions_count#other" = "{count} Reactions";
"lng_context_seen_reacted_none" = "Nobody Reacted";
"lng_context_seen_reacted_all" = "Show All Reactions";
"lng_context_sent_by" = "Sent by {user}";
@@ -4989,6 +5033,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**.";
"lng_context_animated_emoji_many#other" = "This message contains emoji from **{count} packs**.";
"lng_context_animated_reaction" = "This reaction is from the **{name}** pack.";
"lng_context_animated_emoji_preview" = "This emoji is from the **{name}** pack.";
"lng_context_animated_tag" = "This tag is from **{name} pack**.";
"lng_context_animated_reactions" = "Reactions contain emoji from **{name} pack**.";
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
@@ -4997,6 +5042,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
"lng_context_noforwards_info_bot" = "Copying and forwarding is not allowed from this bot.";
"lng_context_noforwards_info_his" = "{user} disabled copying and forwarding in this chat.";
"lng_context_noforwards_info_mine" = "You disabled copying and forwarding in this chat.";
"lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove Spoiler";
@@ -5487,9 +5534,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_menu_formatting_blockquote" = "Quote";
"lng_menu_formatting_monospace" = "Monospace";
"lng_menu_formatting_spoiler" = "Spoiler";
"lng_menu_formatting_date" = "Date";
"lng_menu_formatting_link_create" = "Create link";
"lng_menu_formatting_link_edit" = "Edit link";
"lng_menu_formatting_clear" = "Clear formatting";
"lng_formatting_date_title" = "Choose date and time";
"lng_formatting_link_create_title" = "Create link";
"lng_formatting_link_edit_title" = "Edit link";
"lng_formatting_link_text" = "Text";
@@ -5501,6 +5550,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_text_copied" = "Text copied to clipboard.";
"lng_code_copied" = "Code copied to clipboard.";
"lng_date_copied" = "Date copied to clipboard.";
"lng_date_relative_now" = "now";
"lng_date_relative_seconds_ago#one" = "{count} second ago";
"lng_date_relative_seconds_ago#other" = "{count} seconds ago";
"lng_date_relative_minutes_ago#one" = "{count} minute ago";
"lng_date_relative_minutes_ago#other" = "{count} minutes ago";
"lng_date_relative_hours_ago#one" = "{count} hour ago";
"lng_date_relative_hours_ago#other" = "{count} hours ago";
"lng_date_relative_days_ago#one" = "{count} day ago";
"lng_date_relative_days_ago#other" = "{count} days ago";
"lng_date_relative_months_ago#one" = "{count} month ago";
"lng_date_relative_months_ago#other" = "{count} months ago";
"lng_date_relative_years_ago#one" = "{count} year ago";
"lng_date_relative_years_ago#other" = "{count} years ago";
"lng_date_relative_in_seconds#one" = "in {count} second";
"lng_date_relative_in_seconds#other" = "in {count} seconds";
"lng_date_relative_in_minutes#one" = "in {count} minute";
"lng_date_relative_in_minutes#other" = "in {count} minutes";
"lng_date_relative_in_hours#one" = "in {count} hour";
"lng_date_relative_in_hours#other" = "in {count} hours";
"lng_date_relative_in_days#one" = "in {count} day";
"lng_date_relative_in_days#other" = "in {count} days";
"lng_date_relative_in_months#one" = "in {count} month";
"lng_date_relative_in_months#other" = "in {count} months";
"lng_date_relative_in_years#one" = "in {count} year";
"lng_date_relative_in_years#other" = "in {count} years";
"lng_context_copy_date" = "Copy Date";
"lng_context_add_to_calendar" = "Add to Calendar";
"lng_context_set_reminder" = "Set a Reminder";
"lng_reminder_scheduled_in" = "Reminder scheduled in {link}.";
"lng_spellchecker_submenu" = "Spelling";
"lng_spellchecker_add" = "Add to Dictionary";
@@ -5959,7 +6042,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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}";
"lng_player_message_yesterday" = "yesterday at {time}";
"lng_player_message_date" = "{date} at {time}";
@@ -5975,6 +6057,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_edit_admin_header" = "What can this admin do?";
"lng_rights_edit_admin_rank_name" = "Custom title";
"lng_rights_edit_admin_rank_about" = "A title that members will see instead of '{title}'.";
"lng_context_add_my_tag" = "Add tag";
"lng_context_edit_my_tag" = "Edit tag";
"lng_context_add_member_tag" = "Add member tag";
"lng_context_edit_member_tag" = "Edit member tag";
"lng_rights_edit_tag_title" = "Edit tag";
"lng_rights_tag_about" = "Add short tag next to {name}'s name.";
"lng_rights_tag_about_self" = "Share your role, title, or how you're known in this group. Your tag is visible to all members.";
"lng_tag_info_title_user" = "Member Tags";
"lng_tag_info_title_admin" = "Admin Tags";
"lng_tag_info_title_owner" = "Owner Tags";
"lng_tag_info_text_user" = "This grey tag {emoji} is {author}'s member tag in {group}.";
"lng_tag_info_text_admin" = "This green tag {emoji} is {author}'s admin tag in {group}.";
"lng_tag_info_text_owner" = "This purple tag {emoji} is {author}'s owner tag in {group}.";
"lng_tag_info_preview_member" = "Member Tag";
"lng_tag_info_preview_admin" = "Admin Tag";
"lng_tag_info_preview_owner" = "Owner Tag";
"lng_tag_info_add_my_tag" = "Add My Tag";
"lng_tag_info_edit_my_tag" = "Edit My Tag";
"lng_tag_info_admins_only" = "Only admins can change tags in this group.";
"lng_rights_promote_member" = "Promote to Admin";
"lng_rights_remove_member" = "Remove from Group";
"lng_rights_dismiss_admin" = "Dismiss Admin";
"lng_rights_about_add_admins_yes" = "This admin will be able to add new admins with equal or fewer rights.";
"lng_rights_about_add_admins_no" = "This admin will not be able to add new admins.";
"lng_rights_about_by" = "This admin promoted by {user} on {date}.";
@@ -6067,9 +6173,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_group_pin" = "Pin messages";
"lng_rights_group_topics" = "Manage topics";
"lng_rights_group_add_topics" = "Create topics";
"lng_rights_group_edit_rank" = "Edit own tags";
"lng_rights_group_edit_rank_single" = "Edit own tag";
"lng_rights_group_manage_calls" = "Manage video chats";
"lng_rights_group_delete" = "Delete messages";
"lng_rights_group_anonymous" = "Remain anonymous";
"lng_rights_group_manage_ranks" = "Edit member tags";
"lng_rights_add_admins" = "Add new admins";
"lng_rights_chat_send_text" = "Send messages";
"lng_rights_chat_send_media" = "Send media";
@@ -6202,6 +6311,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_filter_subscribers_removed" = "Subscribers leaving";
"lng_admin_log_filter_topics" = "Topics";
"lng_admin_log_filter_sub_extend" = "Subscription Renewals";
"lng_admin_log_filter_edit_rank" = "Tag Changes";
"lng_admin_log_filter_all_admins" = "All users and admins";
"lng_admin_log_filter_actions_admins_subtitle" = "Filter actions by admins";
"lng_admin_log_filter_actions_admins_section" = "Show Actions by All Admins";
@@ -6380,14 +6490,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_admin_invite_users" = "Add users";
"lng_admin_log_admin_invite_link" = "Invite users via link";
"lng_admin_log_admin_pin_messages" = "Pin messages";
"lng_admin_log_banned_edit_rank" = "Edit own tags";
"lng_admin_log_banned_edit_rank_single" = "Edit own tag";
"lng_admin_log_admin_manage_topics" = "Manage topics";
"lng_admin_log_admin_create_topics" = "Create topics";
"lng_admin_log_admin_manage_calls" = "Manage video chats";
"lng_admin_log_admin_manage_calls_channel" = "Manage live streams";
"lng_admin_log_admin_manage_direct" = "Manage direct messages";
"lng_admin_log_admin_manage_ranks" = "Edit member tags";
"lng_admin_log_admin_add_admins" = "Add new admins";
"lng_admin_log_subscription_extend" = "{name} renewed subscription until {date}";
"lng_admin_log_changed_rank_from" = "{from} changed tag for {user} from \"{previous}\" to \"{tag}\"";
"lng_admin_log_set_rank" = "{from} set tag for {user} to \"{tag}\"";
"lng_admin_log_removed_rank" = "{from} removed tag for {user} (was \"{previous}\")";
"lng_admin_log_changed_own_rank_from" = "{from} changed own tag from \"{previous}\" to \"{tag}\"";
"lng_admin_log_set_own_rank" = "{from} set own tag to \"{tag}\"";
"lng_admin_log_removed_own_rank" = "{from} removed own tag (was \"{previous}\")";
"lng_admin_log_antispam_menu_report" = "Report False Positive";
"lng_admin_log_antispam_menu_report_toast" = "You can manage anti-spam settings in {link}.";
"lng_admin_log_antispam_menu_report_toast_link" = "Group Info > Administrators";
@@ -6715,6 +6835,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_show_more#one" = "Show more ({count})";
"lng_polls_show_more#other" = "Show more ({count})";
"lng_polls_votes_collapse" = "Collapse";
"lng_polls_vote_yesterday" = "yesterday";
"lng_todo_title" = "Checklist";
"lng_todo_title_group" = "Group Checklist";
@@ -7528,6 +7649,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_summarize_header_title" = "AI summary";
"lng_summarize_header_about" = "Click to see original text";
"lng_calendar" = "Calendar";
"lng_new_window_tooltip_ctrl" = "Use Ctrl+Click to open in New Window.";
"lng_new_window_tooltip_cmd" = "Use Cmd+Click to open in New Window.";
// Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program...";
@@ -7594,6 +7720,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mac_hold_to_quit" = "Hold {text} to Quit";
"lng_rename_file" = "Rename file";
"lng_sr_playback_order" = "Playback order";
"lng_sr_player_close" = "Close media player";
"lng_sr_group_call_menu" = "Video chat menu";
"lng_sr_search_date" = "Search by date";
"lng_sr_cancel_search" = "Cancel search";
"lng_sr_clear_search" = "Clear search";
"lng_sr_scroll_to_top" = "Scroll to top";
"lng_sr_verified_badge" = "Verified";
"lng_sr_bot_verified_badge" = "Verified Bot";
"lng_sr_profile_menu" = "Profile menu";
"lng_sr_close_panel" = "Close panel";
// Keys finished
@@ -56,6 +56,7 @@
<file alias="ban.tgs">../../animations/ban.tgs</file>
<file alias="cocoon.tgs">../../animations/cocoon.tgs</file>
<file alias="craft_failed.tgs">../../animations/craft_failed.tgs</file>
<file alias="stop.tgs">../../animations/stop.tgs</file>
<file alias="profile_muting.tgs">../../animations/profile/profile_muting.tgs</file>
<file alias="profile_unmuting.tgs">../../animations/profile/profile_unmuting.tgs</file>
+1 -1
View File
@@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="6.5.1.0" />
Version="6.6.2.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
+4 -4
View File
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,5,1,0
PRODUCTVERSION 6,5,1,0
FILEVERSION 6,6,2,0
PRODUCTVERSION 6,6,2,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "6.5.1.0"
VALUE "FileVersion", "6.6.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "6.5.1.0"
VALUE "ProductVersion", "6.6.2.0"
END
END
BLOCK "VarFileInfo"
+4 -4
View File
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,5,1,0
PRODUCTVERSION 6,5,1,0
FILEVERSION 6,6,2,0
PRODUCTVERSION 6,6,2,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "6.5.1.0"
VALUE "FileVersion", "6.6.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "6.5.1.0"
VALUE "ProductVersion", "6.6.2.0"
END
END
BLOCK "VarFileInfo"
@@ -51,40 +51,47 @@ std::vector<ChatParticipant> ParseList(
void ApplyMegagroupAdmins(not_null<ChannelData*> channel, Members list) {
Expects(channel->isMegagroup());
const auto i = ranges::find_if(list, &Api::ChatParticipant::isCreator);
if (i != list.end()) {
i->tryApplyCreatorTo(channel);
} else {
channel->mgInfo->creator = nullptr;
channel->mgInfo->creatorRank = QString();
}
const auto creatorIt = ranges::find_if(
list,
&Api::ChatParticipant::isCreator);
auto adding = base::flat_map<UserId, QString>();
auto adding = base::flat_set<UserId>();
auto addingRanks = base::flat_map<UserId, QString>();
for (const auto &p : list) {
if (p.isUser()) {
adding.emplace(p.userId(), p.rank());
adding.emplace(p.userId());
if (!p.rank().isEmpty()) {
addingRanks.emplace(p.userId(), p.rank());
}
}
}
if (channel->mgInfo->creator) {
adding.emplace(
peerToUser(channel->mgInfo->creator->id),
channel->mgInfo->creatorRank);
if (creatorIt != list.end() && creatorIt->isUser()) {
adding.emplace(creatorIt->userId());
if (!creatorIt->rank().isEmpty()) {
addingRanks.emplace(creatorIt->userId(), creatorIt->rank());
}
}
auto removing = channel->mgInfo->admins;
if (removing.empty() && adding.empty()) {
// Add some admin-placeholder so we don't DDOS
// server with admins list requests.
LOG(("API Error: Got empty admins list from server."));
adding.emplace(0, QString());
adding.emplace(UserId(0));
}
Data::ChannelAdminChanges changes(channel);
for (const auto &[addingId, rank] : adding) {
if (!removing.remove(addingId)) {
changes.add(addingId, rank);
}
if (creatorIt != list.end()) {
creatorIt->tryApplyCreatorTo(channel);
} else {
channel->mgInfo->creator = nullptr;
}
for (const auto &[removingId, rank] : removing) {
for (const auto &addingId : adding) {
const auto r = addingRanks.find(addingId);
const auto rank = (r != end(addingRanks))
? r->second
: QString();
removing.remove(addingId);
changes.add(addingId, rank);
}
for (const auto &removingId : removing) {
changes.remove(removingId);
}
}
@@ -112,6 +119,7 @@ void ApplyLastList(
channel->mgInfo->lastAdmins.clear();
channel->mgInfo->lastRestricted.clear();
channel->mgInfo->lastParticipants.clear();
channel->mgInfo->memberRanks.clear();
channel->mgInfo->lastParticipantsStatus
= MegagroupInfo::LastParticipantsUpToDate
| MegagroupInfo::LastParticipantsOnceReceived;
@@ -148,6 +156,9 @@ void ApplyLastList(
channel->mgInfo->botStatus = 2;
}
}
if (!p.rank().isEmpty()) {
channel->mgInfo->memberRanks[p.userId()] = p.rank();
}
}
}
//
@@ -287,12 +298,14 @@ ChatParticipant::ChatParticipant(
_type = Type::Member;
_date = data.vdate().v;
_by = peerToUser(peerFromUser(data.vinviter_id()));
_rank = qs(data.vrank().value_or_empty());
if (data.vsubscription_until_date()) {
_subscriptionDate = data.vsubscription_until_date()->v;
}
}, [&](const MTPDchannelParticipant &data) {
_type = Type::Member;
_date = data.vdate().v;
_rank = qs(data.vrank().value_or_empty());
if (data.vsubscription_until_date()) {
_subscriptionDate = data.vsubscription_until_date()->v;
}
@@ -300,6 +313,7 @@ ChatParticipant::ChatParticipant(
_restrictions = ChatRestrictionsInfo(data.vbanned_rights());
_by = peerToUser(peerFromUser(data.vkicked_by()));
_date = data.vdate().v;
_rank = qs(data.vrank().value_or_empty());
_type = (_restrictions.flags & ChatRestriction::ViewMessages)
? Type::Banned
@@ -331,7 +345,11 @@ void ChatParticipant::tryApplyCreatorTo(
if (isCreator() && isUser()) {
if (const auto info = channel->mgInfo.get()) {
info->creator = channel->owner().userLoaded(userId());
info->creatorRank = rank();
if (!rank().isEmpty()) {
info->memberRanks[userId()] = rank();
} else {
info->memberRanks.remove(userId());
}
}
}
}
@@ -651,7 +669,7 @@ void ChatParticipants::Restrict(
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail) {
Fn<void(const QString&)> onFail) {
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel(),
participant->input(),
@@ -662,9 +680,9 @@ void ChatParticipants::Restrict(
if (onDone) {
onDone();
}
}).fail([=] {
}).fail([=](const MTP::Error &error) {
if (onFail) {
onFail();
onFail(error.type());
}
}).send();
}
@@ -113,7 +113,7 @@ public:
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail);
Fn<void(const QString&)> onFail);
void add(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
+44 -1
View File
@@ -520,6 +520,40 @@ void ConfirmGiftSaleDecline(
ShowGiftSaleRejectBox(window, item, suggestion);
}
void RespondToNoForwardsRequest(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
not_null<HistoryServiceNoForwardsRequest*> request,
bool accept) {
if (request->requestId) {
return;
}
const auto id = item->fullId();
const auto session = &item->history()->session();
const auto peer = item->history()->peer;
const auto msgId = item->id;
const auto finish = [=] {
if (const auto item = session->data().message(id)) {
if (const auto r = item->Get<HistoryServiceNoForwardsRequest>()) {
r->requestId = 0;
}
}
};
using Flag = MTPmessages_ToggleNoForwards::Flag;
request->requestId = session->api().request(MTPmessages_ToggleNoForwards(
MTP_flags(Flag::f_request_msg_id),
peer->input(),
MTP_bool(!accept),
MTP_int(msgId)
)).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
finish();
}).fail([=](const MTP::Error &error) {
controller->showToast(error.type());
finish();
}).send();
}
} // namespace
std::shared_ptr<ClickHandler> AcceptClickHandler(
@@ -538,7 +572,11 @@ std::shared_ptr<ClickHandler> AcceptClickHandler(
}
const auto show = controller->uiShow();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
const auto nfRequest = item->Get<HistoryServiceNoForwardsRequest>();
if (!suggestion && !nfRequest) {
return;
} else if (nfRequest) {
RespondToNoForwardsRequest(controller, item, nfRequest, true);
return;
} else if (suggestion->gift) {
ConfirmGiftSaleAccept(controller, item, suggestion);
@@ -564,6 +602,11 @@ std::shared_ptr<ClickHandler> DeclineClickHandler(
if (!item) {
return;
}
const auto nfRequest = item->Get<HistoryServiceNoForwardsRequest>();
if (nfRequest) {
RespondToNoForwardsRequest(controller, item, nfRequest, false);
return;
}
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (suggestion && suggestion->gift) {
ConfirmGiftSaleDecline(controller, item, suggestion);
+59 -1
View File
@@ -238,6 +238,32 @@ EntitiesInText EntitiesFromMTP(
d.vlength().v,
d.is_collapsed() ? u"1"_q : QString(),
});
}, [&](const MTPDmessageEntityFormattedDate &d) {
auto flags = FormattedDateFlags();
if (d.is_relative()) {
flags |= FormattedDateFlag::Relative;
}
if (d.is_short_time()) {
flags |= FormattedDateFlag::ShortTime;
}
if (d.is_long_time()) {
flags |= FormattedDateFlag::LongTime;
}
if (d.is_short_date()) {
flags |= FormattedDateFlag::ShortDate;
}
if (d.is_long_date()) {
flags |= FormattedDateFlag::LongDate;
}
if (d.is_day_of_week()) {
flags |= FormattedDateFlag::DayOfWeek;
}
result.push_back({
EntityType::FormattedDate,
d.voffset().v,
d.vlength().v,
SerializeFormattedDateData(d.vdate().v, flags),
});
});
}
return result;
@@ -265,7 +291,8 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
&& entity.type() != EntityType::Spoiler
&& entity.type() != EntityType::MentionName
&& entity.type() != EntityType::CustomUrl
&& entity.type() != EntityType::CustomEmoji) {
&& entity.type() != EntityType::CustomEmoji
&& entity.type() != EntityType::FormattedDate) {
continue;
}
@@ -358,6 +385,37 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
v.push_back(*valid);
}
} break;
case EntityType::FormattedDate: {
const auto [date, dateFlags] = DeserializeFormattedDateData(
entity.data());
if (date) {
using Flag = MTPDmessageEntityFormattedDate::Flag;
auto mtpFlags = MTPDmessageEntityFormattedDate::Flags();
if (dateFlags & FormattedDateFlag::Relative) {
mtpFlags |= Flag::f_relative;
}
if (dateFlags & FormattedDateFlag::ShortTime) {
mtpFlags |= Flag::f_short_time;
}
if (dateFlags & FormattedDateFlag::LongTime) {
mtpFlags |= Flag::f_long_time;
}
if (dateFlags & FormattedDateFlag::ShortDate) {
mtpFlags |= Flag::f_short_date;
}
if (dateFlags & FormattedDateFlag::LongDate) {
mtpFlags |= Flag::f_long_date;
}
if (dateFlags & FormattedDateFlag::DayOfWeek) {
mtpFlags |= Flag::f_day_of_week;
}
v.push_back(MTP_messageEntityFormattedDate(
MTP_flags(mtpFlags),
offset,
length,
MTP_int(date)));
}
} break;
}
}
return MTP_vector<MTPMessageEntity>(std::move(v));
+7
View File
@@ -1128,6 +1128,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
? peerToMTP(_session->userPeerId())
: MTP_peerUser(d.vuser_id())),
MTPint(), // from_boosts_applied
MTPstring(), // from_rank
MTP_peerUser(d.vuser_id()),
MTPPeer(), // saved_peer_id
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
@@ -1170,6 +1171,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
d.vid(),
MTP_peerUser(d.vfrom_id()),
MTPint(), // from_boosts_applied
MTPstring(), // from_rank
MTP_peerChat(d.vchat_id()),
MTPPeer(), // saved_peer_id
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
@@ -1901,6 +1903,10 @@ void Updates::feedUpdate(const MTPUpdate &update) {
session().data().applyUpdate(update.c_updateChatParticipantAdmin());
} break;
case mtpc_updateChatParticipantRank: {
session().data().applyUpdate(update.c_updateChatParticipantRank());
} break;
case mtpc_updateChatDefaultBannedRights: {
session().data().applyUpdate(update.c_updateChatDefaultBannedRights());
} break;
@@ -2695,6 +2701,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
const auto &data = update.c_updateEmojiGameInfo();
_session->diceStickersPacks().apply(data);
} break;
}
}
+151 -98
View File
@@ -542,6 +542,8 @@ void ApiWrap::sendMessageFail(
} else if (show && error == u"CHAT_FORWARDS_RESTRICTED"_q) {
show->showToast(peer->isBroadcast()
? tr::lng_error_noforwards_channel(tr::now)
: peer->isUser()
? tr::lng_error_noforwards_user(tr::now)
: tr::lng_error_noforwards_group(tr::now), kJoinErrorDuration);
} else if (error == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
Settings::ShowPremium(&session(), "premium_stickers");
@@ -1966,7 +1968,7 @@ void ApiWrap::updateNotifySettingsDelayed(Data::DefaultNotify type) {
void ApiWrap::sendNotifySettingsUpdates() {
_updateNotifyQueueLifetime.destroy();
for (const auto topic : base::take(_updateNotifyTopics)) {
for (const auto &topic : base::take(_updateNotifyTopics)) {
request(MTPaccount_UpdateNotifySettings(
MTP_inputNotifyForumTopic(
topic->peer()->input(),
@@ -1974,7 +1976,7 @@ void ApiWrap::sendNotifySettingsUpdates() {
topic->notify().serialize()
)).afterDelay(kSmallDelayMs).send();
}
for (const auto peer : base::take(_updateNotifyPeers)) {
for (const auto &peer : base::take(_updateNotifyPeers)) {
request(MTPaccount_UpdateNotifySettings(
MTP_inputNotifyPeer(peer->input()),
peer->notify().serialize()
@@ -3582,7 +3584,6 @@ void ApiWrap::forwardMessages(
if (shared) {
++shared->requestsLeft;
}
const auto requestType = Data::Histories::RequestType::Send;
const auto idsCopy = localIds;
const auto scheduled = action.options.scheduled;
const auto starsPaid = std::min(
@@ -3593,64 +3594,87 @@ void ApiWrap::forwardMessages(
action.options.starsApproved -= starsPaid;
oneFlags |= SendFlag::f_allow_paid_stars;
}
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
history->sendRequestId = request(MTPmessages_ForwardMessages(
MTP_flags(oneFlags),
auto buildMessage = [=](
not_null<History*> history,
FullReplyTo replyTo)
-> Data::Histories::PreparedMessage {
const auto kGeneralId = Data::ForumTopic::kGeneralId;
const auto realTopMsgId = (replyTo.topicRootId == kGeneralId)
? MsgId(0)
: replyTo.topicRootId;
auto flags = oneFlags;
if (realTopMsgId) {
flags |= SendFlag::f_top_msg_id;
} else {
flags &= ~SendFlag::f_top_msg_id;
}
return MTPmessages_ForwardMessages(
MTP_flags(flags),
forwardFrom->input(),
MTP_vector<MTPint>(ids),
MTP_vector<MTPlong>(randomIds),
peer->input(),
MTP_int(topMsgId),
history->peer->input(),
MTP_int(realTopMsgId),
(action.options.suggest
? ReplyToForMTP(history, action.replyTo)
? ReplyToForMTP(history, replyTo)
: monoforumPeer
? MTP_inputReplyToMonoForum(monoforumPeer->input())
? MTP_inputReplyToMonoForum(
monoforumPeer->input())
: MTPInputReplyTo()),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
(sendAs
? sendAs->input()
: MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(
&history->session(),
action.options.shortcutId),
MTP_long(action.options.effectId),
MTPint(), // video_timestamp
MTPint(),
MTP_long(starsPaid),
Api::SuggestToMTP(action.options.suggest)
)).done([=](const MTPUpdates &result) {
Api::SuggestToMTP(action.options.suggest));
};
histories.sendPreparedMessage(
history,
FullReplyTo{ .topicRootId = topicRootId },
uint64(0),
std::move(buildMessage),
[=](const MTPUpdates &result, const MTP::Response &) {
if (!scheduled) {
this->updates().checkForSentToScheduled(result);
_session->api().updates().checkForSentToScheduled(
result);
}
applyUpdates(result);
if (shared && !--shared->requestsLeft) {
shared->callback();
}
const auto &ghost = AyuSettings::ghost(&session());
const auto &ghost = AyuSettings::ghost(_session);
if (!ghost.sendReadMessages() && ghost.markReadAfterAction() && history->lastMessage())
{
readHistory(history->lastMessage());
}
finish();
if (peer->isSelf() && session().premium()) {
if (peer->isSelf() && _session->premium()) {
ProcessRecentSelfForwards(
_session,
result,
peer->id,
forwardFrom->id);
}
}).fail([=](const MTP::Error &error) {
},
[=](const MTP::Error &error, const MTP::Response &) {
if (idsCopy) {
for (const auto &[randomId, itemId] : *idsCopy) {
sendMessageFail(error, peer, randomId, itemId);
_session->api().sendMessageFail(
error,
peer,
randomId,
itemId);
}
} else {
sendMessageFail(error, peer);
_session->api().sendMessageFail(error, peer);
}
finish();
}).afterRequest(
history->sendRequestId
).send();
return history->sendRequestId;
});
});
ids.resize(0);
randomIds.resize(0);
@@ -3659,7 +3683,7 @@ void ApiWrap::forwardMessages(
ids.reserve(count);
randomIds.reserve(count);
for (const auto item : draft.items) {
for (const auto &item : draft.items) {
const auto randomId = base::RandomValue<uint64>();
if (genClientSideMessage) {
const auto newId = FullMsgId(
@@ -3805,14 +3829,16 @@ void ApiWrap::sendVoiceMessage(
applyGhostScheduling(_session, scheduledAction.options, 17);
const auto caption = TextWithTags();
const auto to = FileLoadTaskOptions(scheduledAction);
_fileLoader->addTask(std::make_unique<FileLoadTask>(
&session(),
result,
duration,
waveform,
video,
to,
caption));
_fileLoader->addTask(
std::make_unique<FileLoadTask>(FileLoadTask::VoiceArgs{
.session = &session(),
.voice = result,
.duration = duration,
.waveform = waveform,
.video = video,
.to = to,
.caption = caption,
}));
}
void ApiWrap::editMedia(
@@ -3838,29 +3864,36 @@ void ApiWrap::editMedia(
}
const auto forceFile = (type == SendMediaType::File)
&& (file.type == Ui::PreparedFile::Type::Video);
_fileLoader->addTask(std::make_unique<FileLoadTask>(
&session(),
file.path,
file.content,
std::move(file.information),
(file.videoCover
? std::make_unique<FileLoadTask>(
&session(),
file.videoCover->path,
file.videoCover->content,
std::move(file.videoCover->information),
nullptr,
SendMediaType::Photo,
to,
TextWithTags(),
false)
_fileLoader->addTask(std::make_unique<FileLoadTask>(FileLoadTask::Args{
.session = &session(),
.filepath = file.path,
.content = file.content,
.information = std::move(file.information),
.videoCover = (file.videoCover
? std::make_unique<FileLoadTask>(FileLoadTask::Args{
.session = &session(),
.filepath = file.videoCover->path,
.content = file.videoCover->content,
.information = std::move(file.videoCover->information),
.videoCover = nullptr,
.type = SendMediaType::Photo,
.to = to,
.caption = TextWithTags(),
.spoiler = false,
.album = nullptr,
.forceFile = false,
.idOverride = 0,
})
: nullptr),
type,
to,
caption,
file.spoiler,
nullptr,
forceFile));
.type = type,
.to = to,
.caption = caption,
.spoiler = file.spoiler,
.album = nullptr,
.forceFile = forceFile,
.idOverride = 0,
.displayName = file.displayName,
}));
}
void ApiWrap::sendFiles(
@@ -3870,10 +3903,22 @@ void ApiWrap::sendFiles(
std::shared_ptr<SendingAlbum> album,
const SendAction &action) {
const auto haveCaption = !caption.text.isEmpty();
if (haveCaption
&& !list.canAddCaption(
const auto captionAttached = !haveCaption
? false
: (list.files.size() == 1)
? list.canAddCaption(
album != nullptr,
type == SendMediaType::Photo)) {
type == SendMediaType::Photo)
: Ui::CaptionWillBeAttached(
list,
[&] {
auto way = Ui::SendFilesWay();
way.setGroupFiles(album != nullptr);
way.setSendImagesAsPhotos(type == SendMediaType::Photo);
return way;
}(),
false);
if (haveCaption && !captionAttached) {
auto message = MessageToSend(action);
message.textWithTags = base::take(caption);
message.action.clearDraft = false;
@@ -3895,30 +3940,36 @@ void ApiWrap::sendFiles(
: SendMediaType::File;
const auto forceFile = (type == SendMediaType::File)
&& (file.type == Ui::PreparedFile::Type::Video);
tasks.push_back(std::make_unique<FileLoadTask>(
&session(),
file.path,
file.content,
std::move(file.information),
(file.videoCover
? std::make_unique<FileLoadTask>(
&session(),
file.videoCover->path,
file.videoCover->content,
std::move(file.videoCover->information),
nullptr,
SendMediaType::Photo,
to,
TextWithTags(),
false,
nullptr)
tasks.push_back(std::make_unique<FileLoadTask>(FileLoadTask::Args{
.session = &session(),
.filepath = file.path,
.content = file.content,
.information = std::move(file.information),
.videoCover = (file.videoCover
? std::make_unique<FileLoadTask>(FileLoadTask::Args{
.session = &session(),
.filepath = file.videoCover->path,
.content = file.videoCover->content,
.information = std::move(file.videoCover->information),
.videoCover = nullptr,
.type = SendMediaType::Photo,
.to = to,
.caption = TextWithTags(),
.spoiler = false,
.album = nullptr,
.forceFile = false,
.idOverride = 0,
})
: nullptr),
uploadWithType,
to,
caption,
file.spoiler,
album,
forceFile));
.type = uploadWithType,
.to = to,
.caption = caption,
.spoiler = file.spoiler,
.album = album,
.forceFile = forceFile,
.idOverride = 0,
.displayName = file.displayName,
}));
caption = TextWithTags();
}
if (album) {
@@ -3938,18 +3989,20 @@ void ApiWrap::sendFile(
const auto to = FileLoadTaskOptions(action);
auto caption = TextWithTags();
const auto spoiler = false;
const auto information = nullptr;
const auto videoCover = nullptr;
_fileLoader->addTask(std::make_unique<FileLoadTask>(
&session(),
QString(),
fileContent,
information,
videoCover,
type,
to,
caption,
spoiler));
_fileLoader->addTask(std::make_unique<FileLoadTask>(FileLoadTask::Args{
.session = &session(),
.filepath = QString(),
.content = fileContent,
.information = nullptr,
.videoCover = nullptr,
.type = type,
.to = to,
.caption = caption,
.spoiler = spoiler,
.album = nullptr,
.forceFile = false,
.idOverride = 0
}));
}
void ApiWrap::sendUploadedPhoto(
+36 -1
View File
@@ -806,14 +806,31 @@ backgroundConfirmCancel: RoundButton(backgroundConfirm) {
color: shadowFg;
}
}
urlAuthBox: Box(defaultBox) {
buttonPadding: margins(0px, 0px, 0px, 12px);
buttonHeight: 0px;
}
urlAuthCheckbox: Checkbox(defaultBoxCheckbox) {
width: 240px;
}
urlAuthCodesTitle: FlatLabel(defaultPeerListAbout) {
textFg: boxTitleFg;
style: TextStyle(defaultTextStyle) {
font: boxTitleFont;
}
}
urlAuthCheckboxAbout: FlatLabel(defaultPeerListAbout) {
style: TextStyle(boxTextStyle) {
font: font(12px);
}
}
urlAuthCodesButton: RoundButton(defaultLightButton) {
height: 58px;
textTop: 19px;
style: TextStyle(semiboldTextStyle) {
font: font(20px semibold);
}
}
urlAuthBoxRowTopLabel: FlatLabel(boxLabel) {
maxHeight: 30px;
}
@@ -840,7 +857,7 @@ boostsUnrestrictLabel: FlatLabel(defaultFlatLabel) {
}
customBadgeField: InputField(defaultInputField) {
textMargins: margins(2px, 7px, 2px, 0px);
textMargins: margins(2px, 0px, 2px, 0px);
placeholderFg: placeholderFg;
placeholderFgActive: placeholderFgActive;
@@ -849,9 +866,16 @@ customBadgeField: InputField(defaultInputField) {
placeholderScale: 0.;
placeholderFont: normalFont;
border: 0px;
borderActive: 0px;
heightMin: 32px;
}
tagPreviewLineHeight: 8px;
tagPreviewLineSpacing: 4px;
tagPreviewInputSkip: 16px;
pollResultsQuestion: FlatLabel(defaultFlatLabel) {
minWidth: 320px;
textFg: windowBoldFg;
@@ -876,6 +900,11 @@ pollResultsShowMore: SettingsButton(defaultSettingsButton) {
ripple: defaultRippleAnimation;
}
pollResultsVoteTimeFont: font(fsize);
pollResultsVoteTimeDateFont: font(fsize);
pollResultsVoteTimeRightSkip: 16px;
pollResultsVoteTimeLeftSkip: 8px;
pollResultsVoteTimeGap: 2px;
inviteViaLinkButton: SettingsButton(defaultSettingsButton) {
textFg: lightButtonFg;
@@ -1200,3 +1229,9 @@ futureOwnerBox: Box(defaultBox) {
buttonHeight: 0px;
}
futureOwnerBoxSelect: collectibleBox;
disableSharingIconPadding: margins(12px, 12px, 12px, 8px);
disableSharingButtonLock: IconEmoji {
icon: icon {{ "emoji/premium_lock", activeButtonFg }};
padding: margins(-2px, 1px, 0px, 0px);
}
@@ -470,28 +470,43 @@ bool FillChooseFilterWithAdminedGroupsMenu(
return added;
}
History *HistoryFromMimeData(
const QMimeData *mime,
not_null<Main::Session*> session) {
const auto mimeFormat = u"application/x-telegram-dialog"_q;
if (mime->hasFormat(mimeFormat)) {
auto peerId = int64(-1);
auto isTestMode = false;
auto stream = QDataStream(mime->data(mimeFormat));
stream >> peerId;
stream >> isTestMode;
if (isTestMode != session->isTestMode()) {
return nullptr;
}
return session->data().historyLoaded(PeerId(peerId));
}
if (mime->hasText()) {
auto text = mime->text().trimmed();
if (text.startsWith('@')) {
text = text.mid(1);
} else if (text.startsWith(u"https://t.me/"_q)) {
text = text.mid(13);
} else {
return nullptr;
}
if (const auto peer = session->data().peerByUsername(text)) {
return session->data().historyLoaded(peer->id);
}
}
return nullptr;
}
void SetupFilterDragAndDrop(
not_null<Ui::RpWidget*> outer,
not_null<Main::Session*> session,
Fn<std::optional<FilterId>(QPoint)> filterIdAtPosition,
Fn<FilterId()> activeFilterId) {
const auto mimeFormat = u"application/x-telegram-dialog"_q;
const auto peerIdFromMime = [=](const QMimeData *mimeData) {
auto peerId = int64(-1);
auto isTestMode = false;
if (mimeData->hasFormat(mimeFormat)) {
auto stream = QDataStream(mimeData->data(mimeFormat));
stream >> peerId;
stream >> isTestMode;
if (isTestMode != session->isTestMode()) {
return int64(-1);
}
}
return peerId;
};
const auto historyFromMime = [=](const QMimeData *mime) {
return session->data().historyLoaded(PeerId(peerIdFromMime(mime)));
};
Fn<FilterId()> activeFilterId,
Fn<void(FilterId)> selectByFilterId) {
const auto hasAction = [=](not_null<QDropEvent*> drop, bool perform) {
const auto mimeData = drop->mimeData();
const auto filterId = filterIdAtPosition(
@@ -500,7 +515,7 @@ void SetupFilterDragAndDrop(
return false;
}
const auto id = *filterId;
if (const auto h = historyFromMime(mimeData)) {
if (const auto h = HistoryFromMimeData(mimeData, session)) {
auto v = ChooseFilterValidator(h);
if (id) {
if (v.canAdd(id)) {
@@ -508,6 +523,7 @@ void SetupFilterDragAndDrop(
if (perform) {
v.add(id);
}
selectByFilterId(perform ? FilterId(-1) : id);
return true;
}
}
@@ -517,10 +533,12 @@ void SetupFilterDragAndDrop(
if (perform) {
v.remove(active);
}
selectByFilterId(perform ? FilterId(-1) : active);
return true;
}
}
}
selectByFilterId(-1);
return false;
};
outer->setAcceptDrops(true);
@@ -546,6 +564,7 @@ void SetupFilterDragAndDrop(
dm->ignore();
}
} else if (e->type() == QEvent::DragLeave) {
selectByFilterId(-1);
} else if (e->type() == QEvent::Drop) {
const auto drop = static_cast<QDropEvent*>(e.get());
if (hasAction(drop, true)) {
@@ -62,4 +62,9 @@ void SetupFilterDragAndDrop(
not_null<Ui::RpWidget*> outer,
not_null<Main::Session*> session,
Fn<std::optional<FilterId>(QPoint)> filterIdAtPosition,
Fn<FilterId()> activeFilterId);
Fn<FilterId()> activeFilterId,
Fn<void(FilterId)> selectByFilterId);
[[nodiscard]] History *HistoryFromMimeData(
const QMimeData *mime,
not_null<Main::Session*> session);
+35 -1
View File
@@ -1116,6 +1116,30 @@ void ProxyBox::prepare() {
}
}, _port->lifetime());
const auto submit = [=] {
if (_host->hasFocus()
&& !_host->getLastText().trimmed().isEmpty()) {
_port->setFocus();
} else if (_port->hasFocus()
&& !_port->getLastText().trimmed().isEmpty()) {
if (_type->current() == Type::Mtproto) {
_secret->setFocus();
} else {
_user->setFocus();
}
} else if (_user->hasFocus()) {
_password->setFocus();
} else {
save();
}
};
connect(_host.data(), &Ui::MaskedInputField::submitted, submit);
connect(_port.data(), &Ui::MaskedInputField::submitted, submit);
_user->submits(
) | rpl::on_next(submit, _user->lifetime());
connect(_password.data(), &Ui::MaskedInputField::submitted, submit);
connect(_secret.data(), &Ui::MaskedInputField::submitted, submit);
refreshButtons();
setDimensionsToContent(st::boxWideWidth, _content);
}
@@ -1421,7 +1445,7 @@ void ProxiesBoxController::ShowApplyConfirmation(
} else if (type == Type::Mtproto) {
add(proxy.password, tr::lng_proxy_box_secret);
}
box->addButton(tr::lng_sure_enable(), [=] {
const auto enableButton = box->addButton(tr::lng_sure_enable(), [=] {
auto &proxies = Core::App().settings().proxy().list();
if (!ranges::contains(proxies, proxy)) {
proxies.push_back(proxy);
@@ -1431,6 +1455,16 @@ void ProxiesBoxController::ShowApplyConfirmation(
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->events(
) | rpl::on_next([=](not_null<QEvent*> e) {
if ((e->type() != QEvent::KeyPress) || !enableButton) {
return;
}
const auto k = static_cast<QKeyEvent*>(e.get());
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {
enableButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
}
}, box->lifetime());
};
if (controller) {
controller->uiShow()->showBox(Box(box));
+24 -18
View File
@@ -114,7 +114,11 @@ constexpr auto kChangesDebounceTimeout = crl::time(1000);
return Ui::AlbumType();
}
[[nodiscard]] bool CanBeCompressed(Ui::AlbumType type) {
[[nodiscard]] bool CanToggleCompressed(Ui::AlbumType type) {
return (type == Ui::AlbumType::None);
}
[[nodiscard]] bool AlbumTypeCompressed(Ui::AlbumType type) {
return (type == Ui::AlbumType::None)
|| (type == Ui::AlbumType::PhotoVideo);
}
@@ -258,6 +262,8 @@ EditCaptionBox::EditCaptionBox(
Expects(!_initialList.files.empty());
Expects(item->allowsEditMedia());
_asFile = !AlbumTypeCompressed(_albumType);
_mediaEditManager.start(item, spoilered, invertCaption);
_controller->session().data().itemRemoved(
@@ -457,7 +463,8 @@ void EditCaptionBox::rebuildPreview() {
const auto photo = media->photo();
const auto document = media->document();
_isPhoto = (photo != nullptr);
if (photo || document->isVideoFile() || document->isAnimation()) {
_isVideo = (document != nullptr) && document->isVideoFile();
if (_isPhoto || _isVideo || document->isAnimation()) {
const auto media = Ui::CreateChild<Ui::ItemSingleMediaPreview>(
this,
st::defaultComposeControls,
@@ -475,20 +482,18 @@ void EditCaptionBox::rebuildPreview() {
}
} else {
const auto &file = _preparedList.files.front();
const auto isVideoFile = file.isVideoFile();
_isVideo = file.isVideoFile();
const auto media = Ui::SingleMediaPreview::Create(
this,
st::defaultComposeControls,
gifPaused,
file,
[=](Ui::AttachActionType type) {
return (type != Ui::AttachActionType::EditCover)
|| isVideoFile;
return (type != Ui::AttachActionType::EditCover) || _isVideo;
},
Ui::AttachControls::Type::EditOnly);
_isPhoto = (media && media->isPhoto());
const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType);
if (media && (!withCheckbox || !_asFile)) {
if (media && !_asFile) {
media->spoileredChanges(
) | rpl::on_next([=](bool spoilered) {
_mediaEditManager.apply({ .type = spoilered
@@ -666,9 +671,9 @@ void EditCaptionBox::setupControls() {
auto hintLabelToggleOn = _previewRebuilds.events_starting_with(
{}
) | rpl::map([=] {
return _controller->session().settings().photoEditorHintShown()
? (_isPhoto && !_asFile)
: false;
return _isPhoto
&& !_asFile
&& _controller->session().settings().photoEditorHintShown();
});
_controls->add(object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
@@ -684,21 +689,21 @@ void EditCaptionBox::setupControls() {
this,
object_ptr<Ui::Checkbox>(
this,
tr::lng_send_compressed_one(tr::now),
true,
tr::lng_send_as_documents_one(tr::now),
_asFile,
st::defaultBoxCheckbox),
st::editMediaCheckboxMargins)
)->toggleOn(
_previewRebuilds.events_starting_with({}) | rpl::map([=] {
return _isPhoto
&& CanBeCompressed(_albumType)
return (_isPhoto || _isVideo)
&& CanToggleCompressed(_albumType)
&& !_preparedList.files.empty();
}),
anim::type::instant
)->entity()->checkedChanges(
) | rpl::on_next([&](bool checked) {
applyChanges();
_asFile = !checked;
_asFile = checked;
rebuildPreview();
}, _controls->lifetime());
@@ -1124,11 +1129,12 @@ void EditCaptionBox::save() {
applyChanges();
}
const auto compressed = CanToggleCompressed(_albumType)
? (!_asFile)
: AlbumTypeCompressed(_albumType);
_controller->session().api().editMedia(
std::move(_preparedList),
(_isPhoto && !_asFile && CanBeCompressed(_albumType))
? SendMediaType::Photo
: SendMediaType::File,
(compressed ? SendMediaType::Photo : SendMediaType::File),
_field->getTextWithAppliedMarkdown(),
action);
closeAfterSave();
@@ -145,6 +145,7 @@ private:
base::Timer _checkChangedTimer;
bool _isPhoto = false;
bool _isVideo = false;
bool _asFile = false;
QString _error;
@@ -585,7 +585,7 @@ void EditNoPaidMessagesExceptions(
setTo.premiums = false;
setTo.miniapps = false;
auto &removeFrom = copy.never;
for (const auto peer : setTo.peers) {
for (const auto &peer : setTo.peers) {
removeFrom.peers.erase(
ranges::remove(removeFrom.peers, peer),
end(removeFrom.peers));
@@ -671,7 +671,7 @@ void EditPrivacyBox::editExceptions(
Unexpected("Invalid exception value.");
}();
auto &removeFrom = exceptions(type);
for (const auto peer : exceptions(exception).peers) {
for (const auto &peer : exceptions(exception).peers) {
removeFrom.peers.erase(
ranges::remove(removeFrom.peers, peer),
end(removeFrom.peers));
@@ -350,7 +350,8 @@ void ProccessCommonGroups(
void CreateModerateMessagesBox(
not_null<Ui::GenericBox*> box,
const HistoryItemsList &items,
Fn<void()> confirmed) {
Fn<void()> confirmed,
ModerateMessagesBoxOptions options) {
Expects(!items.empty());
using Controller = Ui::ExpandablePeerListController;
@@ -381,6 +382,20 @@ void CreateModerateMessagesBox(
const auto historyPeerId = history->peer->id;
const auto ids = session->data().itemsToIds(items);
{
const auto remainingIds
= box->lifetime().make_state<base::flat_set<FullMsgId>>(
ids.begin(),
ids.end());
session->data().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
remainingIds->erase(item->fullId());
if (remainingIds->empty()) {
box->closeBox();
}
}, box->lifetime());
}
if (ModerateCommonGroups.value() || session->supportMode()) {
ProccessCommonGroups(
items,
@@ -568,7 +583,7 @@ void CreateModerateMessagesBox(
object_ptr<Ui::Checkbox>(
box,
tr::lng_report_spam(tr::now),
false,
options.reportSpam,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
const auto controller = box->lifetime().make_state<Controller>(
@@ -599,7 +614,7 @@ void CreateModerateMessagesBox(
lt_user,
tr::bold(firstItem->from()->name()),
tr::marked),
false,
options.deleteAll,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
auto messagesCounts = MessagesCountValue(history, participants);
@@ -715,7 +730,7 @@ void CreateModerateMessagesBox(
rpl::single(isSingle),
tr::lng_ban_user(),
tr::lng_ban_users())),
false,
options.banUser,
st::defaultBoxCheckbox),
st::boxRowPadding + buttonPadding);
const auto controller = box->lifetime().make_state<Controller>(
@@ -820,7 +835,7 @@ void CreateModerateMessagesBox(
box,
prepareFlags,
disabledMessages,
{ .isForum = peer->isForum() });
{ .isForum = peer->isForum(), .isUserSpecific = true });
computeRestrictions = getRestrictions;
std::move(changes) | rpl::on_next([=] {
ban->setChecked(true);
@@ -1155,3 +1170,13 @@ void DeleteSublistBox(
}, st::attentionBoxButton);
box->addButton(tr::lng_cancel(), close);
}
ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions() {
return base::IsCtrlPressed()
? ModerateMessagesBoxOptions{
.reportSpam = true,
.deleteAll = true,
.banUser = true,
}
: ModerateMessagesBoxOptions{};
}
@@ -19,10 +19,19 @@ class GenericBox;
extern const char kModerateCommonGroups[];
struct ModerateMessagesBoxOptions final {
bool reportSpam = false;
bool deleteAll = false;
bool banUser = false;
};
[[nodiscard]] ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions();
void CreateModerateMessagesBox(
not_null<Ui::GenericBox*> box,
const HistoryItemsList &items,
Fn<void()> confirmed);
Fn<void()> confirmed,
ModerateMessagesBoxOptions options);
[[nodiscard]] bool CanCreateModerateMessagesBox(const HistoryItemsList &);
+1 -1
View File
@@ -378,7 +378,7 @@ public:
template <typename PeerDataRange>
void peerListAddSelectedPeers(PeerDataRange &&range) {
for (const auto peer : range) {
for (const auto &peer : range) {
peerListAddSelectedPeerInBunch(peer);
}
peerListFinishSelectedRowsBunch();
@@ -54,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_profile.h"
#include "styles/style_dialogs.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
namespace {
@@ -852,7 +853,7 @@ void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
if (const auto forum = peer->forum()) {
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
auto callback = [=](not_null<Data::ForumTopic*> topic) {
auto callback = [=](not_null<Data::Thread*> thread) {
const auto exists = guard.get();
if (!exists) {
if (*weak) {
@@ -861,15 +862,15 @@ void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
return;
}
auto onstack = std::move(_callback);
onstack(topic);
onstack(thread);
if (guard) {
_callback = std::move(onstack);
} else if (*weak) {
(*weak)->closeBox();
}
};
const auto filter = [=](not_null<Data::ForumTopic*> topic) {
return guard && (!_filter || _filter(topic));
const auto filter = [=](not_null<Data::Thread*> thread) {
return guard && (!_filter || _filter(thread));
};
auto owned = Box<PeerListBox>(
std::make_unique<ChooseTopicBoxController>(
@@ -1095,10 +1096,62 @@ auto ChooseTopicBoxController::Row::generateNameWords() const
return _topic->chatListNameWords();
}
QString ChooseTopicBoxController::AllMessagesRow::name() const {
return _userCreatesTopics
? tr::lng_forum_create_new_topic(tr::now)
: tr::lng_forum_all_messages(tr::now);
}
ChooseTopicBoxController::AllMessagesRow::AllMessagesRow(bool userCreatesTopics)
: PeerListRow(PeerListRowId(0))
, _userCreatesTopics(userCreatesTopics) {
const auto words = TextUtilities::PrepareSearchWords(name());
for (const auto &word : words) {
_nameWords.emplace(word);
_nameFirstLetters.emplace(word[0]);
}
}
QString ChooseTopicBoxController::AllMessagesRow::generateName() {
return name();
}
QString ChooseTopicBoxController::AllMessagesRow::generateShortName() {
return name();
}
auto ChooseTopicBoxController::AllMessagesRow::generatePaintUserpicCallback(
bool forceRound)
-> PaintRoundImageCallback {
return [userCreatesTopics = _userCreatesTopics](
Painter &p,
int x,
int y,
int outerWidth,
int size) {
const auto &icon = userCreatesTopics
? st::menuIconDiscussion
: st::menuIconChats;
icon.paintInCenter(
p,
QRect(x, y - st::lineWidth, size, size));
};
}
auto ChooseTopicBoxController::AllMessagesRow::generateNameFirstLetters() const
-> const base::flat_set<QChar> & {
return _nameFirstLetters;
}
auto ChooseTopicBoxController::AllMessagesRow::generateNameWords() const
-> const base::flat_set<QString> & {
return _nameWords;
}
ChooseTopicBoxController::ChooseTopicBoxController(
not_null<Data::Forum*> forum,
FnMut<void(not_null<Data::ForumTopic*>)> callback,
Fn<bool(not_null<Data::ForumTopic*>)> filter)
FnMut<void(not_null<Data::Thread*>)> callback,
Fn<bool(not_null<Data::Thread*>)> filter)
: PeerListController(std::make_unique<ChooseTopicSearchController>(forum))
, _forum(forum)
, _callback(std::move(callback))
@@ -1127,7 +1180,11 @@ Main::Session &ChooseTopicBoxController::session() const {
void ChooseTopicBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto weak = base::make_weak(this);
auto onstack = base::take(_callback);
onstack(static_cast<Row*>(row.get())->topic());
if (row->id() == PeerListRowId(0)) {
onstack(_forum->history());
} else {
onstack(static_cast<Row*>(row.get())->topic());
}
if (weak) {
_callback = std::move(onstack);
}
@@ -1155,6 +1212,15 @@ void ChooseTopicBoxController::prepare() {
void ChooseTopicBoxController::refreshRows(bool initial) {
auto added = false;
if (_forum->bot()
&& !delegate()->peerListFindRow(PeerListRowId(0))
&& (!_filter || _filter(_forum->history()))) {
const auto userCreatesTopics = Data::IsBotUserCreatesTopics(
_forum->peer());
delegate()->peerListAppendRow(
std::make_unique<AllMessagesRow>(userCreatesTopics));
added = true;
}
for (const auto &row : _forum->topicsList()->indexed()->all()) {
if (const auto topic = row->topic()) {
const auto id = topic->rootId().bare;
@@ -354,8 +354,8 @@ class ChooseTopicBoxController final
public:
ChooseTopicBoxController(
not_null<Data::Forum*> forum,
FnMut<void(not_null<Data::ForumTopic*>)> callback,
Fn<bool(not_null<Data::ForumTopic*>)> filter = nullptr);
FnMut<void(not_null<Data::Thread*>)> callback,
Fn<bool(not_null<Data::Thread*>)> filter = nullptr);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
@@ -391,13 +391,36 @@ private:
};
class AllMessagesRow final : public PeerListRow {
public:
explicit AllMessagesRow(bool userCreatesTopics);
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
auto generateNameFirstLetters() const
-> const base::flat_set<QChar> & override;
auto generateNameWords() const
-> const base::flat_set<QString> & override;
private:
[[nodiscard]] QString name() const;
base::flat_set<QChar> _nameFirstLetters;
base::flat_set<QString> _nameWords;
bool _userCreatesTopics = false;
};
void refreshRows(bool initial = false);
[[nodiscard]] std::unique_ptr<Row> createRow(
not_null<Data::ForumTopic*> topic);
const not_null<Data::Forum*> _forum;
FnMut<void(not_null<Data::ForumTopic*>)> _callback;
Fn<bool(not_null<Data::ForumTopic*>)> _filter;
FnMut<void(not_null<Data::Thread*>)> _callback;
Fn<bool(not_null<Data::Thread*>)> _filter;
};
@@ -233,7 +233,7 @@ void AddBotToGroupBoxController::addBotToGroup(not_null<PeerData*> chat) {
const auto token = _token;
const auto done = [=](
ChatAdminRightsInfo newRights,
const QString &rank) {
const std::optional<QString> &rank) {
if (scope == Scope::GroupAdmin) {
chat->session().api().sendBotStart(show, bot, chat, token);
}
@@ -1500,14 +1500,14 @@ void AddSpecialBoxController::showAdmin(
_peer,
user,
currentRights,
_additional.adminRank(user),
_additional.memberRank(user),
_additional.adminPromotedSince(user),
_additional.adminPromotedBy(user));
const auto show = delegate()->peerListUiShow();
if (_additional.canAddOrEditAdmin(user)) {
const auto done = crl::guard(this, [=](
ChatAdminRightsInfo newRights,
const QString &rank) {
const std::optional<QString> &rank) {
editAdminDone(user, newRights, rank);
});
const auto fail = crl::guard(this, [=] {
@@ -1524,12 +1524,15 @@ void AddSpecialBoxController::showAdmin(
void AddSpecialBoxController::editAdminDone(
not_null<UserData*> user,
ChatAdminRightsInfo rights,
const QString &rank) {
const std::optional<QString> &rank) {
if (_editParticipantBox) {
_editParticipantBox->closeBox();
}
_additional.applyAdminLocally(user, rights, rank);
_additional.applyAdminLocally(
user,
rights,
rank.value_or(_additional.memberRank(user)));
// _adminDoneCallback should call changes().chatAdminUpdated.
if (const auto callback = _adminDoneCallback) {
callback(user, rights, rank);
@@ -1582,6 +1585,7 @@ void AddSpecialBoxController::showRestricted(
user,
_additional.adminRights(user).has_value(),
currentRights,
_additional.memberRank(user),
_additional.restrictedBy(user),
_additional.restrictedSince(user));
if (_additional.canRestrictParticipant(user)) {
@@ -1594,8 +1598,9 @@ void AddSpecialBoxController::showRestricted(
_editParticipantBox->closeBox();
}
});
const auto show = delegate()->peerListUiShow();
box->setSaveCallback(
SaveRestrictedCallback(_peer, user, done, fail));
SaveRestrictedCallback(show, _peer, user, done, fail));
}
_editParticipantBox = showBox(std::move(box));
}
@@ -1668,7 +1673,9 @@ void AddSpecialBoxController::kickUser(
const auto fail = crl::guard(this, [=] {
_editBox = nullptr;
});
const auto show = delegate()->peerListUiShow();
const auto callback = SaveRestrictedCallback(
show,
_peer,
participant,
done,
@@ -101,7 +101,7 @@ public:
using AdminDoneCallback = Fn<void(
not_null<UserData*> user,
ChatAdminRightsInfo adminRights,
const QString &rank)>;
const std::optional<QString> &rank)>;
using BannedDoneCallback = Fn<void(
not_null<PeerData*> participant,
ChatRestrictionsInfo bannedRights)>;
@@ -133,7 +133,7 @@ private:
void editAdminDone(
not_null<UserData*> user,
ChatAdminRightsInfo rights,
const QString &rank);
const std::optional<QString> &rank);
void showRestricted(not_null<UserData*> user, bool sure = false);
void editRestrictedDone(
not_null<PeerData*> participant,
@@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "boxes/passcode_box.h"
#include "core/core_cloud_password.h"
#include "data/data_channel.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
@@ -47,21 +46,10 @@ bool ChannelOwnershipTransfer::handleTransferPasswordError(
}
void ChannelOwnershipTransfer::start() {
if (const auto chat = _peer->asChatNotMigrated()) {
_peer->session().api().migrateChat(chat, [=](
not_null<ChannelData*> channel) {
startTransfer(channel);
});
} else if (const auto channel = _peer->asChannelOrMigrated()) {
startTransfer(channel);
}
}
void ChannelOwnershipTransfer::startTransfer(not_null<ChannelData*> channel) {
const auto api = &channel->session().api();
const auto api = &_peer->session().api();
api->cloudPassword().reload();
api->request(MTPchannels_EditCreator(
channel->inputChannel(),
api->request(MTPmessages_EditChatCreator(
_peer->input(),
MTP_inputUserEmpty(),
MTP_inputCheckPasswordEmpty()
)).fail([=](const MTP::Error &error) {
@@ -78,7 +66,7 @@ void ChannelOwnershipTransfer::startTransfer(not_null<ChannelData*> channel) {
.text = tr::lng_rights_transfer_about(
tr::now,
lt_group,
tr::bold(channel->name()),
tr::bold(_peer->name()),
lt_user,
tr::bold(_selectedUser->shortName()),
tr::rich),
@@ -110,20 +98,16 @@ void ChannelOwnershipTransfer::requestPassword() {
void ChannelOwnershipTransfer::sendRequest(
base::weak_qptr<PasscodeBox> box,
const Core::CloudPasswordResult &result) {
const auto channel = _peer->asChannelOrMigrated();
if (!channel) {
return;
}
const auto api = &channel->session().api();
api->request(MTPchannels_EditCreator(
channel->inputChannel(),
const auto api = &_peer->session().api();
api->request(MTPmessages_EditChatCreator(
_peer->input(),
_selectedUser->inputUser(),
result.result
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
const auto currentShow = box ? box->uiShow() : _show;
currentShow->showToast(
(channel->isBroadcast()
(_peer->isBroadcast()
? tr::lng_rights_transfer_done_channel
: tr::lng_rights_transfer_done_group)(
tr::now,
@@ -143,11 +127,13 @@ void ChannelOwnershipTransfer::sendRequest(
} else if (type == u"CHANNELS_ADMIN_LOCATED_TOO_MUCH"_q) {
return tr::lng_channels_too_much_located_other(tr::now);
} else if (type == u"ADMINS_TOO_MUCH"_q) {
return (channel->isBroadcast()
return (_peer->isBroadcast()
? tr::lng_error_admin_limit_channel
: tr::lng_error_admin_limit)(tr::now);
} else if (type == u"CHANNEL_INVALID"_q) {
return (channel->isBroadcast()
} else if (type == u"CHANNEL_INVALID"_q
|| type == u"CHAT_CREATOR_REQUIRED"_q
|| type == u"PARTICIPANT_MISSING"_q) {
return (_peer->isBroadcast()
? tr::lng_channel_not_accessible
: tr::lng_group_not_accessible)(tr::now);
}
@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class ChannelData;
class UserData;
namespace Ui {
@@ -36,7 +35,6 @@ private:
void sendRequest(
base::weak_qptr<PasscodeBox> box,
const Core::CloudPasswordResult &result);
void startTransfer(not_null<ChannelData*> channel);
const not_null<PeerData*> _peer;
const not_null<UserData*> _selectedUser;
@@ -111,7 +111,7 @@ void Controller::prepare() {
if (_chat) {
appendRow(_chat);
} else {
for (const auto chat : _chats) {
for (const auto &chat : _chats) {
appendRow(chat);
}
if (_chats.size() >= kEnableSearchRowsCount) {
@@ -34,44 +34,50 @@ namespace {
[[nodiscard]] object_ptr<Ui::RpWidget> CreateMembersVisibleButton(
not_null<ChannelData*> megagroup) {
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
const auto min = EnableHideMembersMin(megagroup);
if (!megagroup->canBanMembers() || megagroup->membersCount() < min) {
const auto showHideMembers = megagroup->canBanMembers()
&& megagroup->membersCount() >= min;
if (!showHideMembers) {
return { nullptr };
}
struct State {
rpl::event_stream<bool> toggled;
};
Ui::AddSkip(container);
const auto state = container->lifetime().make_state<State>();
const auto button = container->add(
EditPeerInfoBox::CreateButton(
container,
tr::lng_profile_hide_participants(),
rpl::single(QString()),
[] {},
st::manageGroupNoIconButton,
{}
))->toggleOn(rpl::single(
(megagroup->flags() & ChannelDataFlag::ParticipantsHidden) != 0
) | rpl::then(state->toggled.events()));
Ui::AddSkip(container);
Ui::AddDividerText(container, tr::lng_profile_hide_participants_about());
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
button->toggledValue(
) | rpl::on_next([=](bool toggled) {
megagroup->session().api().request(
MTPchannels_ToggleParticipantsHidden(
megagroup->inputChannel(),
MTP_bool(toggled)
)
).done([=](const MTPUpdates &result) {
megagroup->session().api().applyUpdates(result);
}).send();
}, button->lifetime());
if (showHideMembers) {
struct State {
rpl::event_stream<bool> toggled;
};
Ui::AddSkip(container);
const auto state = container->lifetime().make_state<State>();
const auto button = container->add(
EditPeerInfoBox::CreateButton(
container,
tr::lng_profile_hide_participants(),
rpl::single(QString()),
[] {},
st::manageGroupNoIconButton,
{}
))->toggleOn(rpl::single(
(megagroup->flags() & ChannelDataFlag::ParticipantsHidden) != 0
) | rpl::then(state->toggled.events()));
Ui::AddSkip(container);
Ui::AddDividerText(
container,
tr::lng_profile_hide_participants_about());
button->toggledValue(
) | rpl::on_next([=](bool toggled) {
megagroup->session().api().request(
MTPchannels_ToggleParticipantsHidden(
megagroup->inputChannel(),
MTP_bool(toggled)
)
).done([=](const MTPUpdates &result) {
megagroup->session().api().applyUpdates(result);
}).send();
}, button->lifetime());
}
return result;
}
@@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peers/edit_participant_box.h"
#include "boxes/peers/edit_tag_control.h"
#include "boxes/peers/edit_participants_box.h"
#include "history/view/history_view_message.h"
#include "lang/lang_keys.h"
#include "ui/controls/userpic_button.h"
#include "ui/vertical_list.h"
@@ -31,8 +34,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/add_bot_to_chat_box.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "boxes/peers/edit_peer_info_box.h"
#include "settings/settings_common.h"
#include "data/data_peer_values.h"
#include "data/data_channel.h"
#include "data/data_changes.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "base/unixtime.h"
@@ -41,13 +46,71 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
#include "styles/style_settings.h"
namespace {
constexpr auto kMaxRestrictDelayDays = 366;
constexpr auto kSecondsInDay = 24 * 60 * 60;
constexpr auto kSecondsInWeek = 7 * kSecondsInDay;
constexpr auto kAdminRoleLimit = 16;
class Cover final : public Ui::FixedHeightWidget {
public:
Cover(QWidget *parent, not_null<UserData*> user, bool hasAdminRights);
private:
void setupChildGeometry();
const style::InfoProfileCover &_st;
const not_null<UserData*> _user;
object_ptr<Ui::UserpicButton> _userpic;
object_ptr<Ui::FlatLabel> _name = { nullptr };
object_ptr<Ui::FlatLabel> _status = { nullptr };
};
Cover::Cover(
QWidget *parent,
not_null<UserData*> user,
bool hasAdminRights)
: FixedHeightWidget(parent, st::infoEditContactCover.height)
, _st(st::infoEditContactCover)
, _user(user)
, _userpic(this, _user, _st.photo) {
_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
_name = object_ptr<Ui::FlatLabel>(this, _st.name);
_name->setText(_user->name());
const auto statusText = [&] {
if (_user->isBot()) {
const auto seesAllMessages = _user->botInfo->readsAllHistory
|| hasAdminRights;
return (seesAllMessages
? tr::lng_status_bot_reads_all
: tr::lng_status_bot_not_reads_all)(tr::now);
}
return Data::OnlineText(_user->lastseen(), base::unixtime::now());
}();
_status = object_ptr<Ui::FlatLabel>(this, _st.status);
_status->setText(statusText);
_status->setAttribute(Qt::WA_TransparentForMouseEvents);
setupChildGeometry();
}
void Cover::setupChildGeometry() {
widthValue(
) | rpl::on_next([this](int newWidth) {
_userpic->moveToLeft(_st.photoLeft, _st.photoTop, newWidth);
auto nameWidth = newWidth - _st.nameLeft - _st.rightSkip;
_name->resizeToNaturalWidth(nameWidth);
_name->moveToLeft(_st.nameLeft, _st.nameTop, newWidth);
auto statusWidth = newWidth - _st.statusLeft - _st.rightSkip;
_status->resizeToNaturalWidth(statusWidth);
_status->moveToLeft(_st.statusLeft, _st.statusTop, newWidth);
}, lifetime());
}
} // namespace
@@ -66,6 +129,8 @@ public:
return _rows;
}
void setCoverMode(bool enabled);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
@@ -76,6 +141,7 @@ private:
object_ptr<Ui::UserpicButton> _userPhoto;
Ui::Text::String _userName;
bool _hasAdminRights = false;
bool _coverMode = false;
object_ptr<Ui::VerticalLayout> _rows;
};
@@ -110,7 +176,20 @@ Widget *EditParticipantBox::Inner::addControl(
return _rows->add(std::move(widget), margin);
}
void EditParticipantBox::Inner::setCoverMode(bool enabled) {
_coverMode = enabled;
if (enabled) {
_userPhoto->hide();
}
resizeToWidth(width());
}
int EditParticipantBox::Inner::resizeGetHeight(int newWidth) {
if (_coverMode) {
_rows->resizeToWidth(newWidth);
_rows->moveToLeft(0, 0, newWidth);
return _rows->heightNoMargins();
}
_userPhoto->moveToLeft(
st::rightsPhotoMargin.left(),
st::rightsPhotoMargin.top());
@@ -127,6 +206,10 @@ void EditParticipantBox::Inner::paintEvent(QPaintEvent *e) {
p.fillRect(e->rect(), st::boxBg);
if (_coverMode) {
return;
}
p.setPen(st::contactsNameFg);
auto namex = st::rightsPhotoMargin.left()
+ st::rightsPhotoButton.size .width()
@@ -198,6 +281,12 @@ bool EditParticipantBox::amCreator() const {
Unexpected("Peer type in EditParticipantBox::Inner::amCreator.");
}
void EditParticipantBox::setCoverMode(bool enabled) {
Expects(_inner != nullptr);
_inner->setCoverMode(enabled);
}
EditAdminBox::EditAdminBox(
QWidget*,
not_null<PeerData*> peer,
@@ -234,7 +323,8 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const {
| Flag::InviteByLinkOrAdd
| Flag::ManageTopics
| Flag::PinMessages
| Flag::ManageCall) }
| Flag::ManageCall
| Flag::ManageRanks) }
: ChatAdminRightsInfo{ (Flag::ChangeInfo
| Flag::PostMessages
| Flag::EditMessages
@@ -255,6 +345,11 @@ void EditAdminBox::prepare() {
EditParticipantBox::prepare();
setCoverMode(true);
addControl(
object_ptr<Cover>(this, user(), hasAdminRights()),
style::margins());
setTitle(_addingBot
? (_addingBot->existing
? tr::lng_rights_edit_admin()
@@ -395,7 +490,6 @@ void EditAdminBox::prepare() {
Ui::AddSkip(emptyAboutAddAdminsInner->entity());
if (hasRank) {
Ui::AddDivider(emptyAboutAddAdminsInner->entity());
Ui::AddSkip(emptyAboutAddAdminsInner->entity());
}
Ui::AddSkip(aboutAddAdminsInner->entity());
Ui::AddDividerText(
@@ -433,7 +527,65 @@ void EditAdminBox::prepare() {
}
if (canSave()) {
_rank = hasRank ? addRankInput(inner).get() : nullptr;
if (hasRank) {
const auto role = _oldRights.flags
? LookupBadgeRole(peer(), user())
: HistoryView::BadgeRole::Admin;
_tagControl = inner->add(
object_ptr<EditTagControl>(
inner,
&peer()->session(),
user(),
_oldRank,
role),
style::margins());
}
if (_tagControl) {
Ui::AddSkip(inner);
Ui::AddDividerText(
inner,
user()->isSelf()
? tr::lng_rights_tag_about_self()
: tr::lng_rights_tag_about(
lt_name,
rpl::single(user()->shortName())));
}
if (_oldRights.flags && !_addingBot) {
const auto isTargetCreator = (LookupBadgeRole(peer(), user())
== HistoryView::BadgeRole::Creator);
if (!isTargetCreator) {
if (!_tagControl) {
Ui::AddSkip(inner);
inner->add(
object_ptr<Ui::BoxContentDivider>(inner),
{ 0, st::infoProfileSkip, 0, st::infoProfileSkip });
}
Ui::AddSkip(inner);
const auto dismissButton = Settings::AddButtonWithIcon(
inner,
tr::lng_rights_dismiss_admin(),
st::settingsAttentionButton,
{ nullptr });
dismissButton->setClickedCallback([=] {
if (const auto callback = _saveCallback) {
const auto old = _oldRights;
const auto confirmed = [=](Fn<void()> close) {
callback(old, {}, {});
close();
};
uiShow()->showBox(
Ui::MakeConfirmBox({
.text = tr::lng_profile_sure_remove_admin(
tr::now,
lt_user,
user()->firstName),
.confirmed = confirmed,
.confirmText = tr::lng_box_remove(),
}));
}
});
}
}
_finishSave = [=, value = getChecked] {
const auto newFlags = (value() | ChatAdminRight::Other)
& ((!channel || channel->amCreator())
@@ -442,7 +594,9 @@ void EditAdminBox::prepare() {
_saveCallback(
_oldRights,
ChatAdminRightsInfo(newFlags),
_rank ? _rank->getLastText().trimmed() : QString());
_tagControl
? std::optional<QString>(_tagControl->currentRank())
: std::nullopt);
};
_save = [=] {
const auto show = uiShow();
@@ -497,56 +651,6 @@ void EditAdminBox::refreshButtons() {
}
}
not_null<Ui::InputField*> EditAdminBox::addRankInput(
not_null<Ui::VerticalLayout*> container) {
// Ui::AddDivider(container);
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_rights_edit_admin_rank_name(),
st::rightsHeaderLabel),
st::rightsHeaderMargin);
const auto isOwner = [&] {
if (user()->isSelf() && amCreator()) {
return true;
} else if (const auto chat = peer()->asChat()) {
return chat->creator == peerToUser(user()->id);
} else if (const auto channel = peer()->asChannel()) {
return channel->mgInfo && channel->mgInfo->creator == user();
}
Unexpected("Peer type in EditAdminBox::addRankInput.");
}();
const auto result = container->add(
object_ptr<Ui::InputField>(
container,
st::customBadgeField,
(isOwner ? tr::lng_owner_badge : tr::lng_admin_badge)(),
TextUtilities::RemoveEmoji(_oldRank)),
st::rightsAboutMargin);
result->setMaxLength(kAdminRoleLimit);
result->setInstantReplaces(Ui::InstantReplaces::TextOnly());
result->changes(
) | rpl::on_next([=] {
const auto text = result->getLastText();
const auto removed = TextUtilities::RemoveEmoji(text);
if (removed != text) {
result->setText(removed);
}
}, result->lifetime());
Ui::AddSkip(container);
Ui::AddDividerText(
container,
tr::lng_rights_edit_admin_rank_about(
lt_title,
(isOwner ? tr::lng_owner_badge : tr::lng_admin_badge)()));
Ui::AddSkip(container);
return result;
}
bool EditAdminBox::canTransferOwnership() const {
if (user()->isInaccessible() || user()->isBot() || user()->isSelf()) {
return false;
@@ -599,10 +703,12 @@ EditRestrictedBox::EditRestrictedBox(
not_null<UserData*> user,
bool hasAdminRights,
ChatRestrictionsInfo rights,
const QString &rank,
UserData *by,
TimeId since)
: EditParticipantBox(nullptr, peer, user, hasAdminRights)
, _oldRights(rights)
, _oldRank(rank)
, _by(by)
, _since(since) {
}
@@ -613,6 +719,11 @@ void EditRestrictedBox::prepare() {
EditParticipantBox::prepare();
setCoverMode(true);
addControl(
object_ptr<Cover>(this, user(), hasAdminRights()),
style::margins());
setTitle(tr::lng_rights_user_restrictions());
Ui::AddDivider(verticalLayout());
@@ -658,13 +769,36 @@ void EditRestrictedBox::prepare() {
this,
prepareFlags,
disabledMessages,
{ .isForum = peer()->isForum() });
{ .isForum = peer()->isForum(), .isUserSpecific = true });
addControl(std::move(checkboxes), QMargins());
if (canSave() && peer()->canManageRanks()) {
Ui::AddSkip(verticalLayout());
Ui::AddDivider(verticalLayout());
_tagControl = addControl(
object_ptr<EditTagControl>(
this,
&peer()->session(),
user(),
_oldRank,
HistoryView::BadgeRole::User),
style::margins());
Ui::AddSkip(verticalLayout());
Ui::AddDividerText(
verticalLayout(),
user()->isSelf()
? tr::lng_rights_tag_about_self()
: tr::lng_rights_tag_about(
lt_name,
rpl::single(user()->shortName())));
}
_until = prepareRights.until;
addControl(
object_ptr<Ui::FixedHeightWidget>(this, st::defaultVerticalListSkip));
Ui::AddDivider(verticalLayout());
if (!_tagControl) {
addControl(
object_ptr<Ui::FixedHeightWidget>(this, st::defaultVerticalListSkip));
Ui::AddDivider(verticalLayout());
}
addControl(
object_ptr<Ui::FlatLabel>(
this,
@@ -703,6 +837,107 @@ void EditRestrictedBox::prepare() {
}
if (canSave()) {
Ui::AddSkip(verticalLayout());
Ui::AddDivider(verticalLayout());
Ui::AddSkip(verticalLayout());
const auto canAddAdmins = (chat && chat->canAddAdmins())
|| (channel && channel->canAddAdmins());
if (canAddAdmins) {
const auto promoteButton = Settings::AddButtonWithIcon(
verticalLayout(),
tr::lng_rights_promote_member(),
st::settingsButtonNoIcon,
{ nullptr });
promoteButton->setClickedCallback([=] {
const auto rank = _tagControl
? _tagControl->currentRank()
: _oldRank;
auto adminBox = Box<EditAdminBox>(
peer(),
user(),
ChatAdminRightsInfo(),
rank,
TimeId(0),
nullptr);
const auto adminBoxWeak = QPointer<Ui::BoxContent>(
adminBox.data());
const auto show = uiShow();
const auto restrictWeak = QPointer<EditRestrictedBox>(
this);
const auto closeBoth = [=] {
if (adminBoxWeak) {
adminBoxWeak->closeBox();
}
if (restrictWeak) {
restrictWeak->closeBox();
}
};
const auto savedUser = user();
const auto savedPeer = peer();
const auto done = [=](
ChatAdminRightsInfo newRights,
const std::optional<QString> &rank) {
closeBoth();
const auto effectiveRank = rank.value_or([&] {
const auto ch = savedPeer->asChannel();
if (!ch) {
return QString();
}
const auto info = ch->mgInfo.get();
if (!info) {
return QString();
}
const auto i = info->memberRanks.find(
peerToUser(savedUser->id));
return (i != end(info->memberRanks))
? i->second
: QString();
}());
savedUser->session().changes().chatAdminChanged(
savedPeer,
savedUser,
newRights.flags,
effectiveRank);
};
const auto fail = closeBoth;
adminBox->setSaveCallback(
SaveAdminCallback(
show,
peer(),
user(),
done,
fail));
show->showBox(std::move(adminBox));
});
}
const auto removeButton = Settings::AddButtonWithIcon(
verticalLayout(),
tr::lng_rights_remove_member(),
st::settingsAttentionButton,
{ nullptr });
removeButton->setClickedCallback([=] {
if (const auto callback = _saveCallback) {
const auto old = _oldRights;
const auto confirmed = [=](Fn<void()> close) {
callback(
old,
ChannelData::KickedRestrictedRights(user()));
close();
};
uiShow()->show(
Ui::MakeConfirmBox({
.text = tr::lng_profile_sure_kick(
tr::now,
lt_user,
user()->firstName),
.confirmed = confirmed,
.confirmText = tr::lng_box_remove(),
}));
}
});
const auto save = [=, value = getRestrictions] {
if (!_saveCallback) {
return;
@@ -710,6 +945,18 @@ void EditRestrictedBox::prepare() {
_saveCallback(
_oldRights,
ChatRestrictionsInfo{ value(), getRealUntilValue() });
if (_tagControl) {
const auto rank = _tagControl->currentRank();
if (rank != _oldRank) {
SaveMemberRank(
uiShow(),
peer(),
user(),
rank,
nullptr,
nullptr);
}
}
};
addButton(tr::lng_settings_save(), save);
addButton(tr::lng_cancel(), [=] { closeBox(); });
@@ -23,6 +23,7 @@ class SlideWrap;
} // namespace Ui
class ChannelOwnershipTransfer;
class EditTagControl;
class EditParticipantBox : public Ui::BoxContent {
public:
@@ -48,6 +49,8 @@ protected:
template <typename Widget>
Widget *addControl(object_ptr<Widget> widget, QMargins margin = {});
void setCoverMode(bool enabled);
bool hasAdminRights() const {
return _hasAdminRights;
}
@@ -83,7 +86,7 @@ public:
Fn<void(
ChatAdminRightsInfo,
ChatAdminRightsInfo,
const QString &rank)> callback) {
const std::optional<QString> &rank)> callback) {
_saveCallback = std::move(callback);
}
@@ -93,15 +96,13 @@ protected:
private:
[[nodiscard]] ChatAdminRightsInfo defaultRights() const;
not_null<Ui::InputField*> addRankInput(
not_null<Ui::VerticalLayout*> container);
void transferOwnership();
bool canSave() const {
[[nodiscard]] bool canSave() const {
return _saveCallback != nullptr;
}
void finishAddAdmin();
void refreshButtons();
bool canTransferOwnership() const;
[[nodiscard]] bool canTransferOwnership() const;
not_null<Ui::SlideWrap<Ui::RpWidget>*> setupTransferButton(
not_null<Ui::VerticalLayout*> container,
bool isGroup);
@@ -111,12 +112,12 @@ private:
Fn<void(
ChatAdminRightsInfo,
ChatAdminRightsInfo,
const QString &rank)> _saveCallback;
const std::optional<QString> &rank)> _saveCallback;
base::weak_qptr<Ui::BoxContent> _confirmBox;
Ui::Checkbox *_addAsAdmin = nullptr;
Ui::SlideWrap<Ui::VerticalLayout> *_adminControlsWrap = nullptr;
Ui::InputField *_rank = nullptr;
EditTagControl *_tagControl = nullptr;
Fn<void()> _save, _finishSave;
@@ -139,6 +140,7 @@ public:
not_null<UserData*> user,
bool hasAdminRights,
ChatRestrictionsInfo rights,
const QString &rank,
UserData *by,
TimeId since);
@@ -164,6 +166,8 @@ private:
TimeId getRealUntilValue() const;
const ChatRestrictionsInfo _oldRights;
const QString _oldRank;
EditTagControl *_tagControl = nullptr;
UserData *_by = nullptr;
TimeId _since = 0;
TimeId _until = 0;
File diff suppressed because it is too large Load Diff
@@ -32,23 +32,41 @@ class ChatParticipant;
Fn<void(
ChatAdminRightsInfo oldRights,
ChatAdminRightsInfo newRights,
const QString &rank)> SaveAdminCallback(
const std::optional<QString> &rank)> SaveAdminCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<UserData*> user,
Fn<void(
ChatAdminRightsInfo newRights,
const QString &rank)> onDone,
const std::optional<QString> &rank)> onDone,
Fn<void()> onFail);
Fn<void(
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights)> SaveRestrictedCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<PeerData*> participant,
Fn<void(ChatRestrictionsInfo newRights)> onDone,
Fn<void()> onFail);
void EditCustomRankBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<UserData*> user,
const QString &currentRank,
bool isSelf,
Fn<void(QString rank)> onSaved);
void SaveMemberRank(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<UserData*> user,
const QString &rank,
Fn<void()> onDone,
Fn<void()> onFail);
void SubscribeToMigration(
not_null<PeerData*> peer,
rpl::lifetime &lifetime,
@@ -106,7 +124,7 @@ public:
not_null<PeerData*> participant) const;
[[nodiscard]] std::optional<ChatAdminRightsInfo> adminRights(
not_null<UserData*> user) const;
[[nodiscard]] QString adminRank(not_null<UserData*> user) const;
[[nodiscard]] QString memberRank(not_null<UserData*> user) const;
[[nodiscard]] std::optional<ChatRestrictionsInfo> restrictedRights(
not_null<PeerData*> participant) const;
[[nodiscard]] bool isCreator(not_null<UserData*> user) const;
@@ -129,6 +147,9 @@ public:
void applyBannedLocally(
not_null<PeerData*> participant,
ChatRestrictionsInfo rights);
void applyMemberRankLocally(
not_null<UserData*> user,
const QString &rank);
private:
UserData *applyCreator(const Api::ChatParticipant &data);
@@ -148,7 +169,7 @@ private:
// Data for channels.
base::flat_map<not_null<UserData*>, ChatAdminRightsInfo> _adminRights;
base::flat_map<not_null<UserData*>, QString> _adminRanks;
base::flat_map<not_null<UserData*>, QString> _memberRanks;
base::flat_map<not_null<UserData*>, TimeId> _adminPromotedSince;
base::flat_map<not_null<PeerData*>, TimeId> _restrictedSince;
base::flat_map<not_null<UserData*>, TimeId> _memberSince;
@@ -178,11 +199,15 @@ public:
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Role role);
~ParticipantsBoxController();
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
void rowElementClicked(
not_null<PeerListRow*> row,
int element) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
@@ -206,8 +231,10 @@ public:
void setStoriesShown(bool shown);
protected:
// Allow child controllers not providing navigation.
// This is their responsibility to override all methods that use it.
using Row = Info::Profile::MemberListRow;
using Type = Row::Type;
using Rights = Row::Rights;
struct CreateTag {
};
ParticipantsBoxController(
@@ -219,10 +246,10 @@ protected:
virtual std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const;
std::unique_ptr<Ui::ChatStyle> _chatStyle;
mutable base::flat_map<QRgb, QImage> _pillCircleCache;
private:
using Row = Info::Profile::MemberListRow;
using Type = Row::Type;
using Rights = Row::Rights;
struct SavedState : SavedStateBase {
explicit SavedState(const ParticipantsAdditionalData &additional);
@@ -260,7 +287,7 @@ private:
void editAdminDone(
not_null<UserData*> user,
ChatAdminRightsInfo rights,
const QString &rank);
const std::optional<QString> &rank);
void showRestricted(not_null<UserData*> user);
void editRestrictedDone(
not_null<PeerData*> participant,
@@ -271,7 +298,6 @@ private:
void removeKickedWithRow(not_null<PeerData*> participant);
void removeKicked(not_null<PeerData*> participant);
void kickParticipant(not_null<PeerData*> participant);
void kickParticipantSure(not_null<PeerData*> participant);
void unkickParticipant(not_null<UserData*> user);
void removeAdmin(not_null<UserData*> user);
void removeAdminSure(not_null<UserData*> user);
@@ -2637,7 +2637,10 @@ void SetupPeerColorSample(
) | rpl::map([=] {
return peer->emojiStatusId();
});
const auto name = peer->shortName();
auto name = peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Name
) | rpl::map([=] { return peer->shortName(); });
const auto sampleSize = st::settingsColorSampleSize;
@@ -2648,7 +2651,7 @@ void SetupPeerColorSample(
style,
rpl::duplicate(colorIndexValue),
rpl::duplicate(colorCollectibleValue),
name);
rpl::duplicate(name));
sample->show();
struct ProfileSampleState {
@@ -2681,13 +2684,15 @@ void SetupPeerColorSample(
rpl::duplicate(label),
rpl::duplicate(colorIndexValue),
rpl::duplicate(colorProfileIndexValue),
rpl::duplicate(emojiStatusIdValue)
rpl::duplicate(emojiStatusIdValue),
rpl::duplicate(name)
) | rpl::on_next([=](
int width,
const QString &buttonText,
int colorIndex,
std::optional<uint8> profileIndex,
EmojiStatusId emojiStatusId) {
EmojiStatusId emojiStatusId,
const QString &name) {
const auto available = width
- st::settingsButton.padding.left()
- (st::settingsColorButton.padding.right() - sampleSize)
@@ -2280,11 +2280,13 @@ void Controller::saveUsernamesOrder() {
_api.request(MTPchannels_DeactivateAllUsernames(
channel->inputChannel()
)).done([=] {
channel->setUsernames(channel->editableUsername().isEmpty()
? Data::Usernames()
: Data::Usernames{
if (channel->editableUsername().isEmpty()) {
channel->setUsernames({});
} else {
channel->setUsernames({
{ channel->editableUsername(), true, true }
});
}
continueSave();
}).send();
} else {
@@ -2740,9 +2742,12 @@ void Controller::saveForwards() {
|| *_savingData.noForwards == _peer->isAyuNoForwards()) {
return continueSave();
}
using Flag = MTPmessages_ToggleNoForwards::Flag;
_api.request(MTPmessages_ToggleNoForwards(
MTP_flags(Flag()),
_peer->input(),
MTP_bool(*_savingData.noForwards)
MTP_bool(*_savingData.noForwards),
MTPint()
)).done([=](const MTPUpdates &result) {
_peer->session().api().applyUpdates(result);
continueSave();
@@ -1522,7 +1522,7 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
comment.text = link;
}
auto &api = session->api();
for (const auto thread : result) {
for (const auto &thread : result) {
auto message = Api::MessageToSend(
Api::SendAction(thread, options));
message.textWithTags = comment;
@@ -96,6 +96,9 @@ constexpr auto kDefaultChargeStars = 10;
{ Flag::AddParticipants, tr::lng_rights_chat_add_members(tr::now) },
{ Flag::CreateTopics, tr::lng_rights_group_add_topics(tr::now) },
{ Flag::PinMessages, tr::lng_rights_group_pin(tr::now) },
{ Flag::EditRank, (options.isUserSpecific
? tr::lng_rights_group_edit_rank_single
: tr::lng_rights_group_edit_rank)(tr::now) },
{ Flag::ChangeInfo, tr::lng_rights_group_info(tr::now) },
};
if (!options.isForum) {
@@ -136,6 +139,7 @@ constexpr auto kDefaultChargeStars = 10;
};
auto second = std::vector<AdminRightLabel>{
{ Flag::ManageCall, tr::lng_rights_group_manage_calls(tr::now) },
{ Flag::ManageRanks, tr::lng_rights_group_manage_ranks(tr::now) },
{ Flag::Anonymous, tr::lng_rights_group_anonymous(tr::now) },
{ Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) },
};
@@ -315,7 +319,8 @@ ChatRestrictions NegateRestrictions(ChatRestrictions value) {
| Flag::SendMusic
| Flag::SendVoiceMessages
| Flag::SendFiles
| Flag::SendOther);
| Flag::SendOther
| Flag::EditRank);
}
auto Dependencies(ChatAdminRights)
@@ -296,6 +296,7 @@ void Controller::createContent() {
(_isGroup
? tr::lng_manage_peer_no_forwards_about
: tr::lng_manage_peer_no_forwards_about_channel)());
}
if (_linkOnly) {
_controls.inviteLinkWrap->show(anim::type::instant);
@@ -0,0 +1,417 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peers/edit_tag_control.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/admin_log/history_admin_log_item.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_message.h"
#include "history/view/media/history_view_media_generic.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/painter.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "window/section_widget.h"
#include "window/themes/window_theme.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace {
constexpr auto kRankLimit = 16;
constexpr auto kTextLinesAlpha = 0.1;
using namespace HistoryView;
class TextLinesPart final : public MediaGenericPart {
public:
TextLinesPart(QMargins margins);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
QMargins _margins;
};
TextLinesPart::TextLinesPart(QMargins margins)
: _margins(margins) {
}
QSize TextLinesPart::countOptimalSize() {
const auto h = _margins.top()
+ 4 * st::tagPreviewLineHeight
+ 3 * st::tagPreviewLineSpacing
+ _margins.bottom();
return { st::msgMinWidth, h };
}
QSize TextLinesPart::countCurrentSize(int newWidth) {
return { newWidth, minHeight() };
}
void TextLinesPart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto &stm = context.messageStyle();
auto color = stm->historyTextFg->c;
color.setAlphaF(color.alphaF() * kTextLinesAlpha);
p.setPen(Qt::NoPen);
p.setBrush(color);
auto hq = PainterHighQualityEnabler(p);
const auto available = outerWidth - _margins.left() - _margins.right();
const auto lineHeight = st::tagPreviewLineHeight;
const auto radius = lineHeight / 2.0;
const auto fractions = { 1.0, 0.85, 0.65, 0.4 };
auto y = double(_margins.top());
for (const auto fraction : fractions) {
const auto w = available * fraction;
p.drawRoundedRect(
QRectF(_margins.left(), y, w, lineHeight),
radius,
radius);
y += lineHeight + st::tagPreviewLineSpacing;
}
}
class TagPreviewDelegate final : public DefaultElementDelegate {
public:
TagPreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update);
void setTagText(const QString &text);
[[nodiscard]] const QString &tagText() const;
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
QString elementAuthorRank(not_null<const Element*> view) override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
QString _tagText;
};
TagPreviewDelegate::TagPreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, std::move(update))) {
}
void TagPreviewDelegate::setTagText(const QString &text) {
_tagText = text;
}
const QString &TagPreviewDelegate::tagText() const {
return _tagText;
}
bool TagPreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto TagPreviewDelegate::elementPathShiftGradient()
-> not_null<Ui::PathShiftGradient*> {
return _pathGradient.get();
}
Context TagPreviewDelegate::elementContext() {
return Context::AdminLog;
}
QString TagPreviewDelegate::elementAuthorRank(
not_null<const Element*> view) {
return _tagText;
}
} // namespace
HistoryView::BadgeRole LookupBadgeRole(
not_null<PeerData*> peer,
not_null<UserData*> user) {
if (const auto channel = peer->asMegagroup()) {
const auto info = channel->mgInfo.get();
if (info && info->creator == user) {
return HistoryView::BadgeRole::Creator;
}
const auto userId = peerToUser(user->id);
if (info && info->admins.contains(userId)) {
return HistoryView::BadgeRole::Admin;
}
} else if (const auto chat = peer->asChat()) {
if (peerToUser(user->id) == chat->creator) {
return HistoryView::BadgeRole::Creator;
} else if (chat->admins.contains(user)) {
return HistoryView::BadgeRole::Admin;
}
}
return HistoryView::BadgeRole::User;
}
class EditTagControl::PreviewWidget final : public Ui::RpWidget {
public:
PreviewWidget(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &initialText,
HistoryView::BadgeRole role);
~PreviewWidget();
void setTagText(const QString &text);
private:
void paintEvent(QPaintEvent *e) override;
void createItem();
void applyBadge(const QString &text);
const not_null<History*> _history;
const not_null<UserData*> _user;
const HistoryView::BadgeRole _role;
const std::unique_ptr<Ui::ChatTheme> _theme;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<TagPreviewDelegate> _delegate;
AdminLog::OwnedItem _item;
int _topSkip = 0;
int _bottomSkip = 0;
Ui::PeerUserpicView _userpic;
};
EditTagControl::PreviewWidget::PreviewWidget(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &initialText,
HistoryView::BadgeRole role)
: RpWidget(parent)
, _history(session->data().history(PeerData::kServiceNotificationsId))
, _user(user)
, _role(role)
, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
, _style(std::make_unique<Ui::ChatStyle>(
session->colorIndicesValue()))
, _delegate(std::make_unique<TagPreviewDelegate>(
this,
_style.get(),
[=] { update(); }))
, _topSkip(st::msgMargin.bottom() * 2)
, _bottomSkip(st::msgMargin.bottom() + st::msgMargin.top()) {
_style->apply(_theme.get());
_delegate->setTagText(initialText);
_history->owner().viewRepaintRequest(
) | rpl::on_next([=](Data::RequestViewRepaint data) {
if (data.view == _item.get()) {
update();
}
}, lifetime());
createItem();
widthValue(
) | rpl::filter([=](int w) {
return w >= st::msgMinWidth;
}) | rpl::on_next([=](int w) {
const auto h = _topSkip
+ _item->resizeGetHeight(w)
+ _bottomSkip;
resize(w, h);
}, lifetime());
_history->owner().itemResizeRequest(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
if (_item && item == _item->data() && width() >= st::msgMinWidth) {
const auto h = _topSkip
+ _item->resizeGetHeight(width())
+ _bottomSkip;
resize(width(), h);
}
}, lifetime());
}
EditTagControl::PreviewWidget::~PreviewWidget() {
_item = {};
}
void EditTagControl::PreviewWidget::createItem() {
const auto item = _history->addNewLocalMessage({
.id = _history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeHistoryItem
| MessageFlag::HasFromId),
.from = _user->id,
.date = base::unixtime::now(),
}, TextWithEntities(), MTP_messageMediaEmpty());
auto owned = AdminLog::OwnedItem(_delegate.get(), item);
owned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>(
owned.get(),
[](not_null<HistoryView::MediaGeneric*>,
Fn<void(std::unique_ptr<HistoryView::MediaGenericPart>)> push) {
push(std::make_unique<TextLinesPart>(st::msgPadding));
}));
_item = std::move(owned);
applyBadge(_delegate->tagText());
if (width() >= st::msgMinWidth) {
const auto h = _topSkip
+ _item->resizeGetHeight(width())
+ _bottomSkip;
resize(width(), h);
}
}
void EditTagControl::PreviewWidget::applyBadge(const QString &text) {
if (!_item) {
return;
}
auto badgeText = text;
if (badgeText.isEmpty()) {
if (_role == HistoryView::BadgeRole::Admin) {
badgeText = tr::lng_admin_badge(tr::now);
} else if (_role == HistoryView::BadgeRole::Creator) {
badgeText = tr::lng_owner_badge(tr::now);
}
}
_item->overrideRightBadge(badgeText, _role);
}
void EditTagControl::PreviewWidget::setTagText(const QString &text) {
_delegate->setTagText(text);
applyBadge(text);
update();
}
void EditTagControl::PreviewWidget::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto clip = e->rect();
if (!clip.isEmpty()) {
p.setClipRect(clip);
Window::SectionWidget::PaintBackground(
p,
_theme.get(),
QSize(width(), window()->height()),
clip);
}
auto context = _theme->preparePaintContext(
_style.get(),
rect(),
rect(),
e->rect(),
!window()->isActiveWindow());
p.translate(0, _topSkip);
_item->draw(p, context);
if (_item->displayFromPhoto()) {
auto userpicBottom = height()
- _bottomSkip
- _item->marginBottom()
- _item->marginTop();
const auto userpicTop = userpicBottom - st::msgPhotoSize;
_user->paintUserpicLeft(
p,
_userpic,
st::historyPhotoLeft,
userpicTop,
width(),
st::msgPhotoSize);
}
}
EditTagControl::EditTagControl(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &currentRank,
HistoryView::BadgeRole role)
: RpWidget(parent)
, _preview(Ui::CreateChild<PreviewWidget>(
this,
session,
user,
TextUtilities::RemoveEmoji(currentRank),
role))
, _field(Ui::CreateChild<Ui::InputField>(
this,
st::customBadgeField,
(role == HistoryView::BadgeRole::Admin
? tr::lng_admin_badge()
: role == HistoryView::BadgeRole::Creator
? tr::lng_owner_badge()
: tr::lng_rights_edit_admin_rank_name()),
TextUtilities::RemoveEmoji(currentRank))) {
_field->setMaxLength(kRankLimit);
_field->setInstantReplaces(Ui::InstantReplaces::TextOnly());
_field->changes(
) | rpl::on_next([=] {
const auto text = _field->getLastText();
const auto removed = TextUtilities::RemoveEmoji(text);
if (removed != text) {
_field->setText(removed);
}
_preview->setTagText(_field->getLastText());
}, _field->lifetime());
widthValue(
) | rpl::on_next([=](int w) {
_preview->resizeToWidth(w);
const auto inputMargins = st::boxRowPadding;
_field->resizeToWidth(w - inputMargins.left() - inputMargins.right());
_field->moveToLeft(
inputMargins.left(),
_preview->height() + st::tagPreviewInputSkip);
resize(w, _field->y() + _field->height());
}, lifetime());
_preview->heightValue(
) | rpl::skip(1) | rpl::on_next([=] {
_field->moveToLeft(
st::boxRowPadding.left(),
_preview->height() + st::tagPreviewInputSkip);
resize(width(), _field->y() + _field->height());
}, lifetime());
}
EditTagControl::~EditTagControl() = default;
QString EditTagControl::currentRank() const {
return TextUtilities::RemoveEmoji(
TextUtilities::SingleLine(_field->getLastText().trimmed()));
}
not_null<Ui::InputField*> EditTagControl::field() const {
return _field;
}
@@ -0,0 +1,50 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
namespace Ui {
class InputField;
} // namespace Ui
namespace Main {
class Session;
} // namespace Main
class PeerData;
class UserData;
namespace HistoryView {
enum class BadgeRole : uchar;
} // namespace HistoryView
[[nodiscard]] HistoryView::BadgeRole LookupBadgeRole(
not_null<PeerData*> peer,
not_null<UserData*> user);
class EditTagControl final : public Ui::RpWidget {
public:
EditTagControl(
QWidget *parent,
not_null<Main::Session*> session,
not_null<UserData*> user,
const QString &currentRank,
HistoryView::BadgeRole role);
~EditTagControl();
[[nodiscard]] QString currentRank() const;
[[nodiscard]] not_null<Ui::InputField*> field() const;
private:
class PreviewWidget;
not_null<PreviewWidget*> _preview;
not_null<Ui::InputField*> _field;
};
@@ -0,0 +1,531 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peers/tag_info_box.h"
#include "boxes/peers/edit_participants_box.h"
#include "boxes/peers/edit_tag_control.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_chat_participant_status.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "history/view/history_view_message.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/text/custom_emoji_helper.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "window/section_widget.h"
#include "window/themes/window_theme.h"
#include "styles/style_boxes.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_widgets.h"
namespace {
using HistoryView::BadgeRole;
constexpr auto kTextLinesAlpha = 0.1;
[[nodiscard]] QColor RoleColor(BadgeRole role) {
return (role == BadgeRole::Creator)
? st::rankOwnerFg->c
: (role == BadgeRole::Admin)
? st::rankAdminFg->c
: st::rankUserFg->c;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeRoundColoredLogo(
not_null<QWidget*> parent,
BadgeRole role) {
const auto &icon = st::tagInfoIcon;
const auto &padding = st::tagInfoIconPadding;
const auto logoSize = icon.size();
const auto logoOuter = logoSize.grownBy(padding);
auto result = object_ptr<Ui::RpWidget>(parent);
const auto logo = result.data();
logo->resize(logo->width(), logoOuter.height());
logo->paintRequest() | rpl::on_next([=, &icon] {
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(RoleColor(role));
p.setPen(Qt::NoPen);
p.drawEllipse(outer);
icon.paintInCenter(p, outer);
}, logo->lifetime());
return result;
}
[[nodiscard]] Ui::Text::PaletteDependentEmoji MakeTagPillEmoji(
const QString &text,
BadgeRole role) {
return { .factory = [=] {
const auto color = RoleColor(role);
const auto &padding = st::msgTagBadgePadding;
auto string = Ui::Text::String(st::defaultTextStyle, text);
const auto textWidth = string.maxWidth();
const auto isUser = (role == BadgeRole::User);
const auto contentWidth = padding.left()
+ textWidth
+ padding.right();
const auto pillHeight = padding.top()
+ st::msgFont->height
+ padding.bottom();
const auto imgWidth = isUser
? textWidth
: std::max(contentWidth, pillHeight);
const auto imgHeight = isUser
? st::msgFont->height
: pillHeight;
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(imgWidth, imgHeight) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
if (!isUser) {
auto bgColor = color;
bgColor.setAlphaF(0.15);
p.setPen(Qt::NoPen);
p.setBrush(bgColor);
p.drawRoundedRect(
0,
0,
imgWidth,
imgHeight,
imgHeight / 2.,
imgHeight / 2.);
}
p.setPen(color);
string.draw(p, {
.position = QPoint(
isUser ? 0 : ((imgWidth - textWidth) / 2),
isUser ? 0 : padding.top()),
.availableWidth = textWidth,
});
p.end();
return result;
}, .margin = st::customEmojiTextBadgeMargin };
}
class TagPreviewsWidget final : public Ui::RpWidget {
public:
TagPreviewsWidget(
QWidget *parent,
not_null<Main::Session*> session,
BadgeRole role);
protected:
void paintEvent(QPaintEvent *e) override;
private:
void paintPreview(
QPainter &p,
QRect rect,
BadgeRole previewRole) const;
void paintBubbleToImage(
QRect rect,
BadgeRole previewRole) const;
void invalidateCache();
const std::unique_ptr<Ui::ChatTheme> _theme;
std::unique_ptr<Ui::ChatStyle> _style;
BadgeRole _role = BadgeRole::User;
mutable QImage _leftCache;
mutable QImage _rightCache;
};
TagPreviewsWidget::TagPreviewsWidget(
QWidget *parent,
not_null<Main::Session*> session,
BadgeRole role)
: RpWidget(parent)
, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
, _style(std::make_unique<Ui::ChatStyle>(
session->colorIndicesValue()))
, _role(role) {
_style->apply(_theme.get());
resize(width(), st::tagInfoPreviewHeight);
_theme->repaintBackgroundRequests(
) | rpl::on_next([=] {
invalidateCache();
update();
}, lifetime());
style::PaletteChanged() | rpl::on_next([=] {
invalidateCache();
update();
}, lifetime());
sizeValue() | rpl::skip(1) | rpl::on_next([=] {
invalidateCache();
}, lifetime());
}
void TagPreviewsWidget::invalidateCache() {
_leftCache = QImage();
_rightCache = QImage();
}
void TagPreviewsWidget::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto gap = st::tagInfoPreviewGap;
const auto previewWidth = (width() - gap) / 2;
if (previewWidth <= 0) {
return;
}
const auto leftRect = QRect(0, 0, previewWidth, height());
const auto rightRect = QRect(
previewWidth + gap,
0,
width() - previewWidth - gap,
height());
paintPreview(p, leftRect, BadgeRole::User);
const auto rightRole = (_role == BadgeRole::User)
? BadgeRole::Admin
: _role;
paintPreview(p, rightRect, rightRole);
}
void TagPreviewsWidget::paintPreview(
QPainter &p,
QRect rect,
BadgeRole previewRole) const {
const auto previewRadius = st::tagInfoPreviewRadius;
p.save();
p.translate(rect.topLeft());
const auto local = QRect(0, 0, rect.width(), rect.height());
auto clipPath = QPainterPath();
clipPath.addRoundedRect(local, previewRadius, previewRadius);
p.setClipPath(clipPath);
Window::SectionWidget::PaintBackground(
p,
_theme.get(),
QSize(rect.width(), window()->height()),
local);
auto &cache = (previewRole == BadgeRole::User)
? _leftCache
: _rightCache;
if (cache.isNull()) {
paintBubbleToImage(rect, previewRole);
}
p.drawImage(0, 0, cache);
p.restore();
}
void TagPreviewsWidget::paintBubbleToImage(
QRect rect,
BadgeRole previewRole) const {
const auto ratio = style::DevicePixelRatio();
auto &cache = (previewRole == BadgeRole::User)
? _leftCache
: _rightCache;
cache = QImage(
rect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
cache.setDevicePixelRatio(ratio);
cache.fill(Qt::transparent);
const auto &stm = _style->messageStyle(false, false);
const auto &padding = st::tagInfoPreviewBubblePadding;
const auto radius = st::tagInfoPreviewBubbleRadius;
const auto rightMargin = st::tagInfoPreviewBubbleRightMargin;
const auto bubbleTop = padding.top();
const auto bubbleHeight = rect.height()
- padding.top()
- padding.bottom();
const auto bubbleRight = rect.width() - rightMargin;
const auto bubbleRect = QRect(
-radius,
bubbleTop,
bubbleRight + radius,
bubbleHeight);
auto p = QPainter(&cache);
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(stm.msgBg);
p.drawRoundedRect(bubbleRect, radius, radius);
}
const auto innerLeft = padding.left();
const auto innerRight = bubbleRight - padding.right();
const auto available = innerRight - innerLeft;
const auto badgeColor = RoleColor(previewRole);
const auto badgeText = (previewRole == BadgeRole::Creator)
? tr::lng_tag_info_preview_owner(tr::now)
: (previewRole == BadgeRole::Admin)
? tr::lng_tag_info_preview_admin(tr::now)
: tr::lng_tag_info_preview_member(tr::now);
auto badgeString = Ui::Text::String(st::defaultTextStyle, badgeText);
const auto badgeTextWidth = badgeString.maxWidth();
const auto &badgePadding = st::msgTagBadgePadding;
const auto badgeContentWidth = badgePadding.left()
+ badgeTextWidth
+ badgePadding.right();
const auto pillHeight = badgePadding.top()
+ st::msgFont->height
+ badgePadding.bottom();
const auto pillWidth = std::max(badgeContentWidth, pillHeight);
const auto badgeRight = innerRight;
const auto badgeLeft = badgeRight - pillWidth;
const auto badgeTop = bubbleTop + st::tagInfoPreviewBadgeTop;
if (previewRole != BadgeRole::User) {
auto bgColor = badgeColor;
bgColor.setAlphaF(0.15);
const auto pillRect = QRect(
badgeLeft,
badgeTop,
pillWidth,
pillHeight);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(bgColor);
p.drawRoundedRect(pillRect, pillHeight / 2., pillHeight / 2.);
p.setPen(badgeColor);
badgeString.draw(p, {
.position = QPoint(
badgeLeft + (pillWidth - badgeTextWidth) / 2,
badgeTop + badgePadding.top()),
.availableWidth = badgeTextWidth,
});
} else if (badgeTextWidth > 0) {
p.setPen(st::rankUserFg);
badgeString.draw(p, {
.position = QPoint(innerRight - badgeTextWidth, badgeTop),
.availableWidth = badgeTextWidth,
});
}
p.setFont(st::msgDateFont);
const auto timeText = u"12:00"_q;
const auto timeWidth = st::msgDateFont->width(timeText);
const auto timeX = innerRight - timeWidth;
const auto timeY = bubbleTop
+ bubbleHeight
- padding.bottom()
+ st::msgDateFont->ascent
- st::msgDateFont->height;
p.setPen(stm.msgDateFg);
p.drawText(timeX, timeY, timeText);
{
auto color = stm.historyTextFg->c;
color.setAlphaF(color.alphaF() * kTextLinesAlpha);
p.setPen(Qt::NoPen);
p.setBrush(color);
auto hq = PainterHighQualityEnabler(p);
const auto lineHeight = st::tagInfoPreviewLineHeight;
const auto lineSpacing = st::tagInfoPreviewLineSpacing;
const auto linesTop = badgeTop
+ pillHeight
+ lineSpacing;
const auto lineRadius = lineHeight / 2.0;
const auto timeAreaLeft = timeX - padding.right();
const auto fractions = { 1.0, 0.65, 0.65 };
auto y = double(linesTop);
auto lineIndex = 0;
for (const auto fraction : fractions) {
auto w = available * fraction;
const auto lineBottom = y + lineHeight;
if (lineIndex >= 1 && lineBottom > (timeY - lineSpacing)) {
w = std::min(w, double(timeAreaLeft - innerLeft));
}
p.drawRoundedRect(
QRectF(innerLeft, y, w, lineHeight),
lineRadius,
lineRadius);
y += lineHeight + lineSpacing;
++lineIndex;
}
}
const auto fadeWidth = st::tagInfoPreviewFadeWidth;
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
auto gradient = QLinearGradient(0, 0, fadeWidth, 0);
gradient.setStops({
{ 0., QColor(255, 255, 255, 0) },
{ 1., QColor(255, 255, 255, 255) },
});
p.fillRect(0, 0, fadeWidth, rect.height(), gradient);
p.end();
}
[[nodiscard]] QString LookupCurrentRank(not_null<PeerData*> peer) {
const auto selfId = peerToUser(peer->session().user()->id);
if (const auto channel = peer->asMegagroup()) {
if (const auto info = channel->mgInfo.get()) {
const auto it = info->memberRanks.find(selfId);
if (it != info->memberRanks.end()) {
return it->second;
}
}
} else if (const auto chat = peer->asChat()) {
const auto it = chat->memberRanks.find(selfId);
if (it != chat->memberRanks.end()) {
return it->second;
}
}
return QString();
}
} // namespace
void TagInfoBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<PeerData*> author,
const QString &tagText,
HistoryView::BadgeRole role) {
box->setStyle(st::confcallJoinBox);
box->setWidth(st::boxWideWidth);
box->setNoContentMargin(true);
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
box->addRow(
MakeRoundColoredLogo(box, role),
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
auto title = (role == BadgeRole::Creator)
? tr::lng_tag_info_title_owner()
: (role == BadgeRole::Admin)
? tr::lng_tag_info_title_admin()
: tr::lng_tag_info_title_user();
box->addRow(
object_ptr<Ui::FlatLabel>(box, std::move(title), st::boxTitle),
st::boxRowPadding + st::confcallLinkTitlePadding,
style::al_top);
auto helper = Ui::Text::CustomEmojiHelper();
const auto tagPill = helper.paletteDependent(
MakeTagPillEmoji(tagText, role));
const auto authorName = author->shortName();
const auto groupName = peer->name();
const auto descText = (role == BadgeRole::Creator)
? tr::lng_tag_info_text_owner(
tr::now,
lt_emoji,
tagPill,
lt_author,
tr::bold(authorName),
lt_group,
tr::bold(groupName),
tr::rich)
: (role == BadgeRole::Admin)
? tr::lng_tag_info_text_admin(
tr::now,
lt_emoji,
tagPill,
lt_author,
tr::bold(authorName),
lt_group,
tr::bold(groupName),
tr::rich)
: tr::lng_tag_info_text_user(
tr::now,
lt_emoji,
tagPill,
lt_author,
tr::bold(authorName),
lt_group,
tr::bold(groupName),
tr::rich);
const auto context = helper.context();
const auto desc = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
rpl::single(descText),
st::confcallLinkCenteredText,
st::defaultPopupMenu,
context),
st::boxRowPadding,
style::al_top);
desc->setTryMakeSimilarLines(true);
box->addRow(
object_ptr<TagPreviewsWidget>(
box,
&peer->session(),
role),
st::boxRowPadding + st::tagInfoPreviewPadding);
const auto selfUser = peer->session().user();
const auto selfRole = LookupBadgeRole(peer, selfUser);
const auto isAdmin = (selfRole != BadgeRole::User);
const auto canEditSelf = isAdmin
|| !peer->amRestricted(ChatRestriction::EditRank);
if (canEditSelf) {
const auto currentRank = LookupCurrentRank(peer);
auto buttonText = currentRank.isEmpty()
? tr::lng_tag_info_add_my_tag()
: tr::lng_tag_info_edit_my_tag();
box->addButton(std::move(buttonText), [=] {
box->closeBox();
show->show(Box(
EditCustomRankBox,
show,
peer,
selfUser,
currentRank,
true,
Fn<void(QString)>(nullptr)));
});
} else {
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_tag_info_admins_only(),
st::tagInfoAdminsOnlyLabel),
st::boxRowPadding + st::tagInfoAdminsOnlyPadding,
style::al_top);
box->addButton(
rpl::single(QString()),
[=] { box->closeBox(); }
)->setText(rpl::single(Ui::Text::IconEmoji(
&st::infoStarsUnderstood
).append(' ').append(
tr::lng_stars_rating_understood(tr::now))));
}
}
@@ -0,0 +1,27 @@
/*
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
namespace Ui {
class GenericBox;
class Show;
} // namespace Ui
namespace HistoryView {
enum class BadgeRole : uchar;
} // namespace HistoryView
class PeerData;
void TagInfoBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
not_null<PeerData*> author,
const QString &tagText,
HistoryView::BadgeRole role);
@@ -144,6 +144,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_subtitle_peer_colors();
case PremiumFeature::Gifts:
return tr::lng_premium_summary_subtitle_gifts();
case PremiumFeature::NoForwards:
return tr::lng_premium_summary_subtitle_no_forwards();
case PremiumFeature::BusinessLocation:
return tr::lng_business_subtitle_location();
@@ -215,6 +217,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_about_peer_colors();
case PremiumFeature::Gifts:
return tr::lng_premium_summary_about_gifts();
case PremiumFeature::NoForwards:
return tr::lng_premium_summary_about_no_forwards();
case PremiumFeature::BusinessLocation:
return tr::lng_business_about_location();
@@ -558,6 +562,7 @@ struct VideoPreviewDocument {
case PremiumFeature::TodoLists: return "todo";
case PremiumFeature::PeerColors: return "peer_colors";
case PremiumFeature::Gifts: return "gifts";
case PremiumFeature::NoForwards: return "no_forwards";
case PremiumFeature::BusinessLocation: return "business_location";
case PremiumFeature::BusinessHours: return "business_hours";
@@ -75,6 +75,7 @@ enum class PremiumFeature {
TodoLists,
PeerColors,
Gifts,
NoForwards,
// Business features.
BusinessLocation,
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h"
#include "core/core_cloud_password.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "dialogs/ui/chat_search_empty.h"
#include "boxes/peers/channel_ownership_transfer.h"
#include "data/data_session.h"
@@ -44,58 +45,25 @@ enum class ParticipantType {
Members
};
class ParticipantsController : public PeerListController {
class FutureOwnerController : public PeerListController {
public:
ParticipantsController(
not_null<Window::SessionController*> window,
not_null<ChannelData*> channel,
ParticipantType type);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
void itemDeselectedHook(not_null<PeerData*> peer) override;
void setOnRowClicked(Fn<void()> callback);
rpl::producer<> itemDeselected() const;
private:
const not_null<Window::SessionController*> _window;
const not_null<ChannelData*> _channel;
const ParticipantType _type;
MTP::Sender _api;
Fn<void()> _onRowClicked;
rpl::event_stream<> _itemDeselected;
mtpRequestId _loadRequestId = 0;
int _offset = 0;
bool _allLoaded = false;
};
ParticipantsController::ParticipantsController(
not_null<Window::SessionController*> window,
not_null<ChannelData*> channel,
ParticipantType type)
: _window(window)
, _channel(channel)
, _type(type)
, _api(&channel->session().mtp()) {
}
Main::Session &ParticipantsController::session() const {
return _channel->session();
}
void ParticipantsController::setOnRowClicked(Fn<void()> callback) {
void FutureOwnerController::setOnRowClicked(Fn<void()> callback) {
_onRowClicked = callback;
}
void ParticipantsController::prepare() {
loadMoreRows();
}
void ParticipantsController::rowClicked(not_null<PeerListRow*> row) {
void FutureOwnerController::rowClicked(not_null<PeerListRow*> row) {
delegate()->peerListSetRowChecked(
row,
!delegate()->peerListIsRowChecked(row));
@@ -110,14 +78,51 @@ void ParticipantsController::rowClicked(not_null<PeerListRow*> row) {
}
}
void ParticipantsController::itemDeselectedHook(not_null<PeerData*> peer) {
void FutureOwnerController::itemDeselectedHook(not_null<PeerData*> peer) {
_itemDeselected.fire({});
}
rpl::producer<> ParticipantsController::itemDeselected() const {
rpl::producer<> FutureOwnerController::itemDeselected() const {
return _itemDeselected.events();
}
class ParticipantsController : public FutureOwnerController {
public:
ParticipantsController(
not_null<ChannelData*> channel,
ParticipantType type);
Main::Session &session() const override;
void prepare() override;
void loadMoreRows() override;
private:
const not_null<ChannelData*> _channel;
const ParticipantType _type;
MTP::Sender _api;
mtpRequestId _loadRequestId = 0;
int _offset = 0;
bool _allLoaded = false;
};
ParticipantsController::ParticipantsController(
not_null<ChannelData*> channel,
ParticipantType type)
: _channel(channel)
, _type(type)
, _api(&channel->session().mtp()) {
}
Main::Session &ParticipantsController::session() const {
return _channel->session();
}
void ParticipantsController::prepare() {
loadMoreRows();
}
void ParticipantsController::loadMoreRows() {
if (_loadRequestId || _allLoaded) {
return;
@@ -191,13 +196,71 @@ void ParticipantsController::loadMoreRows() {
}).send();
}
class LegacyParticipantsController : public FutureOwnerController {
public:
LegacyParticipantsController(
not_null<ChatData*> chat,
ParticipantType type);
Main::Session &session() const override;
void prepare() override;
void loadMoreRows() override;
private:
const not_null<ChatData*> _chat;
const ParticipantType _type;
};
LegacyParticipantsController::LegacyParticipantsController(
not_null<ChatData*> chat,
ParticipantType type)
: _chat(chat)
, _type(type) {
}
Main::Session &LegacyParticipantsController::session() const {
return _chat->session();
}
void LegacyParticipantsController::prepare() {
if (_chat->noParticipantInfo()) {
_chat->updateFullForced();
}
const auto &source = (_type == ParticipantType::Admins)
? _chat->admins
: _chat->participants;
for (const auto &user : source) {
if (user->isBot()) {
continue;
}
if (user->id == peerFromUser(_chat->creator)) {
continue;
}
if (_type == ParticipantType::Members
&& _chat->admins.contains(user)) {
continue;
}
delegate()->peerListAppendRow(
std::make_unique<PeerListRow>(user));
}
delegate()->peerListRefreshRows();
}
void LegacyParticipantsController::loadMoreRows() {
}
} // namespace
void SelectFutureOwnerbox(
not_null<Ui::GenericBox*> box,
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
not_null<UserData*> user) {
const auto content = box->verticalLayout();
const auto channel = peer->asChannel();
const auto chat = peer->asChat();
const auto isGroup = peer->isMegagroup() || peer->isChat();
const auto isLegacy = (chat != nullptr);
Ui::AddSkip(content);
Ui::AddSkip(content);
content->add(
@@ -205,7 +268,7 @@ void SelectFutureOwnerbox(
content,
rpl::single(std::vector<not_null<PeerData*>>{
user->session().user(),
channel,
peer,
}),
user,
UserpicsTransferType::ChannelFutureOwner),
@@ -215,24 +278,36 @@ void SelectFutureOwnerbox(
content->add(
object_ptr<Ui::FlatLabel>(
content,
channel->isMegagroup()
isGroup
? tr::lng_leave_next_owner_box_title_group()
: tr::lng_leave_next_owner_box_title(),
box->getDelegate()->style().title),
st::boxRowPadding);
Ui::AddSkip(content);
Ui::AddSkip(content);
const auto adminsAreEqual = (channel->adminsCount() <= 1);
const auto adminsCount = [&] {
if (channel) {
return channel->adminsCount();
} else if (chat) {
return int(chat->admins.size()) + 1;
}
return 0;
}();
const auto adminsAreEqual = (adminsCount <= 1);
content->add(
object_ptr<Ui::FlatLabel>(
content,
(adminsAreEqual
? tr::lng_leave_next_owner_box_about
: tr::lng_leave_next_owner_box_about_admin)(
(isLegacy
? (adminsAreEqual
? tr::lng_leave_next_owner_box_about_legacy
: tr::lng_leave_next_owner_box_about_admin_legacy)
: (adminsAreEqual
? tr::lng_leave_next_owner_box_about
: tr::lng_leave_next_owner_box_about_admin))(
lt_user,
Info::Profile::NameValue(user) | rpl::map(tr::marked),
lt_chat,
Info::Profile::NameValue(channel) | rpl::map(tr::marked),
Info::Profile::NameValue(peer) | rpl::map(tr::marked),
tr::rich),
st::boxLabel),
st::boxRowPadding);
@@ -264,14 +339,14 @@ void SelectFutureOwnerbox(
const auto leave = content->add(
object_ptr<Ui::RoundButton>(
content,
channel->isMegagroup()
isGroup
? tr::lng_profile_leave_group()
: tr::lng_profile_leave_channel(),
st::attentionBoxButton),
st::boxRowPadding,
style::al_justify);
leave->setClickedCallback([=, revoke = false] {
channel->session().api().deleteConversation(channel, revoke);
peer->session().api().deleteConversation(peer, revoke);
box->closeBox();
});
select->setClickedCallback([=] {
@@ -283,16 +358,31 @@ void SelectFutureOwnerbox(
return;
}
auto adminsOwned = std::make_unique<ParticipantsController>(
sessionController,
channel,
ParticipantType::Admins);
auto membersOwned = std::make_unique<ParticipantsController>(
sessionController,
channel,
ParticipantType::Members);
using Pair = std::pair<
std::unique_ptr<FutureOwnerController>,
std::unique_ptr<FutureOwnerController>>;
auto makeControllers = [&]() -> Pair {
if (channel) {
return {
std::make_unique<ParticipantsController>(
channel,
ParticipantType::Admins),
std::make_unique<ParticipantsController>(
channel,
ParticipantType::Members),
};
} else {
return {
std::make_unique<LegacyParticipantsController>(
chat,
ParticipantType::Admins),
std::make_unique<LegacyParticipantsController>(
chat,
ParticipantType::Members),
};
}
};
auto [adminsOwned, membersOwned] = makeControllers();
const auto admins = adminsOwned.get();
const auto members = membersOwned.get();
@@ -331,14 +421,14 @@ void SelectFutureOwnerbox(
0,
CreatePeerListSectionSubtitle(
selectBox,
!channel->isMegagroup()
!isGroup
? tr::lng_select_next_owner_box_sub_admins()
: tr::lng_select_next_owner_box_sub_admins_group()));
const auto separatorMembers = selectBox->addSeparatorBefore(
1,
CreatePeerListSectionSubtitle(
selectBox,
!channel->isMegagroup()
!isGroup
? tr::lng_select_next_owner_box_sub_members()
: tr::lng_select_next_owner_box_sub_members_group()));
rpl::combine(
@@ -407,13 +497,13 @@ void SelectFutureOwnerbox(
if (const auto user = selected.front()->asUser()) {
auto &lifetime = selectBox->lifetime();
lifetime.make_state<ChannelOwnershipTransfer>(
channel,
peer,
user,
selectBox->uiShow(),
[=](std::shared_ptr<Ui::Show> show) {
const auto revoke = false;
channel->session().api().deleteConversation(
channel,
peer->session().api().deleteConversation(
peer,
revoke);
show->hideLayer();
})->start();
@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class ChannelData;
class PeerData;
class UserData;
@@ -18,5 +17,5 @@ class Show;
void SelectFutureOwnerbox(
not_null<Ui::GenericBox*> box,
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
not_null<UserData*> user);
+133 -9
View File
@@ -62,6 +62,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include "styles/style_menu_icons.h"
#include <QtCore/QMimeData>
@@ -78,6 +80,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kMaxMessageLength = 4096;
constexpr auto kMaxDisplayNameLength = 64;
constexpr auto kDragMime = "application/x-tg-sendfile-index";
@@ -117,14 +120,62 @@ void FileDialogCallback(
rpl::producer<QString> FieldPlaceholder(
const Ui::PreparedList &list,
SendFilesWay way) {
return list.canAddCaption(
way.groupFiles() && way.sendImagesAsPhotos(),
way.sendImagesAsPhotos())
SendFilesWay way,
bool slowmode) {
return Ui::CaptionWillBeAttached(list, way, slowmode)
? tr::lng_photo_caption()
: tr::lng_photos_comment();
}
void RenameFileBox(
not_null<Ui::GenericBox*> box,
const QString &currentName,
Fn<void(QString)> apply) {
box->setTitle(tr::lng_rename_file());
const auto field = box->addRow(object_ptr<Ui::InputField>(
box,
st::settingsDeviceName,
rpl::single(QString()),
currentName));
const auto extension = [&] {
const auto dot = currentName.lastIndexOf('.');
return (dot >= 0) ? currentName.mid(dot) : QString();
}();
const auto nameWithoutExt = extension.isEmpty()
? currentName
: currentName.left(currentName.size() - extension.size());
const auto maxNameLength = kMaxDisplayNameLength - extension.size();
field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0);
field->setText(nameWithoutExt);
field->selectAll();
box->setFocusCallback([=] {
field->setFocusFast();
});
const auto save = [=] {
const auto newName = field->getLastText().trimmed();
if (newName.isEmpty()) {
field->showError();
return;
}
if ((newName.size() + extension.size()) > kMaxDisplayNameLength) {
field->showError();
return;
}
const auto weak = base::make_weak(box);
apply(newName + extension);
if (const auto strong = weak.get()) {
strong->closeBox();
}
};
field->submits() | rpl::on_next([=] {
save();
}, box->lifetime());
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
void EditPriceBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
@@ -481,6 +532,16 @@ QImage SendFilesBox::Block::generatePriceTagBackground() const {
return QImage();
}
bool SendFilesBox::Block::setSingleFileDisplayName(
const QString &displayName) {
if (_isAlbum || _isSingleMedia) {
return false;
}
const auto single = static_cast<Ui::SingleFilePreview*>(_preview.get());
single->setDisplayName(displayName);
return true;
}
SendFilesBox::SendFilesBox(
QWidget*,
not_null<Window::SessionController*> controller,
@@ -708,6 +769,18 @@ void SendFilesBox::refreshAllAfterChanges(int fromItem, Fn<void()> perform) {
captionResized();
}
bool SendFilesBox::setDisplayNameInSingleFilePreview(
int fileIndex,
const QString &displayName) {
for (auto &block : _blocks) {
if (fileIndex < block.fromIndex() || fileIndex >= block.tillIndex()) {
continue;
}
return block.setSingleFileDisplayName(displayName);
}
return false;
}
void SendFilesBox::openDialogToAddFileToAlbum() {
const auto show = uiShow();
const auto checkResult = [=](const Ui::PreparedList &list) {
@@ -738,9 +811,9 @@ void SendFilesBox::openDialogToAddFileToAlbum() {
void SendFilesBox::refreshMessagesCount() {
const auto way = _sendWay.current();
const auto withCaption = _list.canAddCaption(
way.groupFiles() && way.sendImagesAsPhotos(),
way.sendImagesAsPhotos());
const auto slowmode = (_limits & SendFilesAllow::OnlyOne)
&& (_list.files.size() > 1);
const auto withCaption = Ui::CaptionWillBeAttached(_list, way, slowmode);
const auto withComment = !withCaption
&& _caption
&& !_caption->isHidden()
@@ -774,7 +847,10 @@ void SendFilesBox::refreshButtons() {
&_st.tabbed.menu,
&_st.tabbed.icons);
}
addButton(tr::lng_cancel(), [=] { closeBox(); });
addButton(tr::lng_cancel(), [=] {
requestToTakeTextWithTags();
closeBox();
});
_addFile = addLeftButton(
tr::lng_stickers_featured_add(),
base::fn_delayed(st::historyAttach.ripple.hideDuration, this, [=] {
@@ -1054,7 +1130,9 @@ void SendFilesBox::updateCaptionPlaceholder() {
_emojiToggle->hide();
}
} else {
_caption->setPlaceholder(FieldPlaceholder(_list, way));
const auto slowmode = (_limits & SendFilesAllow::OnlyOne)
&& (_list.files.size() > 1);
_caption->setPlaceholder(FieldPlaceholder(_list, way, slowmode));
_caption->show();
if (_emojiToggle) {
_emojiToggle->show();
@@ -1150,6 +1228,7 @@ void SendFilesBox::pushBlock(int from, int till) {
}
// Just close the box if it is the only one.
if (_list.files.size() == 1) {
requestToTakeTextWithTags();
closeBox();
return;
}
@@ -1332,6 +1411,40 @@ void SendFilesBox::pushBlock(int from, int till) {
_priceTag->update();
}
}, widget->lifetime());
struct State {
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto state = widget->lifetime().make_state<State>();
base::install_event_filter(widget, [=, from = from, till = till](
not_null<QEvent*> e) {
if (e->type() == QEvent::ContextMenu) {
const auto mouse = static_cast<QContextMenuEvent*>(e.get());
if (from >= till || from >= _list.files.size()) {
return base::EventFilterResult::Continue;
}
const auto fileIndex = from;
state->menu = base::make_unique_q<Ui::PopupMenu>(
widget,
_st.tabbed.menu);
state->menu->addAction(tr::lng_rename_file(tr::now), [=] {
auto &file = _list.files[fileIndex];
_show->show(Box(RenameFileBox, file.displayName, [=](
QString newName) {
const auto displayName = std::move(newName);
_list.files[fileIndex].displayName = displayName;
if (!setDisplayNameInSingleFilePreview(
fileIndex,
displayName)) {
refreshAllAfterChanges(from);
}
}));
}, &st::menuIconEdit);
state->menu->popup(mouse->globalPos());
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
}, widget->lifetime());
}
void SendFilesBox::refreshControls(bool initial) {
@@ -1509,6 +1622,7 @@ void SendFilesBox::setupCaption() {
}, _caption->lifetime());
_caption->cancelled(
) | rpl::on_next([=] {
requestToTakeTextWithTags();
closeBox();
}, _caption->lifetime());
_caption->setMimeDataHook([=](
@@ -1909,6 +2023,16 @@ void SendFilesBox::showFinished() {
}
}
rpl::producer<TextWithTags> SendFilesBox::takeTextWithTagsRequests() const {
return _textWithTagsRequests.events();
}
void SendFilesBox::requestToTakeTextWithTags() const {
if (_caption && !_caption->isHidden()) {
_textWithTagsRequests.fire_copy(_caption->getTextWithTags());
}
}
void SendFilesBox::setInnerFocus() {
if (_caption && !_caption->isHidden()) {
_caption->setFocusFast();
+12 -1
View File
@@ -129,6 +129,8 @@ public:
_cancelledCallback = std::move(callback);
}
[[nodiscard]] rpl::producer<TextWithTags> takeTextWithTagsRequests() const;
void showFinished() override;
~SendFilesBox();
@@ -177,6 +179,8 @@ private:
void applyChanges();
[[nodiscard]] QImage generatePriceTagBackground() const;
[[nodiscard]] bool setSingleFileDisplayName(
const QString &displayName);
private:
base::unique_qptr<Ui::RpWidget> _preview;
@@ -243,6 +247,9 @@ private:
void openDialogToAddFileToAlbum();
void refreshAllAfterChanges(int fromItem, Fn<void()> perform = nullptr);
[[nodiscard]] bool setDisplayNameInSingleFilePreview(
int fileIndex,
const QString &displayName);
void enqueueNextPrepare();
void addPreparedAsyncFile(Ui::PreparedFile &&file);
@@ -250,6 +257,8 @@ private:
void checkCharsLimitation();
void refreshMessagesCount();
void requestToTakeTextWithTags() const;
[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor);
[[nodiscard]] auto prepareSendMenuCallback()
@@ -301,7 +310,7 @@ private:
object_ptr<Ui::ScrollArea> _scroll;
QPointer<Ui::VerticalLayout> _inner;
std::vector<Block> _blocks;
std::deque<Block> _blocks;
Fn<void()> _whenReadySend;
bool _preparing = false;
@@ -310,6 +319,8 @@ private:
QPointer<Ui::RoundButton> _send;
QPointer<Ui::RoundButton> _addFile;
rpl::event_stream<TextWithTags> _textWithTagsRequests;
// AyuGram files reordering
[[nodiscard]] bool isFileBlock(int i) const;
+169 -111
View File
@@ -1385,24 +1385,26 @@ void ShareBox::Inner::changeCheckState(Chat *chat) {
void ShareBox::Inner::chooseForumTopic(not_null<Data::Forum*> forum) {
const auto guard = base::make_weak(this);
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
auto chosen = [=](not_null<Data::ForumTopic*> topic) {
auto chosen = [=](not_null<Data::Thread*> thread) {
if (const auto strong = *weak) {
strong->closeBox();
}
if (!guard) {
return;
}
const auto row = _chatsIndexed->getRow(topic->owningHistory());
const auto row = _chatsIndexed->getRow(thread->owningHistory());
if (!row) {
return;
}
const auto chat = getChat(row);
Assert(!chat->topic);
chat->topic = topic;
chat->topic->destroyed(
) | rpl::on_next([=] {
changePeerCheckState(chat, false);
}, chat->topicLifetime);
if (const auto topic = thread->asTopic()) {
Assert(!chat->topic);
chat->topic = topic;
chat->topic->destroyed(
) | rpl::on_next([=] {
changePeerCheckState(chat, false);
}, chat->topicLifetime);
}
updateChatName(chat);
changePeerCheckState(chat, true);
};
@@ -1416,8 +1418,8 @@ void ShareBox::Inner::chooseForumTopic(not_null<Data::Forum*> forum) {
box->closeBox();
}, box->lifetime());
};
auto filter = [=](not_null<Data::ForumTopic*> topic) {
return guard && _descriptor.filterCallback(topic);
auto filter = [=](not_null<Data::Thread*> thread) {
return guard && _descriptor.filterCallback(thread);
};
auto box = Box<PeerListBox>(
std::make_unique<ChooseTopicBoxController>(
@@ -1687,6 +1689,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
std::optional<TimeId> videoTimestamp) {
struct State final {
base::flat_set<mtpRequestId> requests;
mtpRequestId nextRequestKey = 0;
};
const auto state = std::make_shared<State>();
return [=](
@@ -1736,13 +1739,6 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
for (const auto &fullId : existingIds) {
mtpMsgIds.push_back(MTP_int(fullId.msg));
}
const auto generateRandom = [&] {
auto result = QVector<MTPlong>(existingIds.size());
for (auto &value : result) {
value = base::RandomValue<MTPlong>();
}
return result;
};
auto &api = history->session().api();
auto &histories = history->owner().histories();
const auto donePhraseArgs = CreateForwardedMessagePhraseArgs(
@@ -1751,8 +1747,6 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
const auto showRecentForwardsToSelf = result.size() == 1
&& result.front()->peer()->isSelf()
&& history->session().premium();
const auto requestType = Data::Histories::RequestType::Send;
// AyuGram-changed
const auto dismiss = [=]
@@ -1762,7 +1756,6 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
}
};
if (AyuForward::isFullAyuForwardNeeded(items.front())) {
crl::async([=]{
for (const auto thread : result) {
@@ -1776,8 +1769,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
dismiss();
return;
}
if (AyuForward::isAyuForwardNeeded(items)) {
} else if (AyuForward::isAyuForwardNeeded(items)) {
crl::async([=]
{
for (const auto thread : result) {
@@ -1793,110 +1785,176 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
}
// AyuGram-changed
for (const auto &thread : result) {
const auto peer = thread->peer();
const auto threadHistory = thread->owningHistory();
const auto forum = threadHistory->asForum();
const auto needNewTopic = forum
&& forum->bot()
&& Data::IsBotUserCreatesTopics(peer)
&& !thread->asTopic();
const auto effectiveThread = [&]() -> not_null<Data::Thread*> {
if (needNewTopic) {
const auto topic = forum->reserveNewBotTopic();
Assert(topic != nullptr);
return topic;
}
return thread;
}();
for (const auto thread : result) {
if (!comment.text.isEmpty()) {
auto message = Api::MessageToSend(
Api::SendAction(thread, options));
Api::SendAction(effectiveThread, options));
message.textWithTags = comment;
message.action.clearDraft = false;
api.sendMessage(std::move(message));
}
const auto topicRootId = thread->topicRootId();
const auto sublistPeer = thread->maybeSublistPeer();
const auto kGeneralId = Data::ForumTopic::kGeneralId;
const auto topMsgId = (topicRootId == kGeneralId)
? MsgId(0)
: topicRootId;
const auto peer = thread->peer();
const auto threadHistory = thread->owningHistory();
const auto topicRootId = effectiveThread->topicRootId();
const auto sublistPeer = needNewTopic
? nullptr
: thread->maybeSublistPeer();
const auto fromPeer = history->peer;
const auto msgCount = int(existingIds.size());
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
options.starsApproved);
if (starsPaid) {
options.starsApproved -= starsPaid;
}
histories.sendRequest(threadHistory, requestType, [=](
Fn<void()> finish) {
const auto session = &threadHistory->session();
auto &api = session->api();
const auto sendFlags = commonSendFlags
| (topMsgId ? Flag::f_top_msg_id : Flag(0))
| (ShouldSendSilent(peer, options)
? Flag::f_silent
: Flag(0))
| (options.shortcutId
? Flag::f_quick_reply_shortcut
: Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag())
| (sublistPeer ? Flag::f_reply_to : Flag())
| (options.suggest ? Flag::f_suggested_post : Flag())
| (options.effectId ? Flag::f_effect : Flag());
threadHistory->sendRequestId = api.request(
MTPmessages_ForwardMessages(
MTP_flags(sendFlags),
history->peer->input(),
MTP_vector<MTPint>(mtpMsgIds),
MTP_vector<MTPlong>(generateRandom()),
peer->input(),
MTP_int(topMsgId),
(sublistPeer
? MTP_inputReplyToMonoForum(sublistPeer->input())
: MTPInputReplyTo()),
MTP_int(options.scheduled),
MTP_int(options.scheduleRepeatPeriod),
MTP_inputPeerEmpty(), // send_as
Data::ShortcutIdToMTP(session, options.shortcutId),
MTP_long(options.effectId),
MTP_int(videoTimestamp.value_or(0)),
MTP_long(starsPaid),
Api::SuggestToMTP(options.suggest)
)).done([=](const MTPUpdates &updates, mtpRequestId reqId) {
threadHistory->session().api().applyUpdates(updates);
if (showRecentForwardsToSelf) {
ApiWrap::ProcessRecentSelfForwards(
&threadHistory->session(),
updates,
peer->id,
history->peer->id);
}
state->requests.remove(reqId);
if (state->requests.empty()) {
if (show->valid()) {
auto phrase = rpl::variable<TextWithEntities>(
ChatHelpers::ForwardedMessagePhrase(
donePhraseArgs)).current();
if (!phrase.empty()) {
show->showToast(std::move(phrase));
}
show->hideLayer();
const auto sendFlags = commonSendFlags
| (ShouldSendSilent(peer, options)
? Flag::f_silent
: Flag(0))
| (options.shortcutId
? Flag::f_quick_reply_shortcut
: Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag())
| (sublistPeer ? Flag::f_reply_to : Flag())
| (options.suggest ? Flag::f_suggested_post : Flag())
| (options.effectId ? Flag::f_effect : Flag());
auto buildMessage = [=](
not_null<History*> history,
FullReplyTo replyTo)
-> Data::Histories::PreparedMessage {
const auto kGeneralId
= Data::ForumTopic::kGeneralId;
const auto realTopMsgId
= (replyTo.topicRootId == kGeneralId)
? MsgId(0)
: replyTo.topicRootId;
auto flags = sendFlags;
if (realTopMsgId) {
flags |= Flag::f_top_msg_id;
} else {
flags &= ~Flag::f_top_msg_id;
}
auto randoms = QVector<MTPlong>(msgCount);
for (auto &value : randoms) {
value = base::RandomValue<MTPlong>();
}
return MTPmessages_ForwardMessages(
MTP_flags(flags),
fromPeer->input(),
MTP_vector<MTPint>(mtpMsgIds),
MTP_vector<MTPlong>(randoms),
history->peer->input(),
MTP_int(realTopMsgId),
(sublistPeer
? MTP_inputReplyToMonoForum(
sublistPeer->input())
: MTPInputReplyTo()),
MTP_int(options.scheduled),
MTP_int(options.scheduleRepeatPeriod),
MTP_inputPeerEmpty(),
Data::ShortcutIdToMTP(
&history->session(),
options.shortcutId),
MTP_long(options.effectId),
MTP_int(videoTimestamp.value_or(0)),
MTP_long(starsPaid),
Api::SuggestToMTP(options.suggest));
};
const auto requestDone = [=](
const MTPUpdates &updates,
mtpRequestId requestKey) {
const auto &ghost = AyuSettings::ghost(&history->owner().session());
if (!ghost.sendReadMessages() && ghost.markReadAfterAction() && history->lastMessage())
{
readHistory(history->lastMessage());
}
if (showRecentForwardsToSelf) {
ApiWrap::ProcessRecentSelfForwards(
&threadHistory->session(),
updates,
peer->id,
history->peer->id);
}
state->requests.remove(requestKey);
if (state->requests.empty()) {
if (show->valid()) {
auto phrase = rpl::variable<
TextWithEntities>(
ChatHelpers::ForwardedMessagePhrase(
donePhraseArgs)).current();
if (!phrase.empty()) {
show->showToast(std::move(phrase));
}
show->hideLayer();
}
const auto &ghost = AyuSettings::ghost(&history->owner().session());
if (!ghost.sendReadMessages() && ghost.markReadAfterAction() && history->lastMessage())
{
readHistory(history->lastMessage());
}
};
const auto requestFail = [=](
const MTP::Error &error,
mtpRequestId requestKey) {
const auto type = error.type();
if (type.startsWith(
u"ALLOW_PAYMENT_REQUIRED_"_q)) {
show->showToast(
u"Payment requirements changed. "
"Please, try again."_q);
} else if (type
== u"VOICE_MESSAGES_FORBIDDEN"_q) {
show->showToast(
tr::lng_restricted_send_voice_messages(
tr::now,
lt_user,
peer->name()));
}
state->requests.remove(requestKey);
if (state->requests.empty()) {
if (show->valid()) {
show->hideLayer();
}
finish();
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (type.startsWith(u"ALLOW_PAYMENT_REQUIRED_"_q)) {
show->showToast(u"Payment requirements changed. "
"Please, try again."_q);
} else if (type == u"VOICE_MESSAGES_FORBIDDEN"_q) {
show->showToast(
tr::lng_restricted_send_voice_messages(
tr::now,
lt_user,
peer->name()));
}
finish();
}).afterRequest(threadHistory->sendRequestId).send();
return threadHistory->sendRequestId;
});
state->requests.insert(threadHistory->sendRequestId);
}
};
const auto requestKey = ++state->nextRequestKey;
state->requests.insert(requestKey);
histories.sendPreparedMessage(
threadHistory,
FullReplyTo{ .topicRootId = topicRootId },
uint64(0),
std::move(buildMessage),
[=](const MTPUpdates &updates,
const MTP::Response &) {
requestDone(updates, requestKey);
},
[=](const MTP::Error &error,
const MTP::Response &) {
requestFail(error, requestKey);
});
}
if (state->requests.empty()) {
if (show->valid()) {
auto phrase = rpl::variable<TextWithEntities>(
ChatHelpers::ForwardedMessagePhrase(
donePhraseArgs)).current();
if (!phrase.empty()) {
show->showToast(std::move(phrase));
}
show->hideLayer();
}
}
};
}
@@ -2100,7 +2158,7 @@ void FastShareLink(
comment.text = url;
}
auto &api = show->session().api();
for (const auto thread : result) {
for (const auto &thread : result) {
auto message = Api::MessageToSend(
Api::SendAction(thread, options));
message.textWithTags = comment;
@@ -1823,7 +1823,7 @@ TextWithEntities ActiveAuctionsTitle(const Data::ActiveAuctions &auctions) {
).append(' ').append(tr::lng_auction_bar_active(tr::now));
}
auto result = tr::marked();
for (const auto auction : list | ranges::views::take(3)) {
for (const auto &auction : list | ranges::views::take(3)) {
result.append(Data::SingleCustomEmoji(auction->gift->document));
}
return result.append(' ').append(
@@ -1851,7 +1851,7 @@ ManyAuctionsState ActiveAuctionsState(const Data::ActiveAuctions &auctions) {
return { std::move(text), !position };
}
auto outbid = 0;
for (const auto auction : list) {
for (const auto &auction : list) {
if (!winning(auction)) {
++outbid;
}
@@ -2036,7 +2036,7 @@ Fn<void()> ActiveAuctionsCallback(
.ends = state.nextRoundAt ? state.nextRoundAt : state.endDate,
};
};
for (const auto auction : list) {
for (const auto &auction : list) {
state->list.push_back(singleFrom(*auction));
}
return [=] {
+25 -8
View File
@@ -1332,7 +1332,9 @@ void SendStarsFormRequest(
done(Payments::CheckoutResult::Failed, nullptr);
});
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
if (!ShowGiftErrorToast(show, error)) {
show->showToast(error.type());
}
done(Payments::CheckoutResult::Failed, nullptr);
}).send();
} else if (result == BalanceResult::Cancelled) {
@@ -1379,7 +1381,9 @@ void UpgradeGift(
formDone(Payments::CheckoutResult::Paid, &result);
}).fail([=](const MTP::Error &error) {
if (const auto strong = weak.get()) {
strong->showToast(error.type());
if (!ShowGiftErrorToast(strong->uiShow(), error)) {
strong->showToast(error.type());
}
}
formDone(Payments::CheckoutResult::Failed, nullptr);
}).send();
@@ -2141,7 +2145,7 @@ Controller::Controller(not_null<Main::Session*> session, PickCallback pick)
return aBirthday.day() < bBirthday.day();
});
for (const auto user : usersWithBirthdays) {
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));
@@ -2860,7 +2864,7 @@ void UpdateGiftSellPrice(
const auto newAvailableAt = base::unixtime::now() + seconds;
unique->canResellAt = newAvailableAt;
ShowResaleGiftLater(show, unique);
} else {
} else if (!ShowGiftErrorToast(show, error)) {
show->showToast(type);
}
}).send();
@@ -3061,9 +3065,10 @@ void SendOfferBuyGift(
show->session().api().applyUpdates(result);
done(true);
}).fail([=](const MTP::Error &error) {
if (error.type() == u""_q) {
} else {
show->showToast(error.type());
const auto type = error.type();
if (type == u""_q) {
} else if (!ShowGiftErrorToast(show, error)) {
show->showToast(type);
}
done(false);
}).send();
@@ -4297,7 +4302,9 @@ void RequestOurForm(
show->showToast(tr::lng_edit_privacy_gifts_restricted(tr::now));
fail(Payments::CheckoutResult::Cancelled);
} else {
show->showToast(type);
if (!ShowGiftErrorToast(show, error)) {
show->showToast(type);
}
fail(Payments::CheckoutResult::Failed);
}
}).send();
@@ -4338,6 +4345,16 @@ void ShowGiftTransferredToast(
});
}
bool ShowGiftErrorToast(
std::shared_ptr<Ui::Show> show,
const MTP::Error &error) {
if (error.type() == u"STARGIFT_ALREADY_BURNED"_q) {
show->showToast(tr::lng_gift_burned_message(tr::now));
return true;
}
return false;
}
CreditsAmount StarsFromTon(
not_null<Main::Session*> session,
CreditsAmount ton) {
@@ -39,6 +39,10 @@ namespace Payments {
enum class CheckoutResult;
} // namespace Payments
namespace MTP {
class Error;
} // namespace MTP
namespace Settings {
struct GiftWearBoxStyleOverride;
struct CreditsEntryBoxStyleOverrides;
@@ -57,6 +61,7 @@ namespace Ui {
class RpWidget;
class PopupMenu;
class GenericBox;
class Show;
class VerticalLayout;
void ChooseStarGiftRecipient(
@@ -146,6 +151,10 @@ void ShowGiftTransferredToast(
not_null<PeerData*> to,
const Data::UniqueGift &gift);
[[nodiscard]] bool ShowGiftErrorToast(
std::shared_ptr<Ui::Show> show,
const MTP::Error &error);
[[nodiscard]] CreditsAmount StarsFromTon(
not_null<Main::Session*> session,
CreditsAmount ton);
@@ -699,8 +699,12 @@ UniqueGiftCoverWidget::UniqueGiftCoverWidget(
+ st::uniqueAttributePadding.bottom();
_state->attrs->resize(_state->attrs->width(), attrsHeight);
_state->attrs->paintOn([this, astate, attrsHeight](QPainter &p) {
const auto boxPadding = st::giftBoxPadding;
const auto skip = st::giftBoxGiftSkip.x();
const auto available = _state->attrs->width() - 2 * skip;
const auto available = _state->attrs->width()
- boxPadding.left()
- boxPadding.right()
- 2 * skip;
const auto single = available / 3;
if (single <= 0) {
return;
@@ -753,10 +757,10 @@ UniqueGiftCoverWidget::UniqueGiftCoverWidget(
.position = percent.topLeft(),
});
};
auto left = 0;
auto left = boxPadding.left();
paint(left, astate->model);
paint(left + single + skip, astate->backdrop);
paint(_state->attrs->width() - single - left, astate->pattern);
paint(_state->attrs->width() - single - boxPadding.right(), astate->pattern);
});
}
_state->updateAttrs(*_state->now.gift);
@@ -810,10 +814,8 @@ UniqueGiftCoverWidget::UniqueGiftCoverWidget(
top += subtitleHeight + (skip / 2);
if (_state->attrs) {
_state->attrs->resizeToWidth(width
- st::giftBoxPadding.left()
- st::giftBoxPadding.right());
_state->attrs->moveToLeft(st::giftBoxPadding.left(), top);
_state->attrs->resizeToWidth(width);
_state->attrs->moveToLeft(0, top);
top += _state->attrs->height() + (skip / 2);
} else {
top += (skip / 2);
@@ -292,13 +292,12 @@ AbstractButton *MakeRemoveButton(
int size,
const GiftForCraft &gift,
Fn<void()> onClick,
rpl::producer<QColor> edgeColor) {
rpl::producer<QColor> edgeColor,
const style::icon &icon) {
auto remove = object_ptr<RpWidget>(parent);
const auto &icon = st::stickerPanDeleteIconFg;
const auto add = (size - icon.width()) / 2;
remove->resize(icon.size() + QSize(add, add) * 2);
remove->paintOn([=](QPainter &p) {
const auto &icon = st::stickerPanDeleteIconFg;
remove->paintOn([=, &icon](QPainter &p) {
icon.paint(p, add, add, add * 2 + icon.width(), st::white->c);
});
remove->setAttribute(Qt::WA_TransparentForMouseEvents);
@@ -381,13 +380,9 @@ AbstractButton *MakeRemoveButton(
},
}, GiftButton::Mode::CraftPreview);
entry.button->show();
if (index > 0) {
entry.button->setClickedCallback([=] {
state->editRequests.fire_copy(index);
});
} else {
entry.button->setAttribute(Qt::WA_TransparentForMouseEvents);
}
entry.button->setClickedCallback([=] {
state->editRequests.fire_copy(index);
});
entry.button->setGeometry(
geometry,
state->delegate.buttonExtend());
@@ -423,19 +418,32 @@ AbstractButton *MakeRemoveButton(
nullptr,
&Entry::button);
const auto canRemove = (count > 1);
const auto secondHasAddress = state->entries[1].gift
&& !state->entries[1].gift.unique->giftAddress.isEmpty();
for (auto i = 0; i != 4; ++i) {
auto &entry = state->entries[i];
if (entry.button) {
if (!canRemove) {
delete base::take(entry.remove);
} else if (!entry.remove) {
delete base::take(entry.remove);
if (canRemove) {
const auto needReplace = (i == 0) && secondHasAddress;
const auto callback = [=] {
if (needReplace) {
state->editRequests.fire_copy(0);
} else {
state->removeRequests.fire_copy(i);
}
};
const auto &icon = needReplace
? st::craftReplaceIcon
: st::stickerPanDeleteIconFg;
entry.remove = MakeRemoveButton(
raw,
entry.button,
entry.percent->height(),
entry.gift,
[=] { state->removeRequests.fire_copy(i); },
state->edgeColor.value());
callback,
state->edgeColor.value(),
icon);
}
}
}
@@ -664,7 +672,8 @@ void ShowSelectGiftBox(
uint64 giftId,
Fn<void(GiftForCraft)> chosen,
std::vector<GiftForCraft> selected,
Fn<void()> boxClosed) {
Fn<void()> boxClosed,
bool firstSlot) {
struct Entry {
Data::SavedStarGift gift;
GiftButton *button = nullptr;
@@ -694,10 +703,13 @@ void ShowSelectGiftBox(
const auto got = crl::guard(box, [=](
std::shared_ptr<Data::UniqueGift> gift) {
if (!ShowCraftLaterError(box->uiShow(), gift)) {
chosen(GiftForCraft{ .unique = gift });
box->closeBox();
if (ShowCraftLaterError(box->uiShow(), gift)
|| (firstSlot
&& ShowCraftAddressError(box->uiShow(), gift))) {
return;
}
chosen(GiftForCraft{ .unique = gift });
box->closeBox();
});
AddCraftGiftsList(
@@ -1194,7 +1206,7 @@ void Craft(
ShowSelectGiftBox(controller, giftId, [=](GiftForCraft chosen) {
ShowGiftCraftBox(controller, { chosen }, false);
closeCurrent();
}, {}, [=] { *requested = false; });
}, {}, [=] { *requested = false; }, true);
};
StartCraftAnimation(
box,
@@ -1419,12 +1431,14 @@ void MakeCraftContent(
}
state->chosen = std::move(copy);
};
const auto first = (state->requestingIndex == 0);
ShowSelectGiftBox(
controller,
giftId,
crl::guard(raw, callback),
state->chosen.current(),
crl::guard(raw, [=] { state->requestingIndex = -1; }));
crl::guard(raw, [=] { state->requestingIndex = -1; }),
first);
}, raw->lifetime());
auto fullName = state->chosen.value(
@@ -1501,10 +1515,14 @@ void MakeCraftContent(
if (state->crafting) {
return;
}
const auto &gifts = state->chosen.current();
if (!gifts.empty()
&& ShowCraftAddressError(box->uiShow(), gifts.front().unique)) {
return;
}
state->crafting = true;
const auto &cs = state->craftState;
const auto &gifts = state->chosen.current();
cs->giftName = state->name.current();
cs->successPermille = state->successPercentPermille.current();
@@ -1735,4 +1753,17 @@ void ShowCraftLaterError(
}));
}
bool ShowCraftAddressError(
std::shared_ptr<Show> show,
std::shared_ptr<Data::UniqueGift> gift) {
if (gift->giftAddress.isEmpty()) {
return false;
}
show->show(MakeInformBox({
.text = tr::lng_gift_craft_address_error(tr::marked),
.title = tr::lng_gift_craft_unavailable(),
}));
return true;
}
} // namespace Ui
@@ -43,4 +43,8 @@ void ShowCraftLaterError(
std::shared_ptr<Show> show,
TimeId when);
[[nodiscard]] bool ShowCraftAddressError(
std::shared_ptr<Show> show,
std::shared_ptr<Data::UniqueGift> gift);
} // namespace Ui
@@ -484,7 +484,9 @@ void TransferGift(
ShowTransferGiftLater(strong->uiShow(), gift);
}
} else if (const auto strong = weak.get()) {
strong->showToast(error.type());
if (!Ui::ShowGiftErrorToast(strong->uiShow(), error)) {
strong->showToast(type);
}
}
}).send();
} else {
@@ -512,7 +514,9 @@ void ResolveGiftSaleOffer(
session->api().applyUpdates(result);
done(true);
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
if (!Ui::ShowGiftErrorToast(show, error)) {
show->showToast(error.type());
}
done(false);
}).send();
}
+289 -82
View File
@@ -9,10 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "boxes/url_auth_box_content.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_user.h"
#include "history/history_item_components.h"
#include "history/history_item.h"
@@ -24,6 +26,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/emoji_config.h"
#include "ui/layers/generic_box.h"
#include "ui/toast/toast.h"
#include "ui/widgets/menu/menu_action.h"
@@ -38,14 +42,52 @@ namespace UrlAuthBox {
namespace {
using AnotherSessionFactory = Fn<not_null<Main::Session*>()>;
using OnUserChangedCallback = Fn<void(Fn<void()>)>;
struct SwitchAccountResult {
not_null<Ui::RpWidget*> widget;
Ui::RpWidget *widget = nullptr;
AnotherSessionFactory anotherSession;
OnUserChangedCallback setOnUserChanged;
Fn<void(UserId)> updateUserIdHint;
};
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> MakeMatchCodeImage(
not_null<Main::Session*> session,
const QString &code) {
auto emojiLength = 0;
const auto emoji = Ui::Emoji::Find(code, &emojiLength);
if (!emoji || emojiLength != code.size()) {
return nullptr;
}
const auto makeFor = [&](not_null<Main::Session*> source) {
if (const auto sticker = source->emojiStickersPack()
.stickerForEmoji(emoji)) {
return Ui::MakeEmojiThumbnail(
&source->data(),
Data::SerializeCustomEmojiId(sticker.document),
nullptr,
nullptr,
1);
}
return std::shared_ptr<Ui::DynamicImage>();
};
if (session->isTestMode()) {
for (const auto &account : Core::App().domain().orderedAccounts()) {
if (!account->sessionExists()
|| account->session().isTestMode()) {
continue;
}
if (const auto image = makeFor(&account->session())) {
return image;
}
}
}
return makeFor(session);
}
[[nodiscard]] SwitchAccountResult AddAccountsMenu(
not_null<Ui::RpWidget*> parent) {
not_null<Ui::RpWidget*> parent,
UserId userIdHint = UserId()) {
const auto session = &Core::App().domain().active().session();
const auto widget = Ui::CreateChild<SwitchableUserpicButton>(
parent,
@@ -53,15 +95,33 @@ struct SwitchAccountResult {
struct State {
base::unique_qptr<Ui::PopupMenu> menu;
UserData *currentUser = nullptr;
Fn<void()> onUserChanged;
};
const auto state = widget->lifetime().make_state<State>();
state->currentUser = session->user();
const auto isCurrentTest = session->isTestMode();
const auto findHintedUser = [&]() -> UserData* {
if (!userIdHint) {
return session->user().get();
}
for (const auto &account : Core::App().domain().orderedAccounts()) {
if (!account->sessionExists()
|| (account->session().isTestMode() != isCurrentTest)) {
continue;
}
if (account->session().userId() == userIdHint) {
return account->session().user();
}
}
return session->user().get();
};
state->currentUser = findHintedUser();
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
parent,
state->currentUser,
st::restoreUserpicIcon);
widget->setUserpic(userpic);
const auto isCurrentTest = session->isTestMode();
const auto filtered = [=] {
auto result = std::vector<not_null<Main::Session*>>();
for (const auto &account : Core::App().domain().orderedAccounts()) {
@@ -90,6 +150,9 @@ struct SwitchAccountResult {
user,
st::restoreUserpicIcon);
widget->setUserpic(newUserpic);
if (state->onUserChanged) {
state->onUserChanged();
}
});
auto owned = base::make_unique_q<Ui::Menu::Action>(
state->menu->menu(),
@@ -116,6 +179,25 @@ struct SwitchAccountResult {
return {
widget,
[=] { return &state->currentUser->session(); },
[=](Fn<void()> callback) { state->onUserChanged = callback; },
[=](UserId newUserIdHint) {
const auto isCurrentTest = session->isTestMode();
for (const auto &acc : Core::App().domain().orderedAccounts()) {
if (!acc->sessionExists()
|| (acc->session().isTestMode() != isCurrentTest)) {
continue;
}
if (acc->session().userId() == newUserIdHint) {
state->currentUser = acc->session().user();
const auto next = Ui::CreateChild<Ui::UserpicButton>(
parent,
state->currentUser,
st::restoreUserpicIcon);
widget->setUserpic(next);
break;
}
}
},
};
}
@@ -159,7 +241,8 @@ void ActivateButton(
inputPeer,
MTP_int(itemId.msg),
MTP_int(buttonId),
MTPstring() // #TODO auth url
MTPstring(), // #TODO auth url
MTPstring() // in_app_origin
)).done([=](const MTPUrlAuthResult &result) {
const auto button = HistoryMessageMarkupButton::Get(
&session->data(),
@@ -214,7 +297,8 @@ void ActivateUrl(
MTPInputPeer(),
MTPint(), // msg_id
MTPint(), // button_id
MTP_string(url)
MTP_string(url),
MTPstring() // in_app_origin
)).done([=](const MTPUrlAuthResult &result) {
result.match([&](const MTPDurlAuthResultAccepted &data) {
UrlClickHandler::Open(qs(data.vurl().value_or_empty()), context);
@@ -269,19 +353,26 @@ void RequestButton(
};
const auto callback = [=](Result result) {
if (!result.auth) {
session->api().request(MTPmessages_DeclineUrlAuth(
MTP_string(url)
)).send();
finishWithUrl(url, false);
} else if (session->data().message(itemId)) {
using Flag = MTPmessages_AcceptUrlAuth::Flag;
const auto flags = Flag(0)
| (result.allowWrite ? Flag::f_write_allowed : Flag(0))
| (result.sharePhone ? Flag::f_share_phone_number : Flag(0))
| (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code)
| (Flag::f_peer | Flag::f_msg_id | Flag::f_button_id);
session->api().request(MTPmessages_AcceptUrlAuth(
MTP_flags(flags),
inputPeer,
MTP_int(itemId.msg),
MTP_int(buttonId),
MTPstring() // #TODO auth url
MTPstring(),
result.matchCode.isEmpty()
? MTPstring()
: MTP_string(result.matchCode)
)).done([=](const MTPUrlAuthResult &result) {
const auto accepted = result.match(
[](const MTPDurlAuthResultAccepted &data) {
@@ -322,42 +413,70 @@ void RequestUrl(
not_null<Main::Session*> session,
const QString &url,
QVariant context) {
struct State {
base::weak_qptr<Ui::BoxContent> box;
AnotherSessionFactory anotherSession = nullptr;
QString firstMatchCode;
rpl::lifetime boxDeclineLifetime;
rpl::lifetime matchCodesBoxDeclineLifetime;
};
const auto bot = request.is_request_write_access()
? session->data().processUser(request.vbot()).get()
: nullptr;
const auto requestPhone = request.is_request_phone_number();
const auto matchCodesFirst = request.is_match_codes_first();
const auto domain = qs(request.vdomain());
const auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto finishWithUrl = [=](const QString &url, bool accepted) {
if (*box) {
(*box)->closeBox();
const auto userIdHint = request.vuser_id_hint()
? peerToUser(peerFromUser(*request.vuser_id_hint()))
: UserId();
const auto matchCodes = [&] {
auto result = QStringList();
if (const auto codes = request.vmatch_codes()) {
for (const auto &code : codes->v) {
result.push_back(qs(code));
}
}
return result;
}();
const auto state = std::make_shared<State>();
const auto finishWithUrl = [=](const QString &to, bool accepted) {
if (state->box) {
state->box->closeBox();
}
if (url.isEmpty() && accepted) {
if ((to.isEmpty() && accepted) || (to == url)) {
} else {
UrlClickHandler::Open(url, context);
UrlClickHandler::Open(to, context);
}
};
const auto anotherSessionFactory
= std::make_shared<AnotherSessionFactory>(nullptr);
const auto resolveSession = [=] {
return state->anotherSession ? state->anotherSession() : session;
};
const auto requestDecline = [=] {
resolveSession()->api().request(MTPmessages_DeclineUrlAuth(
MTP_string(url)
)).send();
};
const auto sendRequest = [=](Result result) {
if (!result.auth) {
requestDecline();
finishWithUrl(url, false);
} else {
const auto sharePhone = result.sharePhone;
using Flag = MTPmessages_AcceptUrlAuth::Flag;
const auto flags = Flag::f_url
| (result.allowWrite ? Flag::f_write_allowed : Flag(0))
| (sharePhone ? Flag::f_share_phone_number : Flag(0));
const auto currentSession = anotherSessionFactory
? (*anotherSessionFactory)()
: session;
currentSession->api().request(MTPmessages_AcceptUrlAuth(
| (sharePhone ? Flag::f_share_phone_number : Flag(0))
| (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code);
resolveSession()->api().request(MTPmessages_AcceptUrlAuth(
MTP_flags(flags),
MTPInputPeer(),
MTPint(), // msg_id
MTPint(), // button_id
MTP_string(url)
MTP_string(url),
result.matchCode.isEmpty()
? MTPstring()
: MTP_string(result.matchCode)
)).done([=](const MTPUrlAuthResult &result) {
const auto accepted = result.match(
[](const MTPDurlAuthResultAccepted &data) {
@@ -405,69 +524,157 @@ void RequestUrl(
const auto device = qs(request.vplatform().value_or("Unknown platform"));
const auto ip = qs(request.vip().value_or("Unknown IP"));
const auto region = qs(request.vregion().value_or("Unknown region"));
*box = show->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto callback = [=](Result result) {
if (!requestPhone) {
return sendRequest(result);
}
box->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_url_auth_phone_sure_title());
const auto confirm = [=](bool confirmed) {
return [=](Fn<void()> close) {
auto copy = result;
copy.sharePhone = confirmed;
sendRequest(copy);
close();
const auto showAuthBox = [=] {
state->box = show->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto accountResult = box->lifetime().make_state<
SwitchAccountResult>(nullptr);
const auto matchCodesShared = box->lifetime().make_state<
rpl::variable<QStringList>>(matchCodes);
const auto reloadRequest = [=] {
using Flag = MTPmessages_RequestUrlAuth::Flag;
const auto currentSession = resolveSession();
currentSession->api().request(MTPmessages_RequestUrlAuth(
MTP_flags(Flag::f_url),
MTPInputPeer(),
MTPint(), // msg_id
MTPint(), // button_id
MTP_string(url),
MTPstring() // in_app_origin
)).done(crl::guard(box, [=](const MTPUrlAuthResult &result) {
result.match([&](const MTPDurlAuthResultRequest &data) {
const auto newUserId = data.vuser_id_hint()
? peerToUser(peerFromUser(*data.vuser_id_hint()))
: UserId();
accountResult->updateUserIdHint(newUserId);
auto newCodes = QStringList();
if (const auto codes = data.vmatch_codes()) {
for (const auto &code : codes->v) {
newCodes.push_back(qs(code));
}
}
*matchCodesShared = newCodes;
}, [](const auto &) {});
})).send();
};
const auto callback = [=](Result result) {
state->boxDeclineLifetime.destroy();
if (result.matchCode.isEmpty()
&& !state->firstMatchCode.isEmpty()) {
result.matchCode = state->firstMatchCode;
}
if (!requestPhone) {
return sendRequest(result);
}
box->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_url_auth_phone_sure_title());
const auto confirm = [=](bool confirmed) {
return [=](Fn<void()> close) {
auto copy = result;
copy.sharePhone = confirmed;
sendRequest(copy);
close();
};
};
};
const auto currentSession = anotherSessionFactory
? (*anotherSessionFactory)()
: session;
const auto capitalized = [=](const QString &value) {
return value.left(1).toUpper() + value.mid(1).toLower();
};
using namespace Info::Profile;
Ui::ConfirmBox(
box,
Ui::ConfirmBoxArgs{
.text = tr::lng_url_auth_phone_sure_text(
lt_domain,
rpl::single(tr::bold(capitalized(domain))),
lt_phone,
PhoneValue(currentSession->user()),
tr::rich),
.confirmed = confirm(true),
.cancelled = confirm(false),
.confirmText = tr::lng_allow_bot(),
.cancelText = tr::lng_url_auth_phone_sure_deny(),
});
}));
};
ShowDetails(
box,
url,
domain,
callback,
bot
? object_ptr<Ui::UserpicButton>(
box->verticalLayout(),
bot,
st::defaultUserpicButton,
Ui::PeerUserpicShape::Forum)
: nullptr,
bot ? Info::Profile::NameValue(bot) : nullptr,
browser,
device,
ip,
region);
const auto currentSession = resolveSession();
const auto capitalized = [=](const QString &v) {
return v.left(1).toUpper() + v.mid(1).toLower();
};
using namespace Info::Profile;
Ui::ConfirmBox(
box,
Ui::ConfirmBoxArgs{
.text = tr::lng_url_auth_phone_sure_text(
lt_domain,
rpl::single(tr::bold(capitalized(domain))),
lt_phone,
PhoneValue(currentSession->user()),
tr::rich),
.confirmed = confirm(true),
.cancelled = confirm(false),
.confirmText = tr::lng_allow_bot(),
.cancelText = tr::lng_url_auth_phone_sure_deny(),
});
}));
};
ShowDetails(
box,
url,
domain,
[=](QString code) -> std::shared_ptr<Ui::DynamicImage> {
return MakeMatchCodeImage(resolveSession(), code);
},
callback,
(bot
? object_ptr<Ui::UserpicButton>(
box->verticalLayout(),
bot,
st::defaultUserpicButton,
Ui::PeerUserpicShape::Forum)
: nullptr),
bot ? Info::Profile::NameValue(bot) : nullptr,
browser,
device,
ip,
region,
(matchCodesFirst
? (rpl::single(QStringList()) | rpl::type_erased)
: matchCodesShared->value()));
const auto content = box->verticalLayout();
const auto accountResult = AddAccountsMenu(content);
content->widthValue() | rpl::on_next([=, w = accountResult.widget] {
w->moveToRight(st::lineWidth * 4, 0);
}, accountResult.widget->lifetime());
*anotherSessionFactory = accountResult.anotherSession;
}));
*accountResult = AddAccountsMenu(
box->verticalLayout(),
userIdHint);
box->verticalLayout()->widthValue(
) | rpl::on_next([=, w = (*accountResult).widget] {
w->moveToRight(st::lineWidth * 4, 0);
}, (*accountResult).widget->lifetime());
state->anotherSession = (*accountResult).anotherSession;
(*accountResult).setOnUserChanged(reloadRequest);
}));
state->box->boxClosing() | rpl::on_next([=] {
requestDecline();
}, state->boxDeclineLifetime);
};
if (!matchCodesFirst || matchCodes.isEmpty()) {
showAuthBox();
return;
}
auto matchCodesBox = base::weak_qptr<Ui::BoxContent>();
matchCodesBox = show->show(
Box([=](not_null<Ui::GenericBox*> matchBox) {
ShowMatchCodesBox(
matchBox,
[=](QString code) -> std::shared_ptr<Ui::DynamicImage> {
return MakeMatchCodeImage(resolveSession(), code);
},
domain,
matchCodes,
[=](QString matchCode) {
state->matchCodesBoxDeclineLifetime.destroy();
resolveSession()->api().request(
MTPmessages_CheckUrlAuthMatchCode(
MTP_string(url),
MTP_string(matchCode))
).done([=](const MTPBool &result) {
if (!mtpIsTrue(result)) {
show->showToast(
tr::lng_url_auth_phone_toast_bad_expired(
tr::now));
return;
}
state->firstMatchCode = std::move(matchCode);
showAuthBox();
}).fail([=](const MTP::Error &error) {
show->showToast((error.type() == u"URL_EXPIRED"_q)
? tr::lng_url_auth_phone_toast_bad_expired(
tr::now)
: error.type());
}).send();
});
}),
Ui::LayerOption::KeepOther);
matchCodesBox->boxClosing() | rpl::on_next([=] {
requestDecline();
}, state->matchCodesBoxDeclineLifetime);
}
} // namespace UrlAuthBox
@@ -9,13 +9,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/qthelp_url.h"
#include "lang/lang_keys.h"
#include "ui/dynamic_image.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/horizontal_fit_container.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/tooltip.h"
#include "ui/emoji_config.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/ui_utility.h"
#include "styles/style_boxes.h"
@@ -26,8 +29,184 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace UrlAuthBox {
namespace {
constexpr auto kEmojiAnimationActiveFor = crl::time(250);
void PrepareFullWidthRoundButton(
not_null<Ui::RoundButton*> button,
not_null<Ui::VerticalLayout*> content,
const style::margins &padding) {
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->setFullRadius(true);
const auto paddingHorizontal = padding.left() + padding.right();
content->widthValue() | rpl::on_next([=](int w) {
button->resize(w - paddingHorizontal, button->height());
}, button->lifetime());
}
} // namespace
void ShowMatchCodesBox(
not_null<Ui::GenericBox*> box,
Fn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,
const QString &domain,
const QStringList &codes,
Fn<void(QString)> callback) {
box->setWidth(st::boxWidth);
box->setStyle(st::urlAuthBox);
const auto content = box->verticalLayout();
Ui::AddSkip(content);
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_url_auth_match_code_title(),
st::urlAuthCodesTitle),
st::boxRowPadding,
style::al_top);
Ui::AddSkip(content);
Ui::AddSkip(content);
const auto buttons = content->add(
object_ptr<Ui::HorizontalFitContainer>(
content,
st::boxRowPadding.left() * 2),
st::boxRowPadding);
buttons->resize(0, st::urlAuthCodesButton.height);
for (const auto &code : codes) {
auto emojiLength = 0;
const auto emoji = Ui::Emoji::Find(code, &emojiLength);
const auto emojiCode = (emoji && (emojiLength == code.size()));
const auto button = Ui::CreateChild<Ui::RoundButton>(
buttons,
rpl::single(emojiCode ? QString() : code),
st::urlAuthCodesButton);
if (emojiCode) {
button->setTextFgOverride(QColor(Qt::transparent));
const auto overlay = Ui::CreateChild<Ui::RpWidget>(button);
overlay->setAttribute(Qt::WA_TransparentForMouseEvents);
overlay->show();
struct State {
std::shared_ptr<Ui::DynamicImage> image;
bool hovered = false;
crl::time lastFrameUpdate = 0;
};
const auto state = overlay->lifetime().make_state<State>();
const auto animationActive = [=] {
return state->lastFrameUpdate
&& (crl::now() - state->lastFrameUpdate
<= kEmojiAnimationActiveFor);
};
const auto refreshImage = [=] {
if (state->image) {
state->image->subscribeToUpdates(nullptr);
}
state->image = emojiImageFactory
? emojiImageFactory(code)
: nullptr;
if (state->image) {
state->image->subscribeToUpdates([=] {
state->lastFrameUpdate = crl::now();
overlay->update();
});
}
overlay->update();
};
refreshImage();
overlay->lifetime().add([=] {
if (state->image) {
state->image->subscribeToUpdates(nullptr);
}
});
button->events() | rpl::on_next([=](not_null<QEvent*> e) {
switch (e->type()) {
case QEvent::Enter:
if (!state->hovered) {
state->hovered = true;
if (!animationActive()) {
refreshImage();
}
}
break;
case QEvent::Leave:
state->hovered = false;
break;
default:
break;
}
}, overlay->lifetime());
button->sizeValue() | rpl::on_next([=](QSize size) {
overlay->resize(size);
}, overlay->lifetime());
overlay->paintOn([=](QPainter &p) {
if (state->image) {
const auto side = st::urlAuthCodesButton.height;
const auto frame = state->image->image(side);
const auto visible = frame.isNull()
? 0
: (frame.width() / frame.devicePixelRatio());
p.drawImage(
QPoint(
(overlay->width() - visible) / 2,
(overlay->height() - visible) / 2),
frame);
return;
}
const auto size = Ui::Emoji::GetSizeLarge();
const auto visible = size / style::DevicePixelRatio();
Ui::Emoji::Draw(
p,
emoji,
size,
(overlay->width() - visible) / 2,
(overlay->height() - visible) / 2);
});
}
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->setFullRadius(true);
button->setClickedCallback([=] {
callback(code);
box->closeBox();
});
buttons->add(button);
}
Ui::AddSkip(content);
const auto domainUrl = qthelp::validate_url(domain);
if (!domainUrl.isEmpty()) {
Ui::AddSkip(content);
Ui::AddSkip(content);
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_url_auth_login_title(
lt_domain,
rpl::single(Ui::Text::Link(domain, domainUrl)),
tr::marked),
st::urlAuthCheckboxAbout),
st::boxRowPadding,
style::al_top);
}
Ui::AddSkip(content);
Ui::AddSkip(content);
{
const auto &padding = st::boxRowPadding;
const auto button = content->add(
object_ptr<Ui::RoundButton>(
content,
tr::lng_cancel(),
st::attentionBoxButton),
padding);
PrepareFullWidthRoundButton(button, content, padding);
button->setClickedCallback([=] {
box->closeBox();
});
}
}
SwitchableUserpicButton::SwitchableUserpicButton(
not_null<Ui::RpWidget*> parent,
int size)
@@ -246,14 +425,17 @@ void ShowDetails(
not_null<Ui::GenericBox*> box,
const QString &url,
const QString &domain,
Fn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,
Fn<void(Result)> callback,
object_ptr<Ui::RpWidget> userpicOwned,
rpl::producer<QString> botName,
const QString &browser,
const QString &platform,
const QString &ip,
const QString &region) {
const QString &region,
rpl::producer<QStringList> matchCodes) {
box->setWidth(st::boxWidth);
box->setStyle(st::urlAuthBox);
const auto content = box->verticalLayout();
@@ -342,13 +524,64 @@ void ShowDetails(
Ui::AddSkip(content);
}
box->addButton(tr::lng_url_auth_login_button(), [=] {
callback({
.auth = true,
.allowWrite = (allowMessages && allowMessages->toggled()),
struct State {
QStringList matchCodes;
};
const auto state = box->lifetime().make_state<State>();
std::move(matchCodes) | rpl::on_next([=](const QStringList &codes) {
state->matchCodes = codes;
}, box->lifetime());
Ui::AddSkip(content);
{
const auto &padding = st::boxRowPadding;
const auto button = content->add(
object_ptr<Ui::RoundButton>(
content,
tr::lng_url_auth_login_button(),
st::defaultLightButton),
padding);
PrepareFullWidthRoundButton(button, content, padding);
button->setClickedCallback([=] {
if (state->matchCodes.isEmpty()) {
callback({
.auth = true,
.allowWrite = (allowMessages && allowMessages->toggled()),
});
return;
}
box->uiShow()->show(Box([=](
not_null<Ui::GenericBox*> matchCodesBox) {
ShowMatchCodesBox(
matchCodesBox,
emojiImageFactory,
domain,
state->matchCodes,
[=](QString matchCode) {
callback({
.auth = true,
.allowWrite = (allowMessages
&& allowMessages->toggled()),
.matchCode = std::move(matchCode),
});
});
}));
});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
Ui::AddSkip(content);
{
const auto &padding = st::boxRowPadding;
const auto button = content->add(
object_ptr<Ui::RoundButton>(
content,
tr::lng_suggest_action_decline(),
st::attentionBoxButton),
padding);
PrepareFullWidthRoundButton(button, content, padding);
button->setClickedCallback([=] {
box->closeBox();
});
}
}
} // namespace UrlAuthBox
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h"
namespace Ui {
class DynamicImage;
class GenericBox;
class VerticalLayout;
} // namespace Ui
@@ -20,6 +21,7 @@ struct Result {
bool auth : 1 = false;
bool allowWrite : 1 = false;
bool sharePhone : 1 = false;
QString matchCode;
};
class SwitchableUserpicButton final : public Ui::RippleButton {
@@ -51,6 +53,13 @@ void AddAuthInfoRow(
const QString &leftText,
const style::icon &icon);
void ShowMatchCodesBox(
not_null<Ui::GenericBox*> box,
Fn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,
const QString &domain,
const QStringList &codes,
Fn<void(QString)> callback);
void Show(
not_null<Ui::GenericBox*> box,
const QString &url,
@@ -63,12 +72,14 @@ void ShowDetails(
not_null<Ui::GenericBox*> box,
const QString &url,
const QString &domain,
Fn<std::shared_ptr<Ui::DynamicImage>(QString)> emojiImageFactory,
Fn<void(Result)> callback,
object_ptr<Ui::RpWidget> userpicOwned,
rpl::producer<QString> botName,
const QString &browser,
const QString &platform,
const QString &ip,
const QString &region);
const QString &region,
rpl::producer<QStringList> matchCodes = rpl::single(QStringList()));
} // namespace UrlAuthBox
+23 -6
View File
@@ -145,11 +145,17 @@ Panel::Panel(not_null<Call*> call)
, _controlsShownForceTimer([=] { controlsShownForce(false); }) {
_decline->setDuration(st::callPanelDuration);
_decline->entity()->setText(tr::lng_call_decline());
_decline->entity()->setAccessibleName(tr::lng_call_decline(tr::now));
_cancel->setDuration(st::callPanelDuration);
_cancel->entity()->setText(tr::lng_call_cancel());
_cancel->entity()->setAccessibleName(tr::lng_call_cancel(tr::now));
_screencast->setDuration(st::callPanelDuration);
_screencast->entity()->setAccessibleName(tr::lng_call_screencast(tr::now));
_addPeople->setDuration(st::callPanelDuration);
_addPeople->entity()->setText(tr::lng_call_add_people());
_addPeople->entity()->setAccessibleName(tr::lng_call_add_people(tr::now));
_camera->setAccessibleName(tr::lng_call_start_video(tr::now));
_mute->entity()->setAccessibleName(tr::lng_call_mute_audio(tr::now));
initWindow();
initWidget();
@@ -732,6 +738,9 @@ void Panel::reinitWithCall(Call *call) {
_mute->entity()->setText(mute
? tr::lng_call_unmute_audio()
: tr::lng_call_mute_audio());
_mute->entity()->setAccessibleName(mute
? tr::lng_call_unmute_audio(tr::now)
: tr::lng_call_mute_audio(tr::now));
}, _callLifetime);
_call->videoOutgoing()->stateValue(
@@ -742,6 +751,9 @@ void Panel::reinitWithCall(Call *call) {
_camera->setText(active
? tr::lng_call_stop_video()
: tr::lng_call_start_video());
_camera->setAccessibleName(active
? tr::lng_call_stop_video(tr::now)
: tr::lng_call_start_video(tr::now));
}
{
const auto active = _call->isSharingScreen();
@@ -1030,12 +1042,14 @@ void Panel::initMediaDeviceToggles() {
{ Webrtc::DeviceType::Camera, _call->cameraDeviceIdValue() },
});
});
_cameraDeviceToggle->setAccessibleName(tr::lng_settings_call_camera(tr::now));
_audioDeviceToggle->setClickedCallback([=] {
showDevicesMenu(_audioDeviceToggle, {
{ Webrtc::DeviceType::Playback, _call->playbackDeviceIdValue() },
{ Webrtc::DeviceType::Capture, _call->captureDeviceIdValue() },
});
});
_audioDeviceToggle->setAccessibleName(tr::lng_settings_call_section_output(tr::now));
}
void Panel::showDevicesMenu(
@@ -1381,6 +1395,7 @@ void Panel::stateChanged(State state) {
st::callStartVideo);
_startVideo->show();
_startVideo->setText(tr::lng_call_start_video());
_startVideo->setAccessibleName(tr::lng_call_start_video(tr::now));
_startVideo->clicks() | rpl::map_to(true) | rpl::start_to_stream(
_startOutgoingRequests,
_startVideo->lifetime());
@@ -1438,15 +1453,17 @@ void Panel::stateChanged(State state) {
void Panel::refreshAnswerHangupRedialLabel() {
Expects(_answerHangupRedialState.has_value());
_answerHangupRedial->setText([&] {
const auto phrase = [&] {
switch (*_answerHangupRedialState) {
case AnswerHangupRedialState::Answer: return tr::lng_call_accept();
case AnswerHangupRedialState::Hangup: return tr::lng_call_end_call();
case AnswerHangupRedialState::Redial: return tr::lng_call_redial();
case AnswerHangupRedialState::StartCall: return tr::lng_call_start();
case AnswerHangupRedialState::Answer: return tr::lng_call_accept;
case AnswerHangupRedialState::Hangup: return tr::lng_call_end_call;
case AnswerHangupRedialState::Redial: return tr::lng_call_redial;
case AnswerHangupRedialState::StartCall: return tr::lng_call_start;
}
Unexpected("AnswerHangupRedialState value.");
}());
}();
_answerHangupRedial->setText(phrase());
_answerHangupRedial->setAccessibleName(phrase(tr::now));
}
void Panel::updateStatusText(State state) {
@@ -344,7 +344,7 @@ void ConfInviteRow::elementsPaint(
}
void prepare() override {
for (const auto user : _users) {
for (const auto &user : _users) {
delegate()->peerListAppendRow(
std::make_unique<ConfInviteRow>(user, _st));
}
@@ -485,7 +485,7 @@ ConfInviteController::ConfInviteController(
, _shareLink(std::move(shareLink)) {
if (!_shareLink) {
_skip.reserve(_prioritize.size());
for (const auto user : _prioritize) {
for (const auto &user : _prioritize) {
_skip.emplace(user);
}
}
@@ -747,7 +747,9 @@ std::unique_ptr<PeerListRow> InviteController::createRow(
|| user->isInaccessible()) {
return nullptr;
}
auto result = std::make_unique<PeerListRow>(user);
auto result = std::make_unique<Row>(
user,
Type{ .chatStyle = _chatStyle.get(), .circleCache = &_pillCircleCache });
_rowAdded.fire_copy(user);
_inGroup.emplace(user);
if (isAlreadyIn(user)) {
@@ -616,6 +616,7 @@ void Panel::initControls() {
}, lifetime());
_hangup->setClickedCallback([=] { endCall(); });
_hangup->setAccessibleName(tr::lng_group_call_leave(tr::now));
const auto scheduleDate = _call->scheduleDate();
if (scheduleDate) {
@@ -701,12 +702,14 @@ void Panel::refreshLeftButton() {
_settings.destroy();
_callShare.create(widget(), st::groupCallShare);
_callShare->setClickedCallback(_callShareLinkCallback);
_callShare->setAccessibleName(tr::lng_group_call_invite(tr::now));
} else {
_callShare.destroy();
_settings.create(widget(), st::groupCallSettings);
_settings->setClickedCallback([=] {
uiShow()->showBox(Box(SettingsBox, _call));
});
_settings->setAccessibleName(tr::lng_group_call_settings_title(tr::now));
trackControls(_trackControls, true);
}
const auto raw = _callShare ? _callShare.data() : _settings.data();
@@ -754,6 +757,7 @@ void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
StickedTooltipHide::Activated);
_call->toggleVideo(!_call->isSharingCamera());
});
_video->setAccessibleName(tr::lng_call_start_video(tr::now));
_video->setColorOverrides(
toggleableOverrides(_call->isSharingCameraValue()));
_call->isSharingCameraValue(
@@ -764,6 +768,9 @@ void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
StickedTooltipHide::Activated);
}
_video->setProgress(sharing ? 1. : 0.);
_video->setAccessibleName(sharing
? tr::lng_call_stop_video(tr::now)
: tr::lng_call_start_video(tr::now));
}, _video->lifetime());
}
if (!_screenShare) {
@@ -772,17 +779,22 @@ void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
_screenShare->setClickedCallback([=] {
chooseShareScreenSource();
});
_screenShare->setAccessibleName(tr::lng_group_call_screen_share_start(tr::now));
_screenShare->setColorOverrides(
toggleableOverrides(_call->isSharingScreenValue()));
_call->isSharingScreenValue(
) | rpl::on_next([=](bool sharing) {
_screenShare->setProgress(sharing ? 1. : 0.);
_screenShare->setAccessibleName(sharing
? tr::lng_group_call_screen_share_stop(tr::now)
: tr::lng_group_call_screen_share_start(tr::now));
}, _screenShare->lifetime());
}
if (!_wideMenu) {
_wideMenu.create(widget(), st::groupCallMenuToggleSmall);
_wideMenu->show();
_wideMenu->setClickedCallback([=] { showMainMenu(); });
_wideMenu->setAccessibleName(tr::lng_sr_group_call_menu(tr::now));
_wideMenu->setColorOverrides(
toggleableOverrides(_wideMenuShown.value()));
}
@@ -799,6 +811,7 @@ void Panel::createMessageButton() {
&st::groupCallMessageActiveSmall);
_message->show();
_message->setClickedCallback([=] { toggleMessageTyping(); });
_message->setAccessibleName(tr::lng_group_call_message(tr::now));
_message->setColorOverrides(
toggleableOverrides(_messageTyping.value()));
}
@@ -880,8 +893,7 @@ void Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {
const auto wide = (mode == PanelMode::Wide);
using Type = Ui::CallMuteButtonType;
using ExpandType = Ui::CallMuteButtonExpandType;
_mute->setState(Ui::CallMuteButtonState{
.text = (wide
const auto text = (wide
? QString()
: scheduleDate
? (canManage
@@ -897,7 +909,10 @@ void Panel::setupRealMuteButtonState(not_null<Data::GroupCall*> real) {
? tr::lng_group_call_raised_hand(tr::now)
: mute == MuteState::Muted
? tr::lng_group_call_unmute(tr::now)
: tr::lng_group_call_you_are_live(tr::now)),
: tr::lng_group_call_you_are_live(tr::now));
_mute->outer()->setAccessibleName(text);
_mute->setState(Ui::CallMuteButtonState{
.text = text,
.tooltip = ((!scheduleDate && mute == MuteState::Muted)
? tr::lng_group_call_unmute_sub(tr::now)
: QString()),
@@ -1480,6 +1495,7 @@ void Panel::refreshTopButton() {
if (!_menuToggle) {
_menuToggle.create(widget(), st::groupCallMenuToggle);
_menuToggle->show();
_menuToggle->setAccessibleName(tr::lng_sr_group_call_menu(tr::now));
_menuToggle->setClickedCallback([=] { showMainMenu(); });
updateControlsGeometry();
raiseControls();
@@ -198,7 +198,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
comment.text = link;
}
auto &api = peer->session().api();
for (const auto thread : result) {
for (const auto &thread : result) {
auto message = Api::MessageToSend(
Api::SendAction(thread, options));
message.textWithTags = comment;
@@ -112,10 +112,10 @@ void Style::paintButtonBg(
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush((color == Color::Primary)
? QColor(0x29, 0x8a, 0xcf)
? st::botKbPrimaryBg->c
: (color == Color::Danger)
? QColor(0xe0, 0x53, 0x56)
: QColor(0x61, 0xc7, 0x52));
? st::botKbDangerBg->c
: st::botKbSuccessBg->c);
const auto radius = st::roundRadiusSmall;
p.drawRoundedRect(rect, radius, radius);
}
@@ -1423,6 +1423,7 @@ defaultComposeFilesMenu: IconButton(defaultIconButton) {
defaultComposeFilesField: InputField(defaultInputField) {
textMargins: margins(1px, 26px, 31px, 4px);
heightMax: 158px;
style: historyTextStyle;
}
defaultComposeFiles: ComposeFiles {
check: defaultCheck;
@@ -22,12 +22,12 @@ ResolveWindow ResolveWindowDefault() {
return [](not_null<Main::Session*> session)
-> Window::SessionController* {
const auto check = [&](Window::Controller *window) {
if (const auto controller = window->sessionController()) {
if (&controller->session() == session) {
return controller;
}
}
return (Window::SessionController*)nullptr;
const auto controller = window
? window->sessionController()
: nullptr;
return (controller && (&controller->session() == session))
? controller
: nullptr;
};
auto &app = Core::App();
const auto account = not_null(&session->account());
@@ -43,7 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/cached_round_corners.h"
#include "base/unixtime.h"
#include "base/random.h"
#include "base/qt/qt_common_adapters.h"
#include "base/qt/qt_key_modifiers.h"
#include "boxes/sticker_set_box.h"
#include "window/window_adaptive.h"
#include "window/window_session_controller.h"
@@ -507,7 +507,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
sorted.emplace(byOnline(user), user);
}
}
for (const auto user : _chat->lastAuthors) {
for (const auto &user : _chat->lastAuthors) {
if (user->isInaccessible()) continue;
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
@@ -524,7 +524,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
_channel->session().api().chatParticipants().requestAdmins(_channel);
} else {
mrows.reserve(mrows.size() + _channel->mgInfo->admins.size());
for (const auto &[userId, rank] : _channel->mgInfo->admins) {
for (const auto &userId : _channel->mgInfo->admins) {
if (const auto user = _channel->owner().userLoaded(userId)) {
if (user->isInaccessible()) continue;
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
@@ -538,7 +538,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
_channel);
} else {
mrows.reserve(mrows.size() + _channel->mgInfo->lastParticipants.size());
for (const auto user : _channel->mgInfo->lastParticipants) {
for (const auto &user : _channel->mgInfo->lastParticipants) {
if (user->isInaccessible()) continue;
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
@@ -1674,7 +1674,9 @@ void InitFieldAutocomplete(
raw->mentionChosen(
) | rpl::on_next([=](FieldAutocomplete::MentionChosen data) {
const auto user = data.user;
if (data.mention.isEmpty()) {
const auto ctrlClick = base::IsCtrlPressed()
&& data.method == FieldAutocomplete::ChooseMethod::ByClick;
if (data.mention.isEmpty() || ctrlClick) {
field->insertTag(
user->firstName.isEmpty() ? user->name() : user->firstName,
PrepareMentionTag(user));
@@ -17,6 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/event_filter.h"
#include "ui/chat/chat_style.h"
#include "ui/layers/generic_box.h"
#include "ui/boxes/calendar_box.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/basic_click_handlers.h"
#include "ui/rect.h"
#include "core/shortcuts.h"
@@ -410,8 +412,68 @@ Fn<bool(
QString link,
EditLinkAction action) {
if (action == EditLinkAction::Check) {
return Ui::InputField::IsValidMarkdownLink(link)
&& !TextUtilities::IsMentionLink(link);
return (Ui::InputField::IsValidMarkdownLink(link)
&& !TextUtilities::IsMentionLink(link))
|| Ui::InputField::IsCustomDateLink(link);
}
if (Ui::InputField::IsCustomDateLink(link)) {
const auto dateStr = link.mid(
Ui::InputField::kCustomDateTagStart.size());
const auto existingDate = dateStr.toInt();
auto callback = [=](
const TextWithTags &t,
const QString &l) {
if (const auto strong = weak.get()) {
strong->commitMarkdownLinkEdit(selection, t, l);
}
};
const auto savedCallback = std::make_shared<
Fn<void(const TextWithTags &, const QString &)>>(
std::move(callback));
const auto savedText = std::make_shared<TextWithTags>(text);
const auto showDateTimeBox = [=](TimeId time) {
const auto dateBox = std::make_shared<
base::weak_qptr<Ui::GenericBox>>();
*dateBox = show->show(Box(
Ui::ChooseDateTimeBox,
Ui::ChooseDateTimeBoxArgs{
.title = tr::lng_formatting_date_title(),
.submit = tr::lng_settings_save(),
.done = [=](TimeId result) {
const auto dateLink
= Ui::InputField::kCustomDateTagStart
+ QString::number(result);
(*savedCallback)(
*savedText,
dateLink);
if (const auto box = dateBox->get()) {
box->closeBox();
}
},
.min = [] { return TimeId(1); },
.time = time,
.max = [] { return TimeId(2114380800); },
}));
};
if (existingDate > 0) {
showDateTimeBox(existingDate);
} else {
show->show(Box<Ui::CalendarBox>(Ui::CalendarBoxArgs{
.month = QDate::currentDate(),
.highlighted = QDate::currentDate(),
.callback = [=](QDate chosen, Fn<void()> close) {
close();
const auto midday = QDateTime(
chosen,
QTime(12, 0));
showDateTimeBox(
base::unixtime::serialize(midday));
},
.minDate = QDate(1970, 1, 1),
.maxDate = QDate(2036, 12, 31),
}));
}
return true;
}
auto callback = [=](const TextWithTags &text, const QString &link) {
if (const auto strong = weak.get()) {
@@ -1569,3 +1631,22 @@ void FrozenInfoBox(
button->resizeToWidth(buttonWidth);
}, button->lifetime());
}
Ui::InputField::MimeDataHook WrappedMessageFieldMimeHook(
Ui::InputField::MimeDataHook original,
not_null<Ui::InputField*> field) {
return [field, originalHook = std::move(original)](
not_null<const QMimeData*> data,
Ui::InputField::MimeAction action) {
if (data->hasFormat(u"application/x-telegram-input-field"_q)) {
if (action == Ui::InputField::MimeAction::Check) {
return true;
}
const auto text = QString::fromUtf8(
data->data(u"application/x-telegram-input-field"_q));
field->textCursor().insertText(text);
return true;
}
return originalHook ? originalHook(data, action) : false;
};
}
@@ -219,3 +219,7 @@ void FrozenInfoBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
FreezeInfoStyleOverride st);
[[nodiscard]] Ui::InputField::MimeDataHook WrappedMessageFieldMimeHook(
Ui::InputField::MimeDataHook original,
not_null<Ui::InputField*> field);
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "chat_helpers/stickers_list_widget.h"
#include "base/options.h"
#include "base/timer_rpl.h"
#include "core/application.h"
#include "data/data_document.h"
@@ -76,12 +77,20 @@ using Data::StickersPack;
using Data::StickersSetThumbnailView;
using SetFlag = Data::StickersSetFlag;
base::options::toggle OptionUnlimitedRecentStickers({
.id = kOptionUnlimitedRecentStickers,
.name = "Unlimited recent stickers",
.description = "Display as much recent stickers as the server provides",
});
[[nodiscard]] bool SetInMyList(Data::StickersSetFlags flags) {
return (flags & SetFlag::Installed) && !(flags & SetFlag::Archived);
}
} // namespace
const char kOptionUnlimitedRecentStickers[] = "unlimited-recent-stickers";
struct StickersListWidget::Sticker {
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> documentMedia;
@@ -56,6 +56,8 @@ struct FlatLabel;
namespace ChatHelpers {
extern const char kOptionUnlimitedRecentStickers[];
struct StickerIcon;
enum class ValidateIconAnimations;
class StickersListFooter;
@@ -324,18 +324,14 @@ QSize ComputeStickerSize(not_null<DocumentData*> document, QSize box) {
not_null<DocumentData*> GenerateLocalSticker(
not_null<Main::Session*> session,
const QString &path) {
auto task = FileLoadTask(
session,
path,
QByteArray(),
nullptr,
nullptr,
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
false,
nullptr,
LocalStickerId(path));
auto task = FileLoadTask(FileLoadTask::Args{
.session = session,
.filepath = path,
.type = SendMediaType::File,
.to = FileLoadTo(0, {}, {}, 0),
.caption = {},
.idOverride = LocalStickerId(path),
});
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);

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